diff --git a/bundles/org.openhab.binding.boschindego/README.md b/bundles/org.openhab.binding.boschindego/README.md index e4df403b8..5dee2a598 100644 --- a/bundles/org.openhab.binding.boschindego/README.md +++ b/bundles/org.openhab.binding.boschindego/README.md @@ -8,11 +8,12 @@ His [Java Library](https://github.com/zazaz-de/iot-device-bosch-indego-controlle Currently the binding supports ***indego*** mowers as a thing type with these configuration parameters: -| Parameter | Description | -|-----------|----------------------------------------------------------------------| -| username | Username for the Bosch Indego account | -| password | Password for the Bosch Indego account | -| refresh | Specifies the refresh interval in seconds (default 180, minimum: 60) | +| Parameter | Description | Default | +|--------------------|-----------------------------------------------------------------|---------| +| username | Username for the Bosch Indego account | | +| password | Password for the Bosch Indego account | | +| refresh | The number of seconds between refreshing device state | 180 | +| cuttingTimeRefresh | The number of minutes between refreshing last/next cutting time | 60 | ## Channels @@ -24,6 +25,8 @@ Currently the binding supports ***indego*** mowers as a thing type with these | textualstate | String | State as a text. (readonly) | | ready | Number | Shows if the mower is ready to mow (1=ready, 0=not ready, readonly) | | mowed | Dimmer | Cut grass in percent (readonly) | +| lastCutting | DateTime | Last cutting time (readonly) | +| nextCutting | DateTime | Next scheduled cutting time (readonly) | ### State Codes @@ -76,6 +79,8 @@ Number Indego_StateCode { channel="boschindego:indego:lawnmower:statecode" } String Indego_TextualState { channel="boschindego:indego:lawnmower:textualstate" } Number Indego_Ready { channel="boschindego:indego:lawnmower:ready" } Dimmer Indego_Mowed { channel="boschindego:indego:lawnmower:mowed" } +DateTime Indego_LastCutting { channel="boschindego:indego:lawnmower:lastCutting" } +DateTime Indego_NextCutting { channel="boschindego:indego:lawnmower:nextCutting" } ``` ### `indego.sitemap` File diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/BoschIndegoBindingConstants.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/BoschIndegoBindingConstants.java index 546915725..e1c6ff3d5 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/BoschIndegoBindingConstants.java +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/BoschIndegoBindingConstants.java @@ -38,6 +38,8 @@ public class BoschIndegoBindingConstants { public static final String ERRORCODE = "errorcode"; public static final String STATECODE = "statecode"; public static final String READY = "ready"; + public static final String LAST_CUTTING = "lastCutting"; + public static final String NEXT_CUTTING = "nextCutting"; public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_INDEGO); } diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/BoschIndegoHandlerFactory.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/BoschIndegoHandlerFactory.java index 4cb385d89..0c70446a0 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/BoschIndegoHandlerFactory.java +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/BoschIndegoHandlerFactory.java @@ -19,6 +19,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.boschindego.internal.handler.BoschIndegoHandler; import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.thing.Thing; @@ -43,14 +44,16 @@ public class BoschIndegoHandlerFactory extends BaseThingHandlerFactory { private final HttpClient httpClient; private final BoschIndegoTranslationProvider translationProvider; + private final TimeZoneProvider timeZoneProvider; @Activate public BoschIndegoHandlerFactory(@Reference HttpClientFactory httpClientFactory, final @Reference TranslationProvider i18nProvider, final @Reference LocaleProvider localeProvider, - ComponentContext componentContext) { + final @Reference TimeZoneProvider timeZoneProvider, ComponentContext componentContext) { super.activate(componentContext); this.httpClient = httpClientFactory.getCommonHttpClient(); this.translationProvider = new BoschIndegoTranslationProvider(i18nProvider, localeProvider); + this.timeZoneProvider = timeZoneProvider; } @Override @@ -63,7 +66,7 @@ public class BoschIndegoHandlerFactory extends BaseThingHandlerFactory { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (THING_TYPE_INDEGO.equals(thingTypeUID)) { - return new BoschIndegoHandler(thing, httpClient, translationProvider); + return new BoschIndegoHandler(thing, httpClient, translationProvider, timeZoneProvider); } return null; diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoController.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoController.java index b10a09a0e..dd50c8e7c 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoController.java +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoController.java @@ -38,7 +38,8 @@ import org.openhab.binding.boschindego.internal.dto.response.AuthenticationRespo import org.openhab.binding.boschindego.internal.dto.response.DeviceCalendarResponse; import org.openhab.binding.boschindego.internal.dto.response.DeviceStateResponse; import org.openhab.binding.boschindego.internal.dto.response.LocationWeatherResponse; -import org.openhab.binding.boschindego.internal.dto.response.PredictiveCuttingTimeResponse; +import org.openhab.binding.boschindego.internal.dto.response.PredictiveLastCuttingResponse; +import org.openhab.binding.boschindego.internal.dto.response.PredictiveNextCuttingResponse; import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException; import org.openhab.binding.boschindego.internal.exceptions.IndegoException; import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidCommandException; @@ -455,9 +456,8 @@ public class IndegoController { * @throws IndegoException if any communication or parsing error occurred */ public boolean getPredictiveMoving() throws IndegoAuthenticationException, IndegoException { - final PredictiveStatus status = getRequestWithAuthentication( - SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive", PredictiveStatus.class); - return status.enabled; + return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive", + PredictiveStatus.class).enabled; } /** @@ -473,6 +473,18 @@ public class IndegoController { putRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive", status); } + /** + * Queries predictive last cutting as {@link Instant}. + * + * @return predictive last cutting + * @throws IndegoAuthenticationException if request was rejected as unauthorized + * @throws IndegoException if any communication or parsing error occurred + */ + public @Nullable Instant getPredictiveLastCutting() throws IndegoAuthenticationException, IndegoException { + return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/lastcutting", + PredictiveLastCuttingResponse.class).getLastCutting(); + } + /** * Queries predictive next cutting as {@link Instant}. * @@ -480,11 +492,9 @@ public class IndegoController { * @throws IndegoAuthenticationException if request was rejected as unauthorized * @throws IndegoException if any communication or parsing error occurred */ - public Instant getPredictiveNextCutting() throws IndegoAuthenticationException, IndegoException { - final PredictiveCuttingTimeResponse nextCutting = getRequestWithAuthentication( - SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/nextcutting", - PredictiveCuttingTimeResponse.class); - return nextCutting.getNextCutting(); + public @Nullable Instant getPredictiveNextCutting() throws IndegoAuthenticationException, IndegoException { + return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/nextcutting", + PredictiveNextCuttingResponse.class).getNextCutting(); } /** @@ -495,9 +505,8 @@ public class IndegoController { * @throws IndegoException if any communication or parsing error occurred */ public DeviceCalendarResponse getPredictiveExclusionTime() throws IndegoAuthenticationException, IndegoException { - final DeviceCalendarResponse calendar = getRequestWithAuthentication( - SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/calendar", DeviceCalendarResponse.class); - return calendar; + return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/predictive/calendar", + DeviceCalendarResponse.class); } /** diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/config/BoschIndegoConfiguration.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/config/BoschIndegoConfiguration.java index 9512d5d4b..03148db98 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/config/BoschIndegoConfiguration.java +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/config/BoschIndegoConfiguration.java @@ -25,4 +25,5 @@ public class BoschIndegoConfiguration { public @Nullable String username; public @Nullable String password; public long refresh = 180; + public long cuttingTimeRefresh = 60; } diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/PredictiveLastCuttingResponse.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/PredictiveLastCuttingResponse.java new file mode 100644 index 000000000..ddcebef0a --- /dev/null +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/PredictiveLastCuttingResponse.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschindego.internal.dto.response; + +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.format.DateTimeParseException; + +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.annotations.SerializedName; + +/** + * Response for last cutting time. + * + * @author Jacob Laursen - Initial contribution + */ +public class PredictiveLastCuttingResponse { + @SerializedName("last_mowed") + public String lastCutting; + + public @Nullable Instant getLastCutting() { + try { + return ZonedDateTime.parse(lastCutting).toInstant(); + } catch (final DateTimeParseException e) { + // Ignored + } + return null; + } +} diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/PredictiveCuttingTimeResponse.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/PredictiveNextCuttingResponse.java similarity index 87% rename from bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/PredictiveCuttingTimeResponse.java rename to bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/PredictiveNextCuttingResponse.java index 27d78e458..4ffa7c33e 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/PredictiveCuttingTimeResponse.java +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/PredictiveNextCuttingResponse.java @@ -16,6 +16,8 @@ import java.time.Instant; import java.time.ZonedDateTime; import java.time.format.DateTimeParseException; +import org.eclipse.jdt.annotation.Nullable; + import com.google.gson.annotations.SerializedName; /** @@ -23,11 +25,11 @@ import com.google.gson.annotations.SerializedName; * * @author Jacob Laursen - Initial contribution */ -public class PredictiveCuttingTimeResponse { +public class PredictiveNextCuttingResponse { @SerializedName("mow_next") public String nextCutting; - public Instant getNextCutting() { + public @Nullable Instant getNextCutting() { try { return ZonedDateTime.parse(nextCutting).toInstant(); } catch (final DateTimeParseException e) { diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschIndegoHandler.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschIndegoHandler.java index c3d9afa32..da247be33 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschIndegoHandler.java +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschIndegoHandler.java @@ -14,6 +14,8 @@ package org.openhab.binding.boschindego.internal.handler; import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.*; +import java.time.Instant; +import java.time.ZonedDateTime; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -28,6 +30,8 @@ import org.openhab.binding.boschindego.internal.dto.DeviceCommand; import org.openhab.binding.boschindego.internal.dto.response.DeviceStateResponse; import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException; import org.openhab.binding.boschindego.internal.exceptions.IndegoException; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.StringType; @@ -55,16 +59,20 @@ public class BoschIndegoHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(BoschIndegoHandler.class); private final HttpClient httpClient; private final BoschIndegoTranslationProvider translationProvider; + private final TimeZoneProvider timeZoneProvider; private @NonNullByDefault({}) IndegoController controller; - private @Nullable ScheduledFuture pollFuture; - private long refreshRate; + private @Nullable ScheduledFuture statePollFuture; + private @Nullable ScheduledFuture cuttingTimePollFuture; private boolean propertiesInitialized; + private int previousStateCode; - public BoschIndegoHandler(Thing thing, HttpClient httpClient, BoschIndegoTranslationProvider translationProvider) { + public BoschIndegoHandler(Thing thing, HttpClient httpClient, BoschIndegoTranslationProvider translationProvider, + TimeZoneProvider timeZoneProvider) { super(thing); this.httpClient = httpClient; this.translationProvider = translationProvider; + this.timeZoneProvider = timeZoneProvider; } @Override @@ -86,29 +94,37 @@ public class BoschIndegoHandler extends BaseThingHandler { } controller = new IndegoController(httpClient, username, password); - refreshRate = config.refresh; updateStatus(ThingStatus.UNKNOWN); - this.pollFuture = scheduler.scheduleWithFixedDelay(this::refreshState, 0, refreshRate, TimeUnit.SECONDS); + this.statePollFuture = scheduler.scheduleWithFixedDelay(this::refreshStateWithExceptionHandling, 0, + config.refresh, TimeUnit.SECONDS); + this.cuttingTimePollFuture = scheduler.scheduleWithFixedDelay(this::refreshCuttingTimesWithExceptionHandling, 0, + config.cuttingTimeRefresh, TimeUnit.MINUTES); } @Override public void dispose() { logger.debug("Disposing Indego handler"); - ScheduledFuture pollFuture = this.pollFuture; + ScheduledFuture pollFuture = this.statePollFuture; if (pollFuture != null) { pollFuture.cancel(true); } - this.pollFuture = null; + this.statePollFuture = null; + pollFuture = this.cuttingTimePollFuture; + if (pollFuture != null) { + pollFuture.cancel(true); + } + this.cuttingTimePollFuture = null; } @Override public void handleCommand(ChannelUID channelUID, Command command) { - if (command == RefreshType.REFRESH) { - scheduler.submit(() -> this.refreshState()); - return; - } try { + if (command == RefreshType.REFRESH) { + handleRefreshCommand(channelUID.getId()); + return; + } + if (command instanceof DecimalType && channelUID.getId().equals(STATE)) { sendCommand(((DecimalType) command).intValue()); } @@ -120,6 +136,23 @@ public class BoschIndegoHandler extends BaseThingHandler { } } + private void handleRefreshCommand(String channelId) throws IndegoAuthenticationException, IndegoException { + switch (channelId) { + case STATE: + case TEXTUAL_STATE: + case MOWED: + case ERRORCODE: + case STATECODE: + case READY: + this.refreshState(); + break; + case LAST_CUTTING: + case NEXT_CUTTING: + this.refreshCuttingTimes(); + break; + } + } + private void sendCommand(int commandInt) throws IndegoException { DeviceCommand command; switch (commandInt) { @@ -150,16 +183,9 @@ public class BoschIndegoHandler extends BaseThingHandler { updateState(state); } - private void refreshState() { + private void refreshStateWithExceptionHandling() { try { - if (!propertiesInitialized) { - getThing().setProperty(Thing.PROPERTY_SERIAL_NUMBER, controller.getSerialNumber()); - propertiesInitialized = true; - } - - DeviceStateResponse state = controller.getState(); - updateStatus(ThingStatus.ONLINE); - updateState(state); + refreshState(); } catch (IndegoAuthenticationException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/offline.comm-error.authentication-failure"); @@ -168,6 +194,56 @@ public class BoschIndegoHandler extends BaseThingHandler { } } + private void refreshState() throws IndegoAuthenticationException, IndegoException { + if (!propertiesInitialized) { + getThing().setProperty(Thing.PROPERTY_SERIAL_NUMBER, controller.getSerialNumber()); + propertiesInitialized = true; + } + + DeviceStateResponse state = controller.getState(); + updateStatus(ThingStatus.ONLINE); + updateState(state); + + // When state code changed, refresh cutting times immediately. + if (state.state != previousStateCode) { + refreshCuttingTimes(); + previousStateCode = state.state; + } + } + + private void refreshCuttingTimesWithExceptionHandling() { + try { + refreshCuttingTimes(); + } catch (IndegoAuthenticationException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/offline.comm-error.authentication-failure"); + } catch (IndegoException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + } + + private void refreshCuttingTimes() throws IndegoAuthenticationException, IndegoException { + if (isLinked(LAST_CUTTING)) { + Instant lastCutting = controller.getPredictiveLastCutting(); + if (lastCutting != null) { + updateState(LAST_CUTTING, + new DateTimeType(ZonedDateTime.ofInstant(lastCutting, timeZoneProvider.getTimeZone()))); + } else { + updateState(LAST_CUTTING, UnDefType.UNDEF); + } + } + + if (isLinked(NEXT_CUTTING)) { + Instant nextCutting = controller.getPredictiveNextCutting(); + if (nextCutting != null) { + updateState(NEXT_CUTTING, + new DateTimeType(ZonedDateTime.ofInstant(nextCutting, timeZoneProvider.getTimeZone()))); + } else { + updateState(NEXT_CUTTING, UnDefType.UNDEF); + } + } + } + private void updateState(DeviceStateResponse state) { DeviceStatus deviceStatus = DeviceStatus.fromCode(state.state); int status = getStatusFromCommand(deviceStatus.getAssociatedCommand()); @@ -200,7 +276,7 @@ public class BoschIndegoHandler extends BaseThingHandler { logger.debug("Command is equal to state"); return false; } - // Cant pause while the mower is docked + // Can't pause while the mower is docked if (command == DeviceCommand.PAUSE && deviceStatus.getAssociatedCommand() == DeviceCommand.RETURN) { logger.debug("Can't pause the mower while it's docked or docking"); return false; diff --git a/bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/i18n/boschindego.properties b/bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/i18n/boschindego.properties index ee095e6a6..158fba460 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/i18n/boschindego.properties +++ b/bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/i18n/boschindego.properties @@ -10,10 +10,12 @@ thing-type.boschindego.indego.description = Indego which supports the connect fe # thing types config +thing-type.config.boschindego.indego.cuttingTimeRefresh.label = Cutting Time Refresh Interval +thing-type.config.boschindego.indego.cuttingTimeRefresh.description = The number of minutes between refreshing last/next cutting time. thing-type.config.boschindego.indego.password.label = Password thing-type.config.boschindego.indego.password.description = Password for the Bosch Indego account. thing-type.config.boschindego.indego.refresh.label = Refresh Interval -thing-type.config.boschindego.indego.refresh.description = Specifies the refresh interval in seconds. +thing-type.config.boschindego.indego.refresh.description = The number of seconds between refreshing device state. thing-type.config.boschindego.indego.username.label = Username thing-type.config.boschindego.indego.username.description = Username for the Bosch Indego account. @@ -21,7 +23,12 @@ thing-type.config.boschindego.indego.username.description = Username for the Bos channel-type.boschindego.errorcode.label = Error Code channel-type.boschindego.errorcode.description = 0 = no error +channel-type.boschindego.lastCutting.label = Last Cutting +channel-type.boschindego.lastCutting.description = Last cutting time channel-type.boschindego.mowed.label = Cut Grass +channel-type.boschindego.mowed.description = Cut grass in percent +channel-type.boschindego.nextCutting.label = Next Cutting +channel-type.boschindego.nextCutting.description = Next scheduled cutting time channel-type.boschindego.ready.label = Ready channel-type.boschindego.ready.description = Indicates if mower is ready to mow channel-type.boschindego.ready.state.option.0 = not ready diff --git a/bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/thing/thing-types.xml index 192792b4c..03d63cca4 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/thing/thing-types.xml @@ -14,6 +14,8 @@ + + @@ -27,9 +29,15 @@ - Specifies the refresh interval in seconds. + The number of seconds between refreshing device state. 180 + + + The number of minutes between refreshing last/next cutting time. + true + 60 + @@ -96,6 +104,7 @@ Dimmer + Cut grass in percent @@ -109,5 +118,19 @@ + + DateTime + + Last cutting time + Time + + + + DateTime + + Next scheduled cutting time + Time + +