From ca6016ab612e6e213a5702e808bd6ac5e724e9b9 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Thu, 28 Jul 2022 08:39:27 +0200 Subject: [PATCH] [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 --- .../org.openhab.binding.boschindego/README.md | 12 +-- .../boschindego/internal/DeviceStatus.java | 4 +- .../config/BoschIndegoConfiguration.java | 2 +- .../internal/handler/BoschIndegoHandler.java | 76 +++++++++++++------ .../OH-INF/i18n/boschindego.properties | 4 +- .../resources/OH-INF/thing/thing-types.xml | 6 +- 6 files changed, 68 insertions(+), 36 deletions(-) diff --git a/bundles/org.openhab.binding.boschindego/README.md b/bundles/org.openhab.binding.boschindego/README.md index 37c66a9257551..0f983e0cf609e 100644 --- a/bundles/org.openhab.binding.boschindego/README.md +++ b/bundles/org.openhab.binding.boschindego/README.md @@ -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: -| 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 | -| cuttingTimeMapRefresh | The number of minutes between refreshing last/next cutting time and map | 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 diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/DeviceStatus.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/DeviceStatus.java index fbba18e2d8506..e6008e4f9e576 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/DeviceStatus.java +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/DeviceStatus.java @@ -29,6 +29,8 @@ @NonNullByDefault public class DeviceStatus { + public static final int STATE_LEARNING_LAWN = 516; + private final static String STATE_PREFIX = "indego.state."; private final static String STATE_UNKNOWN = "unknown"; @@ -45,7 +47,7 @@ public class DeviceStatus { entry(513, new DeviceStatus("mowing", false, DeviceCommand.MOW)), entry(514, new DeviceStatus("relocalising", 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(518, new DeviceStatus("border-cut", false, DeviceCommand.MOW)), entry(519, new DeviceStatus("idle-in-lawn", true, DeviceCommand.MOW)), 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 ef34f68b34170..03148db98a933 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,5 +25,5 @@ public class BoschIndegoConfiguration { public @Nullable String username; public @Nullable String password; public long refresh = 180; - public long cuttingTimeMapRefresh = 60; + public long cuttingTimeRefresh = 60; } 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 90ccf48b4abdf..3500ac0477bf9 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 @@ import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.*; +import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.time.Instant; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; @@ -40,6 +42,7 @@ import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; 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.unit.SIUnits; import org.openhab.core.library.unit.Units; @@ -64,6 +67,11 @@ @NonNullByDefault 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 HttpClient httpClient; private final BoschIndegoTranslationProvider translationProvider; @@ -71,10 +79,12 @@ public class BoschIndegoHandler extends BaseThingHandler { private @NonNullByDefault({}) IndegoController controller; private @Nullable ScheduledFuture statePollFuture; - private @Nullable ScheduledFuture cuttingTimeMapPollFuture; + private @Nullable ScheduledFuture cuttingTimePollFuture; private @Nullable ScheduledFuture cuttingTimeFuture; private boolean propertiesInitialized; private Optional previousStateCode = Optional.empty(); + private @Nullable RawType cachedMap; + private Instant cachedMapTimestamp = Instant.MIN; public BoschIndegoHandler(Thing thing, HttpClient httpClient, BoschIndegoTranslationProvider translationProvider, TimeZoneProvider timeZoneProvider) { @@ -108,9 +118,8 @@ public void initialize() { previousStateCode = Optional.empty(); this.statePollFuture = scheduler.scheduleWithFixedDelay(this::refreshStateAndOperatingDataWithExceptionHandling, 0, config.refresh, TimeUnit.SECONDS); - this.cuttingTimeMapPollFuture = scheduler.scheduleWithFixedDelay( - this::refreshCuttingTimesAndMapWithExceptionHandling, 0, config.cuttingTimeMapRefresh, - TimeUnit.MINUTES); + this.cuttingTimePollFuture = scheduler.scheduleWithFixedDelay(this::refreshCuttingTimesWithExceptionHandling, 0, + config.cuttingTimeRefresh, TimeUnit.MINUTES); } @Override @@ -121,11 +130,11 @@ public void dispose() { pollFuture.cancel(true); } this.statePollFuture = null; - pollFuture = this.cuttingTimeMapPollFuture; + pollFuture = this.cuttingTimePollFuture; if (pollFuture != null) { pollFuture.cancel(true); } - this.cuttingTimeMapPollFuture = null; + this.cuttingTimePollFuture = null; pollFuture = this.cuttingTimeFuture; if (pollFuture != null) { pollFuture.cancel(true); @@ -166,6 +175,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { private void handleRefreshCommand(String channelId) throws IndegoAuthenticationException, IndegoUnreachableException, IndegoException { switch (channelId) { + case GARDEN_MAP: + // Force map refresh and fall through to state update. + cachedMapTimestamp = Instant.MIN; case STATE: case TEXTUAL_STATE: case MOWED: @@ -185,9 +197,6 @@ private void handleRefreshCommand(String channelId) case GARDEN_SIZE: refreshOperatingData(); break; - case GARDEN_MAP: - refreshMap(); - break; } } @@ -243,9 +252,19 @@ private void refreshState() throws IndegoAuthenticationException, IndegoExceptio DeviceStateResponse state = controller.getState(); updateState(state); + if (state.mapUpdateAvailable) { + cachedMapTimestamp = Instant.MIN; + } + refreshMap(state.svgXPos, state.svgYPos); + // When state code changed, refresh cutting times immediately. if (previousStateCode.isPresent() && state.state != previousStateCode.get()) { 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); } @@ -311,22 +330,33 @@ private void scheduleCuttingTimesRefresh(Instant nextCutting) { } } - private void refreshCuttingTimesAndMapWithExceptionHandling() { - try { - refreshCuttingTimes(); - 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()); + private void refreshMap(int xPos, int yPos) throws IndegoAuthenticationException, IndegoException { + if (!isLinked(GARDEN_MAP)) { + return; } - } - - private void refreshMap() throws IndegoAuthenticationException, IndegoException { - if (isLinked(GARDEN_MAP)) { - updateState(GARDEN_MAP, controller.getMap()); + RawType cachedMap = this.cachedMap; + boolean mapRefreshed; + if (cachedMap == null + || cachedMapTimestamp.isBefore(Instant.now().minus(Duration.ofDays(MAP_REFRESH_INTERVAL_DAYS)))) { + 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("")) { + 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) + "\n"; + updateState(GARDEN_MAP, new RawType(svgMap.getBytes(), cachedMap.getMimeType())); } private void updateState(DeviceStateResponse state) { 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 0d79b191f21e8..fd985085e3c67 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,8 +10,8 @@ thing-type.boschindego.indego.description = Indego which supports the connect fe # thing types config -thing-type.config.boschindego.indego.cuttingTimeMapRefresh.label = Cutting Time/Map 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.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 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 29da1879e2b6f..b4d87a017daae 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 @@ -38,9 +38,9 @@ The number of seconds between refreshing device state. 180 - - - The number of minutes between refreshing last/next cutting time and map. + + + The number of minutes between refreshing last/next cutting time. true 60