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
| 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
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
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<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_INDEGO);
}
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;
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
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;
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;
* @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;
}
/**
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}.
*
* @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();
}
/**
* @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);
}
/**
public @Nullable String username;
public @Nullable String password;
public long refresh = 180;
+ public long cuttingTimeRefresh = 60;
}
+++ /dev/null
-/**
- * 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 com.google.gson.annotations.SerializedName;
-
-/**
- * Response for next cutting time.
- *
- * @author Jacob Laursen - Initial contribution
- */
-public class PredictiveCuttingTimeResponse {
- @SerializedName("mow_next")
- public String nextCutting;
-
- public Instant getNextCutting() {
- try {
- return ZonedDateTime.parse(nextCutting).toInstant();
- } catch (final DateTimeParseException e) {
- // Ignored
- }
- return null;
- }
-}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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 next cutting time.
+ *
+ * @author Jacob Laursen - Initial contribution
+ */
+public class PredictiveNextCuttingResponse {
+ @SerializedName("mow_next")
+ public String nextCutting;
+
+ public @Nullable Instant getNextCutting() {
+ try {
+ return ZonedDateTime.parse(nextCutting).toInstant();
+ } catch (final DateTimeParseException e) {
+ // Ignored
+ }
+ return null;
+ }
+}
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;
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;
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
}
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());
}
}
}
+ 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) {
updateState(state);
}
- private void refreshState() {
+ private void refreshStateWithExceptionHandling() {
try {
- if (!propertiesInitialized) {
- getThing().setProperty(Thing.PROPERTY_SERIAL_NUMBER, controller.getSerialNumber());
- propertiesInitialized = true;
- }
+ refreshState();
+ } 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 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);
- 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");
}
}
+ 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());
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;
# 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.
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
<channel id="statecode" typeId="statecode"/>
<channel id="mowed" typeId="mowed"/>
<channel id="ready" typeId="ready"/>
+ <channel id="lastCutting" typeId="lastCutting"/>
+ <channel id="nextCutting" typeId="nextCutting"/>
</channels>
<config-description>
<parameter name="username" type="text" required="true">
</parameter>
<parameter name="refresh" type="integer" min="60">
<label>Refresh Interval</label>
- <description>Specifies the refresh interval in seconds.</description>
+ <description>The number of seconds between refreshing device state.</description>
<default>180</default>
</parameter>
+ <parameter name="cuttingTimeRefresh" type="integer" min="1">
+ <label>Cutting Time Refresh Interval</label>
+ <description>The number of minutes between refreshing last/next cutting time.</description>
+ <advanced>true</advanced>
+ <default>60</default>
+ </parameter>
</config-description>
</thing-type>
<channel-type id="mowed">
<item-type>Dimmer</item-type>
<label>Cut Grass</label>
+ <description>Cut grass in percent</description>
<state readOnly="true" pattern="%d %%"></state>
</channel-type>
<channel-type id="ready">
</options>
</state>
</channel-type>
+ <channel-type id="lastCutting">
+ <item-type>DateTime</item-type>
+ <label>Last Cutting</label>
+ <description>Last cutting time</description>
+ <category>Time</category>
+ <state readOnly="true"/>
+ </channel-type>
+ <channel-type id="nextCutting">
+ <item-type>DateTime</item-type>
+ <label>Next Cutting</label>
+ <description>Next scheduled cutting time</description>
+ <category>Time</category>
+ <state readOnly="true"/>
+ </channel-type>
</thing:thing-descriptions>