2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.boschindego.internal.handler;
15 import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.*;
17 import java.time.Instant;
18 import java.time.ZonedDateTime;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.eclipse.jetty.client.HttpClient;
25 import org.openhab.binding.boschindego.internal.BoschIndegoTranslationProvider;
26 import org.openhab.binding.boschindego.internal.DeviceStatus;
27 import org.openhab.binding.boschindego.internal.IndegoController;
28 import org.openhab.binding.boschindego.internal.config.BoschIndegoConfiguration;
29 import org.openhab.binding.boschindego.internal.dto.DeviceCommand;
30 import org.openhab.binding.boschindego.internal.dto.response.DeviceStateResponse;
31 import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException;
32 import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
33 import org.openhab.core.i18n.TimeZoneProvider;
34 import org.openhab.core.library.types.DateTimeType;
35 import org.openhab.core.library.types.DecimalType;
36 import org.openhab.core.library.types.PercentType;
37 import org.openhab.core.library.types.StringType;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.binding.BaseThingHandler;
43 import org.openhab.core.types.Command;
44 import org.openhab.core.types.RefreshType;
45 import org.openhab.core.types.UnDefType;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
50 * The {@link BoschIndegoHandler} is responsible for handling commands, which are
51 * sent to one of the channels.
53 * @author Jonas Fleck - Initial contribution
54 * @author Jacob Laursen - Refactoring, bugfixing and removal of dependency towards abandoned library
57 public class BoschIndegoHandler extends BaseThingHandler {
59 private final Logger logger = LoggerFactory.getLogger(BoschIndegoHandler.class);
60 private final HttpClient httpClient;
61 private final BoschIndegoTranslationProvider translationProvider;
62 private final TimeZoneProvider timeZoneProvider;
64 private @NonNullByDefault({}) IndegoController controller;
65 private @Nullable ScheduledFuture<?> statePollFuture;
66 private @Nullable ScheduledFuture<?> cuttingTimePollFuture;
67 private boolean propertiesInitialized;
68 private int previousStateCode;
70 public BoschIndegoHandler(Thing thing, HttpClient httpClient, BoschIndegoTranslationProvider translationProvider,
71 TimeZoneProvider timeZoneProvider) {
73 this.httpClient = httpClient;
74 this.translationProvider = translationProvider;
75 this.timeZoneProvider = timeZoneProvider;
79 public void initialize() {
80 logger.debug("Initializing Indego handler");
81 BoschIndegoConfiguration config = getConfigAs(BoschIndegoConfiguration.class);
82 String username = config.username;
83 String password = config.password;
85 if (username == null || username.isBlank()) {
86 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
87 "@text/offline.conf-error.missing-username");
90 if (password == null || password.isBlank()) {
91 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
92 "@text/offline.conf-error.missing-password");
96 controller = new IndegoController(httpClient, username, password);
98 updateStatus(ThingStatus.UNKNOWN);
99 this.statePollFuture = scheduler.scheduleWithFixedDelay(this::refreshStateWithExceptionHandling, 0,
100 config.refresh, TimeUnit.SECONDS);
101 this.cuttingTimePollFuture = scheduler.scheduleWithFixedDelay(this::refreshCuttingTimesWithExceptionHandling, 0,
102 config.cuttingTimeRefresh, TimeUnit.MINUTES);
106 public void dispose() {
107 logger.debug("Disposing Indego handler");
108 ScheduledFuture<?> pollFuture = this.statePollFuture;
109 if (pollFuture != null) {
110 pollFuture.cancel(true);
112 this.statePollFuture = null;
113 pollFuture = this.cuttingTimePollFuture;
114 if (pollFuture != null) {
115 pollFuture.cancel(true);
117 this.cuttingTimePollFuture = null;
121 public void handleCommand(ChannelUID channelUID, Command command) {
123 if (command == RefreshType.REFRESH) {
124 handleRefreshCommand(channelUID.getId());
128 if (command instanceof DecimalType && channelUID.getId().equals(STATE)) {
129 sendCommand(((DecimalType) command).intValue());
131 } catch (IndegoAuthenticationException e) {
132 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
133 "@text/offline.comm-error.authentication-failure");
134 } catch (IndegoException e) {
135 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
139 private void handleRefreshCommand(String channelId) throws IndegoAuthenticationException, IndegoException {
151 this.refreshCuttingTimes();
156 private void sendCommand(int commandInt) throws IndegoException {
157 DeviceCommand command;
158 switch (commandInt) {
160 command = DeviceCommand.MOW;
163 command = DeviceCommand.RETURN;
166 command = DeviceCommand.PAUSE;
169 logger.warn("Invalid command {}", commandInt);
173 DeviceStateResponse state = controller.getState();
174 DeviceStatus deviceStatus = DeviceStatus.fromCode(state.state);
175 if (!verifyCommand(command, deviceStatus, state.error)) {
178 logger.debug("Sending command {}", command);
179 updateState(TEXTUAL_STATE, UnDefType.UNDEF);
180 controller.sendCommand(command);
181 state = controller.getState();
182 updateStatus(ThingStatus.ONLINE);
186 private void refreshStateWithExceptionHandling() {
189 } catch (IndegoAuthenticationException e) {
190 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
191 "@text/offline.comm-error.authentication-failure");
192 } catch (IndegoException e) {
193 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
197 private void refreshState() throws IndegoAuthenticationException, IndegoException {
198 if (!propertiesInitialized) {
199 getThing().setProperty(Thing.PROPERTY_SERIAL_NUMBER, controller.getSerialNumber());
200 propertiesInitialized = true;
203 DeviceStateResponse state = controller.getState();
204 updateStatus(ThingStatus.ONLINE);
207 // When state code changed, refresh cutting times immediately.
208 if (state.state != previousStateCode) {
209 refreshCuttingTimes();
210 previousStateCode = state.state;
214 private void refreshCuttingTimesWithExceptionHandling() {
216 refreshCuttingTimes();
217 } catch (IndegoAuthenticationException e) {
218 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
219 "@text/offline.comm-error.authentication-failure");
220 } catch (IndegoException e) {
221 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
225 private void refreshCuttingTimes() throws IndegoAuthenticationException, IndegoException {
226 if (isLinked(LAST_CUTTING)) {
227 Instant lastCutting = controller.getPredictiveLastCutting();
228 if (lastCutting != null) {
229 updateState(LAST_CUTTING,
230 new DateTimeType(ZonedDateTime.ofInstant(lastCutting, timeZoneProvider.getTimeZone())));
232 updateState(LAST_CUTTING, UnDefType.UNDEF);
236 if (isLinked(NEXT_CUTTING)) {
237 Instant nextCutting = controller.getPredictiveNextCutting();
238 if (nextCutting != null) {
239 updateState(NEXT_CUTTING,
240 new DateTimeType(ZonedDateTime.ofInstant(nextCutting, timeZoneProvider.getTimeZone())));
242 updateState(NEXT_CUTTING, UnDefType.UNDEF);
247 private void updateState(DeviceStateResponse state) {
248 DeviceStatus deviceStatus = DeviceStatus.fromCode(state.state);
249 int status = getStatusFromCommand(deviceStatus.getAssociatedCommand());
250 int mowed = state.mowed;
251 int error = state.error;
252 int statecode = state.state;
253 boolean ready = isReadyToMow(deviceStatus, state.error);
255 updateState(STATECODE, new DecimalType(statecode));
256 updateState(READY, new DecimalType(ready ? 1 : 0));
257 updateState(ERRORCODE, new DecimalType(error));
258 updateState(MOWED, new PercentType(mowed));
259 updateState(STATE, new DecimalType(status));
260 updateState(TEXTUAL_STATE, new StringType(deviceStatus.getMessage(translationProvider)));
263 private boolean isReadyToMow(DeviceStatus deviceStatus, int error) {
264 return deviceStatus.isReadyToMow() && error == 0;
267 private boolean verifyCommand(DeviceCommand command, DeviceStatus deviceStatus, int errorCode) {
268 // Mower reported an error
269 if (errorCode != 0) {
270 logger.error("The mower reported an error.");
274 // Command is equal to current state
275 if (command == deviceStatus.getAssociatedCommand()) {
276 logger.debug("Command is equal to state");
279 // Can't pause while the mower is docked
280 if (command == DeviceCommand.PAUSE && deviceStatus.getAssociatedCommand() == DeviceCommand.RETURN) {
281 logger.debug("Can't pause the mower while it's docked or docking");
284 // Command means "MOW" but mower is not ready
285 if (command == DeviceCommand.MOW && !isReadyToMow(deviceStatus, errorCode)) {
286 logger.debug("The mower is not ready to mow at the moment");
292 private int getStatusFromCommand(@Nullable DeviceCommand command) {
293 if (command == null) {