[boschindego] Add channels for last/next cutting time (#12989)

* Add channels for last/next cutting time
* Let handleCommand() work synchronously

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
This commit is contained in:
Jacob Laursen 2022-07-02 10:48:33 +02:00 committed by GitHub
parent ad9b4fbf79
commit b886650bae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 212 additions and 44 deletions

View File

@ -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

View File

@ -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<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_INDEGO);
}

View File

@ -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;

View File

@ -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);
}
/**

View File

@ -25,4 +25,5 @@ public class BoschIndegoConfiguration {
public @Nullable String username;
public @Nullable String password;
public long refresh = 180;
public long cuttingTimeRefresh = 60;
}

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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) {
try {
if (command == RefreshType.REFRESH) {
scheduler.submit(() -> this.refreshState());
handleRefreshCommand(channelUID.getId());
return;
}
try {
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,8 +183,18 @@ public class BoschIndegoHandler extends BaseThingHandler {
updateState(state);
}
private void refreshState() {
private void refreshStateWithExceptionHandling() {
try {
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;
@ -160,6 +203,17 @@ public class BoschIndegoHandler extends BaseThingHandler {
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");
@ -168,6 +222,28 @@ public class BoschIndegoHandler extends BaseThingHandler {
}
}
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;

View File

@ -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

View File

@ -14,6 +14,8 @@
<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">
@ -27,9 +29,15 @@
</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>
@ -96,6 +104,7 @@
<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">
@ -109,5 +118,19 @@
</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>