2 * Copyright (c) 2010-2023 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.nio.charset.StandardCharsets;
18 import java.time.Duration;
19 import java.time.Instant;
20 import java.time.LocalDateTime;
21 import java.time.ZonedDateTime;
22 import java.time.temporal.ChronoUnit;
24 import java.util.Optional;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.eclipse.jetty.client.HttpClient;
31 import org.openhab.binding.boschindego.internal.AuthorizationListener;
32 import org.openhab.binding.boschindego.internal.AuthorizationProvider;
33 import org.openhab.binding.boschindego.internal.BoschIndegoTranslationProvider;
34 import org.openhab.binding.boschindego.internal.DeviceStatus;
35 import org.openhab.binding.boschindego.internal.IndegoDeviceController;
36 import org.openhab.binding.boschindego.internal.IndegoTypeDatabase;
37 import org.openhab.binding.boschindego.internal.config.BoschIndegoConfiguration;
38 import org.openhab.binding.boschindego.internal.dto.DeviceCommand;
39 import org.openhab.binding.boschindego.internal.dto.response.DevicePropertiesResponse;
40 import org.openhab.binding.boschindego.internal.dto.response.DeviceStateResponse;
41 import org.openhab.binding.boschindego.internal.dto.response.OperatingDataResponse;
42 import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException;
43 import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
44 import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidCommandException;
45 import org.openhab.binding.boschindego.internal.exceptions.IndegoTimeoutException;
46 import org.openhab.core.i18n.TimeZoneProvider;
47 import org.openhab.core.library.types.DateTimeType;
48 import org.openhab.core.library.types.DecimalType;
49 import org.openhab.core.library.types.OnOffType;
50 import org.openhab.core.library.types.PercentType;
51 import org.openhab.core.library.types.QuantityType;
52 import org.openhab.core.library.types.RawType;
53 import org.openhab.core.library.types.StringType;
54 import org.openhab.core.library.unit.SIUnits;
55 import org.openhab.core.library.unit.Units;
56 import org.openhab.core.thing.Bridge;
57 import org.openhab.core.thing.ChannelUID;
58 import org.openhab.core.thing.Thing;
59 import org.openhab.core.thing.ThingStatus;
60 import org.openhab.core.thing.ThingStatusDetail;
61 import org.openhab.core.thing.ThingStatusInfo;
62 import org.openhab.core.thing.binding.BaseThingHandler;
63 import org.openhab.core.types.Command;
64 import org.openhab.core.types.RefreshType;
65 import org.openhab.core.types.UnDefType;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
70 * The {@link BoschIndegoHandler} is responsible for handling commands, which are
71 * sent to one of the channels.
73 * @author Jonas Fleck - Initial contribution
74 * @author Jacob Laursen - Refactoring, bugfixing and removal of dependency towards abandoned library
77 public class BoschIndegoHandler extends BaseThingHandler implements AuthorizationListener {
79 private static final String MAP_POSITION_STROKE_COLOR = "#8c8b6d";
80 private static final String MAP_POSITION_FILL_COLOR = "#fff701";
81 private static final int MAP_POSITION_RADIUS = 10;
82 private static final Duration DEVICE_PROPERTIES_VALIDITY_PERIOD = Duration.ofDays(1);
84 private static final Duration MAP_REFRESH_INTERVAL = Duration.ofDays(1);
85 private static final Duration OPERATING_DATA_INACTIVE_REFRESH_INTERVAL = Duration.ofHours(6);
86 private static final Duration OPERATING_DATA_OFFLINE_REFRESH_INTERVAL = Duration.ofMinutes(30);
87 private static final Duration OPERATING_DATA_ACTIVE_REFRESH_INTERVAL = Duration.ofMinutes(2);
88 private static final Duration MAP_REFRESH_SESSION_DURATION = Duration.ofMinutes(5);
89 private static final Duration COMMAND_STATE_REFRESH_TIMEOUT = Duration.ofSeconds(10);
91 private final Logger logger = LoggerFactory.getLogger(BoschIndegoHandler.class);
92 private final HttpClient httpClient;
93 private final BoschIndegoTranslationProvider translationProvider;
94 private final TimeZoneProvider timeZoneProvider;
95 private Instant devicePropertiesUpdated = Instant.MIN;
97 private @NonNullByDefault({}) AuthorizationProvider authorizationProvider;
98 private @NonNullByDefault({}) IndegoDeviceController controller;
99 private @Nullable ScheduledFuture<?> statePollFuture;
100 private @Nullable ScheduledFuture<?> cuttingTimePollFuture;
101 private @Nullable ScheduledFuture<?> cuttingTimeFuture;
102 private Optional<Integer> previousStateCode = Optional.empty();
103 private @Nullable RawType cachedMap;
104 private Instant cachedMapTimestamp = Instant.MIN;
105 private Instant operatingDataTimestamp = Instant.MIN;
106 private Instant mapRefreshStartedTimestamp = Instant.MIN;
107 private ThingStatus lastOperatingDataStatus = ThingStatus.UNINITIALIZED;
108 private int stateInactiveRefreshIntervalSeconds;
109 private int stateActiveRefreshIntervalSeconds;
110 private int currentRefreshIntervalSeconds;
112 public BoschIndegoHandler(Thing thing, HttpClient httpClient, BoschIndegoTranslationProvider translationProvider,
113 TimeZoneProvider timeZoneProvider) {
115 this.httpClient = httpClient;
116 this.translationProvider = translationProvider;
117 this.timeZoneProvider = timeZoneProvider;
121 public void initialize() {
122 BoschIndegoConfiguration config = getConfigAs(BoschIndegoConfiguration.class);
123 stateInactiveRefreshIntervalSeconds = (int) config.refresh;
124 stateActiveRefreshIntervalSeconds = (int) config.stateActiveRefresh;
126 Bridge bridge = getBridge();
127 if (bridge == null) {
128 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
129 "@text/offline.conf-error.missing-bridge");
133 if (bridge.getHandler() instanceof BoschAccountHandler accountHandler) {
134 authorizationProvider = accountHandler.getAuthorizationProvider();
135 accountHandler.registerAuthorizationListener(this);
137 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
138 "@text/offline.conf-error.missing-bridge");
142 devicePropertiesUpdated = Instant.MIN;
143 updateProperty(Thing.PROPERTY_SERIAL_NUMBER, config.serialNumber);
145 controller = new IndegoDeviceController(httpClient, authorizationProvider, config.serialNumber);
147 updateStatus(ThingStatus.UNKNOWN);
148 previousStateCode = Optional.empty();
149 rescheduleStatePoll(0, stateInactiveRefreshIntervalSeconds, false);
150 this.cuttingTimePollFuture = scheduler.scheduleWithFixedDelay(this::refreshCuttingTimesWithExceptionHandling, 0,
151 config.cuttingTimeRefresh, TimeUnit.MINUTES);
155 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
156 if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE
157 && getThing().getStatusInfo().getStatus() == ThingStatus.OFFLINE) {
158 updateStatus(ThingStatus.UNKNOWN);
159 } else if (bridgeStatusInfo.getStatus() == ThingStatus.OFFLINE) {
160 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
164 public void onSuccessfulAuthorization() {
168 public void onFailedAuthorization(Throwable throwable) {
172 public void onAuthorizationFlowCompleted() {
173 // Trigger immediate state refresh upon authorization success.
174 rescheduleStatePoll(0, stateInactiveRefreshIntervalSeconds, true);
177 private boolean rescheduleStatePoll(int delaySeconds, int refreshIntervalSeconds, boolean force) {
178 ScheduledFuture<?> statePollFuture = this.statePollFuture;
179 if (statePollFuture != null) {
180 if (!force && refreshIntervalSeconds == currentRefreshIntervalSeconds) {
184 statePollFuture.cancel(force);
186 logger.debug("Scheduling state refresh job with {}s interval and {}s delay", refreshIntervalSeconds,
188 this.statePollFuture = scheduler.scheduleWithFixedDelay(this::refreshStateWithExceptionHandling, delaySeconds,
189 refreshIntervalSeconds, TimeUnit.SECONDS);
190 currentRefreshIntervalSeconds = refreshIntervalSeconds;
196 public void dispose() {
197 Bridge bridge = getBridge();
198 if (bridge != null) {
199 if (bridge.getHandler() instanceof BoschAccountHandler accountHandler) {
200 accountHandler.unregisterAuthorizationListener(this);
204 ScheduledFuture<?> pollFuture = this.statePollFuture;
205 if (pollFuture != null) {
206 pollFuture.cancel(true);
208 this.statePollFuture = null;
209 pollFuture = this.cuttingTimePollFuture;
210 if (pollFuture != null) {
211 pollFuture.cancel(true);
213 this.cuttingTimePollFuture = null;
214 pollFuture = this.cuttingTimeFuture;
215 if (pollFuture != null) {
216 pollFuture.cancel(true);
218 this.cuttingTimeFuture = null;
222 public void handleCommand(ChannelUID channelUID, Command command) {
223 logger.debug("handleCommand {} for channel {}", command, channelUID);
225 if (command == RefreshType.REFRESH) {
226 handleRefreshCommand(channelUID.getId());
229 if (command instanceof DecimalType && channelUID.getId().equals(STATE)) {
230 sendCommand(((DecimalType) command).intValue());
232 } catch (IndegoAuthenticationException e) {
233 // Ignore, will be handled by bridge
234 } catch (IndegoTimeoutException e) {
235 updateStatus(lastOperatingDataStatus = ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
236 "@text/offline.comm-error.unreachable");
237 } catch (IndegoInvalidCommandException e) {
238 logger.warn("Invalid command: {}", e.getMessage());
239 if (e.hasErrorCode()) {
240 updateState(ERRORCODE, new DecimalType(e.getErrorCode()));
242 } catch (IndegoException e) {
243 logger.warn("Command failed: {}", e.getMessage());
247 private void handleRefreshCommand(String channelId)
248 throws IndegoAuthenticationException, IndegoTimeoutException, IndegoException {
251 // Force map refresh and fall through to state update.
252 cachedMapTimestamp = Instant.MIN;
262 refreshLastCuttingTime();
265 refreshNextCuttingTime();
269 case BATTERY_VOLTAGE:
270 case BATTERY_TEMPERATURE:
272 refreshOperatingData();
277 private void sendCommand(int commandInt) throws IndegoException {
278 DeviceCommand command;
279 switch (commandInt) {
281 command = DeviceCommand.MOW;
284 command = DeviceCommand.RETURN;
287 command = DeviceCommand.PAUSE;
290 logger.warn("Invalid command {}", commandInt);
294 DeviceStateResponse state = controller.getState();
295 DeviceStatus deviceStatus = DeviceStatus.fromCode(state.state);
296 if (!verifyCommand(command, deviceStatus, state.error)) {
299 logger.debug("Sending command {}", command);
300 controller.sendCommand(command);
302 // State is not updated immediately, so await new state for some seconds.
303 // For command MOW, state will shortly be updated to 262 (docked, loading map).
304 // This is considered "active", so after this state change, polling frequency will
305 // be increased for faster updates.
306 DeviceStateResponse stateResponse = controller.getState(COMMAND_STATE_REFRESH_TIMEOUT);
307 if (stateResponse.state != 0) {
308 updateState(stateResponse);
309 deviceStatus = DeviceStatus.fromCode(stateResponse.state);
310 rescheduleStatePollAccordingToState(deviceStatus);
314 private void refreshStateWithExceptionHandling() {
317 } catch (IndegoAuthenticationException e) {
318 // Ignore, will be handled by bridge
319 } catch (IndegoTimeoutException e) {
320 updateStatus(lastOperatingDataStatus = ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
321 "@text/offline.comm-error.unreachable");
322 } catch (IndegoException e) {
323 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
327 private void refreshState() throws IndegoAuthenticationException, IndegoException {
328 DeviceStateResponse state = controller.getState();
329 DeviceStatus deviceStatus = DeviceStatus.fromCode(state.state);
332 if (devicePropertiesUpdated.isBefore(Instant.now().minus(DEVICE_PROPERTIES_VALIDITY_PERIOD))) {
333 refreshDeviceProperties();
336 // Update map and start tracking positions if mower is active.
337 if (state.mapUpdateAvailable) {
338 cachedMapTimestamp = Instant.MIN;
340 refreshMap(state.svgXPos, state.svgYPos);
341 if (deviceStatus.isActive()) {
346 DeviceStatus previousDeviceStatus;
347 if (previousStateCode.isPresent()) {
348 previousState = previousStateCode.get();
349 previousDeviceStatus = DeviceStatus.fromCode(previousState);
350 if (state.state != previousState
351 && ((!previousDeviceStatus.isDocked() && deviceStatus.isDocked()) || deviceStatus.isCompleted())) {
352 // When returning to dock or on its way after completing lawn, refresh last cutting time immediately.
353 // We cannot fully rely on completed lawn state since active polling refresh interval is configurable
354 // and we might miss the state if mower returns before next poll.
355 refreshLastCuttingTime();
358 previousState = state.state;
359 previousDeviceStatus = DeviceStatus.fromCode(previousState);
361 previousStateCode = Optional.of(state.state);
363 refreshOperatingDataConditionally(
364 previousDeviceStatus.isCharging() || deviceStatus.isCharging() || deviceStatus.isActive());
366 if (lastOperatingDataStatus == ThingStatus.ONLINE && thing.getStatus() != ThingStatus.ONLINE) {
367 // Revert temporary offline status caused by disruptions other than unreachable device.
368 updateStatus(ThingStatus.ONLINE);
369 } else if (lastOperatingDataStatus == ThingStatus.OFFLINE) {
370 // Update description to reflect why thing is still offline.
371 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
372 "@text/offline.comm-error.unreachable");
375 rescheduleStatePollAccordingToState(deviceStatus);
378 private void refreshDeviceProperties() throws IndegoAuthenticationException, IndegoException {
379 DevicePropertiesResponse deviceProperties = controller.getDeviceProperties();
380 Map<String, String> properties = editProperties();
381 if (deviceProperties.firmwareVersion != null) {
382 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, deviceProperties.firmwareVersion);
384 if (deviceProperties.bareToolNumber != null) {
385 properties.put(Thing.PROPERTY_MODEL_ID,
386 IndegoTypeDatabase.nameFromTypeNumber(deviceProperties.bareToolNumber));
387 properties.put(PROPERTY_BARE_TOOL_NUMBER, deviceProperties.bareToolNumber);
389 properties.put(PROPERTY_SERVICE_COUNTER, String.valueOf(deviceProperties.serviceCounter));
390 properties.put(PROPERTY_NEEDS_SERVICE, String.valueOf(deviceProperties.needsService));
391 properties.put(PROPERTY_RENEW_DATE,
392 LocalDateTime.ofInstant(deviceProperties.renewDate, timeZoneProvider.getTimeZone()).toString());
394 updateProperties(properties);
395 devicePropertiesUpdated = Instant.now();
398 private void rescheduleStatePollAccordingToState(DeviceStatus deviceStatus) {
399 int refreshIntervalSeconds;
400 if (deviceStatus.isActive()) {
401 refreshIntervalSeconds = stateActiveRefreshIntervalSeconds;
402 } else if (deviceStatus.isCharging()) {
403 refreshIntervalSeconds = (int) OPERATING_DATA_ACTIVE_REFRESH_INTERVAL.getSeconds();
405 refreshIntervalSeconds = stateInactiveRefreshIntervalSeconds;
407 if (rescheduleStatePoll(refreshIntervalSeconds, refreshIntervalSeconds, false)) {
408 // After job has been rescheduled, request operating data one last time on next poll.
409 // This is needed to update battery values after a charging cycle has completed.
410 operatingDataTimestamp = Instant.MIN;
414 private void refreshOperatingDataConditionally(boolean isActive)
415 throws IndegoAuthenticationException, IndegoTimeoutException, IndegoException {
416 // Refresh operating data only occationally or when robot is active/charging.
417 // This will contact the robot directly through cellular network and wake it up
418 // when sleeping. Additionally, refresh more often after being offline to try to get
419 // back online as soon as possible without putting too much stress on the service.
420 if ((isActive && operatingDataTimestamp.isBefore(Instant.now().minus(OPERATING_DATA_ACTIVE_REFRESH_INTERVAL)))
421 || (lastOperatingDataStatus != ThingStatus.ONLINE && operatingDataTimestamp
422 .isBefore(Instant.now().minus(OPERATING_DATA_OFFLINE_REFRESH_INTERVAL)))
423 || operatingDataTimestamp.isBefore(Instant.now().minus(OPERATING_DATA_INACTIVE_REFRESH_INTERVAL))) {
424 refreshOperatingData();
428 private void refreshOperatingData() throws IndegoAuthenticationException, IndegoTimeoutException, IndegoException {
429 updateOperatingData(controller.getOperatingData());
430 operatingDataTimestamp = Instant.now();
431 updateStatus(lastOperatingDataStatus = ThingStatus.ONLINE);
434 private void refreshCuttingTimesWithExceptionHandling() {
436 refreshLastCuttingTime();
437 refreshNextCuttingTime();
438 } catch (IndegoAuthenticationException e) {
439 // Ignore, will be handled by bridge
440 } catch (IndegoException e) {
441 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
445 private void refreshLastCuttingTime() throws IndegoAuthenticationException, IndegoException {
446 if (isLinked(LAST_CUTTING)) {
447 Instant lastCutting = controller.getPredictiveLastCutting();
448 if (lastCutting != null) {
449 updateState(LAST_CUTTING,
450 new DateTimeType(ZonedDateTime.ofInstant(lastCutting, timeZoneProvider.getTimeZone())));
452 updateState(LAST_CUTTING, UnDefType.UNDEF);
457 private void refreshNextCuttingTimeWithExceptionHandling() {
459 refreshNextCuttingTime();
460 } catch (IndegoAuthenticationException e) {
461 // Ignore, will be handled by bridge
462 } catch (IndegoException e) {
463 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
467 private void refreshNextCuttingTime() throws IndegoAuthenticationException, IndegoException {
468 cancelCuttingTimeRefresh();
469 if (isLinked(NEXT_CUTTING)) {
470 Instant nextCutting = controller.getPredictiveNextCutting();
471 if (nextCutting != null) {
472 updateState(NEXT_CUTTING,
473 new DateTimeType(ZonedDateTime.ofInstant(nextCutting, timeZoneProvider.getTimeZone())));
474 scheduleCuttingTimesRefresh(nextCutting);
476 updateState(NEXT_CUTTING, UnDefType.UNDEF);
481 private void cancelCuttingTimeRefresh() {
482 ScheduledFuture<?> cuttingTimeFuture = this.cuttingTimeFuture;
483 if (cuttingTimeFuture != null) {
484 // Do not interrupt as we might be running within that job.
485 cuttingTimeFuture.cancel(false);
486 this.cuttingTimeFuture = null;
490 private void scheduleCuttingTimesRefresh(Instant nextCutting) {
491 // Schedule additional update right after next planned cutting. This ensures a faster update.
492 long secondsUntilNextCutting = Instant.now().until(nextCutting, ChronoUnit.SECONDS) + 2;
493 if (secondsUntilNextCutting > 0) {
494 logger.debug("Scheduling fetching of next cutting time in {} seconds", secondsUntilNextCutting);
495 this.cuttingTimeFuture = scheduler.schedule(this::refreshNextCuttingTimeWithExceptionHandling,
496 secondsUntilNextCutting, TimeUnit.SECONDS);
500 private void refreshMap(int xPos, int yPos) throws IndegoAuthenticationException, IndegoException {
501 if (!isLinked(GARDEN_MAP)) {
504 RawType cachedMap = this.cachedMap;
505 boolean mapRefreshed;
506 if (cachedMap == null || cachedMapTimestamp.isBefore(Instant.now().minus(MAP_REFRESH_INTERVAL))) {
507 this.cachedMap = cachedMap = controller.getMap();
508 cachedMapTimestamp = Instant.now();
511 mapRefreshed = false;
513 String svgMap = new String(cachedMap.getBytes(), StandardCharsets.UTF_8);
514 if (!svgMap.endsWith("</svg>")) {
516 logger.warn("Unexpected map format, unable to plot location");
517 logger.trace("Received map: {}", svgMap);
518 updateState(GARDEN_MAP, cachedMap);
522 svgMap = svgMap.substring(0, svgMap.length() - 6) + "<circle cx=\"" + xPos + "\" cy=\"" + yPos + "\" r=\""
523 + MAP_POSITION_RADIUS + "\" stroke=\"" + MAP_POSITION_STROKE_COLOR + "\" fill=\""
524 + MAP_POSITION_FILL_COLOR + "\" />\n</svg>";
525 updateState(GARDEN_MAP, new RawType(svgMap.getBytes(), cachedMap.getMimeType()));
528 private void trackPosition() throws IndegoAuthenticationException, IndegoException {
529 if (!isLinked(GARDEN_MAP)) {
532 if (mapRefreshStartedTimestamp.isBefore(Instant.now().minus(MAP_REFRESH_SESSION_DURATION))) {
533 int count = (int) MAP_REFRESH_SESSION_DURATION.getSeconds() / stateActiveRefreshIntervalSeconds + 1;
534 logger.debug("Requesting position updates (count: {}; interval: {}s), previously triggered {}", count,
535 stateActiveRefreshIntervalSeconds, mapRefreshStartedTimestamp);
536 controller.requestPosition(count, stateActiveRefreshIntervalSeconds);
537 mapRefreshStartedTimestamp = Instant.now();
541 private void updateState(DeviceStateResponse state) {
542 DeviceStatus deviceStatus = DeviceStatus.fromCode(state.state);
543 DeviceCommand associatedCommand = deviceStatus.getAssociatedCommand();
544 int status = associatedCommand != null ? getStatusFromCommand(associatedCommand) : 0;
545 int mowed = state.mowed;
546 int error = state.error;
547 int statecode = state.state;
548 boolean ready = isReadyToMow(deviceStatus, state.error);
550 updateState(STATECODE, new DecimalType(statecode));
551 updateState(READY, new DecimalType(ready ? 1 : 0));
552 updateState(ERRORCODE, new DecimalType(error));
553 updateState(MOWED, new PercentType(mowed));
554 updateState(STATE, new DecimalType(status));
555 updateState(TEXTUAL_STATE, new StringType(deviceStatus.getMessage(translationProvider)));
558 private void updateOperatingData(OperatingDataResponse operatingData) {
559 updateState(BATTERY_VOLTAGE, new QuantityType<>(operatingData.battery.voltage, Units.VOLT));
560 updateState(BATTERY_LEVEL, new DecimalType(operatingData.battery.percent));
561 updateState(LOW_BATTERY, OnOffType.from(operatingData.battery.percent < 20));
562 updateState(BATTERY_TEMPERATURE, new QuantityType<>(operatingData.battery.batteryTemperature, SIUnits.CELSIUS));
563 updateState(GARDEN_SIZE, new QuantityType<>(operatingData.garden.size, SIUnits.SQUARE_METRE));
566 private boolean isReadyToMow(DeviceStatus deviceStatus, int error) {
567 return deviceStatus.isReadyToMow() && error == 0;
570 private boolean verifyCommand(DeviceCommand command, DeviceStatus deviceStatus, int errorCode) {
571 // Mower reported an error
572 if (errorCode != 0) {
573 logger.warn("The mower reported an error.");
577 // Command is equal to current state
578 if (command == deviceStatus.getAssociatedCommand()) {
579 logger.debug("Command is equal to state");
582 // Can't pause while the mower is docked
583 if (command == DeviceCommand.PAUSE && deviceStatus.getAssociatedCommand() == DeviceCommand.RETURN) {
584 logger.info("Can't pause the mower while it's docked or docking");
587 // Command means "MOW" but mower is not ready
588 if (command == DeviceCommand.MOW && !isReadyToMow(deviceStatus, errorCode)) {
589 logger.info("The mower is not ready to mow at the moment");
595 private int getStatusFromCommand(DeviceCommand command) {