[boschindego] Plot location on map (#13179)

* Plot location on map
* Invalidate map when requested by service
* Optimize update of raw map

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
This commit is contained in:
Jacob Laursen 2022-07-28 08:39:27 +02:00 committed by GitHub
parent 3b8567bd9e
commit 6028533e8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 68 additions and 36 deletions

View File

@ -8,12 +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: Currently the binding supports ***indego*** mowers as a thing type with these configuration parameters:
| Parameter | Description | Default | | Parameter | Description | Default |
|-----------------------|-------------------------------------------------------------------------|---------| |--------------------|-----------------------------------------------------------------|---------|
| username | Username for the Bosch Indego account | | | username | Username for the Bosch Indego account | |
| password | Password for the Bosch Indego account | | | password | Password for the Bosch Indego account | |
| refresh | The number of seconds between refreshing device state | 180 | | refresh | The number of seconds between refreshing device state | 180 |
| cuttingTimeMapRefresh | The number of minutes between refreshing last/next cutting time and map | 60 | | cuttingTimeRefresh | The number of minutes between refreshing last/next cutting time | 60 |
## Channels ## Channels

View File

@ -29,6 +29,8 @@ import org.openhab.binding.boschindego.internal.dto.DeviceCommand;
@NonNullByDefault @NonNullByDefault
public class DeviceStatus { public class DeviceStatus {
public static final int STATE_LEARNING_LAWN = 516;
private final static String STATE_PREFIX = "indego.state."; private final static String STATE_PREFIX = "indego.state.";
private final static String STATE_UNKNOWN = "unknown"; private final static String STATE_UNKNOWN = "unknown";
@ -45,7 +47,7 @@ public class DeviceStatus {
entry(513, new DeviceStatus("mowing", false, DeviceCommand.MOW)), entry(513, new DeviceStatus("mowing", false, DeviceCommand.MOW)),
entry(514, new DeviceStatus("relocalising", false, DeviceCommand.MOW)), entry(514, new DeviceStatus("relocalising", false, DeviceCommand.MOW)),
entry(515, new DeviceStatus("loading-map", false, DeviceCommand.MOW)), entry(515, new DeviceStatus("loading-map", false, DeviceCommand.MOW)),
entry(516, new DeviceStatus("learning-lawn", false, DeviceCommand.MOW)), entry(STATE_LEARNING_LAWN, new DeviceStatus("learning-lawn", false, DeviceCommand.MOW)),
entry(517, new DeviceStatus("paused", true, DeviceCommand.PAUSE)), entry(517, new DeviceStatus("paused", true, DeviceCommand.PAUSE)),
entry(518, new DeviceStatus("border-cut", false, DeviceCommand.MOW)), entry(518, new DeviceStatus("border-cut", false, DeviceCommand.MOW)),
entry(519, new DeviceStatus("idle-in-lawn", true, DeviceCommand.MOW)), entry(519, new DeviceStatus("idle-in-lawn", true, DeviceCommand.MOW)),

View File

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

View File

@ -14,6 +14,8 @@ package org.openhab.binding.boschindego.internal.handler;
import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.*; import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
@ -40,6 +42,7 @@ import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.RawType;
import org.openhab.core.library.types.StringType; import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SIUnits; import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units; import org.openhab.core.library.unit.Units;
@ -64,6 +67,11 @@ import org.slf4j.LoggerFactory;
@NonNullByDefault @NonNullByDefault
public class BoschIndegoHandler extends BaseThingHandler { public class BoschIndegoHandler extends BaseThingHandler {
private static final String MAP_POSITION_STROKE_COLOR = "#8c8b6d";
private static final String MAP_POSITION_FILL_COLOR = "#fff701";
private static final int MAP_POSITION_RADIUS = 10;
private static final int MAP_REFRESH_INTERVAL_DAYS = 1;
private final Logger logger = LoggerFactory.getLogger(BoschIndegoHandler.class); private final Logger logger = LoggerFactory.getLogger(BoschIndegoHandler.class);
private final HttpClient httpClient; private final HttpClient httpClient;
private final BoschIndegoTranslationProvider translationProvider; private final BoschIndegoTranslationProvider translationProvider;
@ -71,10 +79,12 @@ public class BoschIndegoHandler extends BaseThingHandler {
private @NonNullByDefault({}) IndegoController controller; private @NonNullByDefault({}) IndegoController controller;
private @Nullable ScheduledFuture<?> statePollFuture; private @Nullable ScheduledFuture<?> statePollFuture;
private @Nullable ScheduledFuture<?> cuttingTimeMapPollFuture; private @Nullable ScheduledFuture<?> cuttingTimePollFuture;
private @Nullable ScheduledFuture<?> cuttingTimeFuture; private @Nullable ScheduledFuture<?> cuttingTimeFuture;
private boolean propertiesInitialized; private boolean propertiesInitialized;
private Optional<Integer> previousStateCode = Optional.empty(); private Optional<Integer> previousStateCode = Optional.empty();
private @Nullable RawType cachedMap;
private Instant cachedMapTimestamp = Instant.MIN;
public BoschIndegoHandler(Thing thing, HttpClient httpClient, BoschIndegoTranslationProvider translationProvider, public BoschIndegoHandler(Thing thing, HttpClient httpClient, BoschIndegoTranslationProvider translationProvider,
TimeZoneProvider timeZoneProvider) { TimeZoneProvider timeZoneProvider) {
@ -108,9 +118,8 @@ public class BoschIndegoHandler extends BaseThingHandler {
previousStateCode = Optional.empty(); previousStateCode = Optional.empty();
this.statePollFuture = scheduler.scheduleWithFixedDelay(this::refreshStateAndOperatingDataWithExceptionHandling, this.statePollFuture = scheduler.scheduleWithFixedDelay(this::refreshStateAndOperatingDataWithExceptionHandling,
0, config.refresh, TimeUnit.SECONDS); 0, config.refresh, TimeUnit.SECONDS);
this.cuttingTimeMapPollFuture = scheduler.scheduleWithFixedDelay( this.cuttingTimePollFuture = scheduler.scheduleWithFixedDelay(this::refreshCuttingTimesWithExceptionHandling, 0,
this::refreshCuttingTimesAndMapWithExceptionHandling, 0, config.cuttingTimeMapRefresh, config.cuttingTimeRefresh, TimeUnit.MINUTES);
TimeUnit.MINUTES);
} }
@Override @Override
@ -121,11 +130,11 @@ public class BoschIndegoHandler extends BaseThingHandler {
pollFuture.cancel(true); pollFuture.cancel(true);
} }
this.statePollFuture = null; this.statePollFuture = null;
pollFuture = this.cuttingTimeMapPollFuture; pollFuture = this.cuttingTimePollFuture;
if (pollFuture != null) { if (pollFuture != null) {
pollFuture.cancel(true); pollFuture.cancel(true);
} }
this.cuttingTimeMapPollFuture = null; this.cuttingTimePollFuture = null;
pollFuture = this.cuttingTimeFuture; pollFuture = this.cuttingTimeFuture;
if (pollFuture != null) { if (pollFuture != null) {
pollFuture.cancel(true); pollFuture.cancel(true);
@ -166,6 +175,9 @@ public class BoschIndegoHandler extends BaseThingHandler {
private void handleRefreshCommand(String channelId) private void handleRefreshCommand(String channelId)
throws IndegoAuthenticationException, IndegoUnreachableException, IndegoException { throws IndegoAuthenticationException, IndegoUnreachableException, IndegoException {
switch (channelId) { switch (channelId) {
case GARDEN_MAP:
// Force map refresh and fall through to state update.
cachedMapTimestamp = Instant.MIN;
case STATE: case STATE:
case TEXTUAL_STATE: case TEXTUAL_STATE:
case MOWED: case MOWED:
@ -185,9 +197,6 @@ public class BoschIndegoHandler extends BaseThingHandler {
case GARDEN_SIZE: case GARDEN_SIZE:
refreshOperatingData(); refreshOperatingData();
break; break;
case GARDEN_MAP:
refreshMap();
break;
} }
} }
@ -243,9 +252,19 @@ public class BoschIndegoHandler extends BaseThingHandler {
DeviceStateResponse state = controller.getState(); DeviceStateResponse state = controller.getState();
updateState(state); updateState(state);
if (state.mapUpdateAvailable) {
cachedMapTimestamp = Instant.MIN;
}
refreshMap(state.svgXPos, state.svgYPos);
// When state code changed, refresh cutting times immediately. // When state code changed, refresh cutting times immediately.
if (previousStateCode.isPresent() && state.state != previousStateCode.get()) { if (previousStateCode.isPresent() && state.state != previousStateCode.get()) {
refreshCuttingTimes(); refreshCuttingTimes();
// After learning lawn, trigger a forced map refresh on next poll.
if (previousStateCode.get() == DeviceStatus.STATE_LEARNING_LAWN) {
cachedMapTimestamp = Instant.MIN;
}
} }
previousStateCode = Optional.of(state.state); previousStateCode = Optional.of(state.state);
} }
@ -311,22 +330,33 @@ public class BoschIndegoHandler extends BaseThingHandler {
} }
} }
private void refreshCuttingTimesAndMapWithExceptionHandling() { private void refreshMap(int xPos, int yPos) throws IndegoAuthenticationException, IndegoException {
try { if (!isLinked(GARDEN_MAP)) {
refreshCuttingTimes(); return;
refreshMap();
} 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());
} }
} RawType cachedMap = this.cachedMap;
boolean mapRefreshed;
private void refreshMap() throws IndegoAuthenticationException, IndegoException { if (cachedMap == null
if (isLinked(GARDEN_MAP)) { || cachedMapTimestamp.isBefore(Instant.now().minus(Duration.ofDays(MAP_REFRESH_INTERVAL_DAYS)))) {
updateState(GARDEN_MAP, controller.getMap()); this.cachedMap = cachedMap = controller.getMap();
cachedMapTimestamp = Instant.now();
mapRefreshed = true;
} else {
mapRefreshed = false;
} }
String svgMap = new String(cachedMap.getBytes(), StandardCharsets.UTF_8);
if (!svgMap.endsWith("</svg>")) {
if (mapRefreshed) {
logger.warn("Unexpected map format, unable to plot location");
logger.trace("Received map: {}", svgMap);
updateState(GARDEN_MAP, cachedMap);
}
return;
}
svgMap = svgMap.substring(0, svgMap.length() - 6) + "<circle cx=\"" + xPos + "\" cy=\"" + yPos + "\" r=\""
+ MAP_POSITION_RADIUS + "\" stroke=\"" + MAP_POSITION_STROKE_COLOR + "\" fill=\""
+ MAP_POSITION_FILL_COLOR + "\" />\n</svg>";
updateState(GARDEN_MAP, new RawType(svgMap.getBytes(), cachedMap.getMimeType()));
} }
private void updateState(DeviceStateResponse state) { private void updateState(DeviceStateResponse state) {

View File

@ -10,8 +10,8 @@ thing-type.boschindego.indego.description = Indego which supports the connect fe
# thing types config # thing types config
thing-type.config.boschindego.indego.cuttingTimeMapRefresh.label = Cutting Time/Map Refresh Interval thing-type.config.boschindego.indego.cuttingTimeRefresh.label = Cutting Time Refresh Interval
thing-type.config.boschindego.indego.cuttingTimeMapRefresh.description = The number of minutes between refreshing last/next cutting time and map. 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.label = Password
thing-type.config.boschindego.indego.password.description = Password for the Bosch Indego account. 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.label = Refresh Interval

View File

@ -38,9 +38,9 @@
<description>The number of seconds between refreshing device state.</description> <description>The number of seconds between refreshing device state.</description>
<default>180</default> <default>180</default>
</parameter> </parameter>
<parameter name="cuttingTimeMapRefresh" type="integer" min="1"> <parameter name="cuttingTimeRefresh" type="integer" min="1">
<label>Cutting Time/Map Refresh Interval</label> <label>Cutting Time Refresh Interval</label>
<description>The number of minutes between refreshing last/next cutting time and map.</description> <description>The number of minutes between refreshing last/next cutting time.</description>
<advanced>true</advanced> <advanced>true</advanced>
<default>60</default> <default>60</default>
</parameter> </parameter>