[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:
| 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

View File

@ -29,6 +29,8 @@ import org.openhab.binding.boschindego.internal.dto.DeviceCommand;
@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)),

View File

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

View File

@ -14,6 +14,8 @@ package org.openhab.binding.boschindego.internal.handler;
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.DecimalType;
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 @@ import org.slf4j.LoggerFactory;
@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<Integer> 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 class BoschIndegoHandler extends BaseThingHandler {
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 class BoschIndegoHandler extends BaseThingHandler {
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 class BoschIndegoHandler extends BaseThingHandler {
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 @@ public class BoschIndegoHandler extends BaseThingHandler {
case GARDEN_SIZE:
refreshOperatingData();
break;
case GARDEN_MAP:
refreshMap();
break;
}
}
@ -243,9 +252,19 @@ public class BoschIndegoHandler extends BaseThingHandler {
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 @@ public class BoschIndegoHandler extends BaseThingHandler {
}
}
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("</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) {

View File

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

View File

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