[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:
parent
3b8567bd9e
commit
6028533e8e
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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)),
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user