diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/BoschIndegoBindingConstants.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/BoschIndegoBindingConstants.java index 11be02c7d..f24eff511 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/BoschIndegoBindingConstants.java +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/BoschIndegoBindingConstants.java @@ -32,6 +32,8 @@ public class BoschIndegoBindingConstants { public static final ThingTypeUID THING_TYPE_ACCOUNT = new ThingTypeUID(BINDING_ID, "account"); public static final ThingTypeUID THING_TYPE_INDEGO = new ThingTypeUID(BINDING_ID, "indego"); + public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT, THING_TYPE_INDEGO); + // List of all Channel ids public static final String STATE = "state"; public static final String TEXTUAL_STATE = "textualstate"; @@ -48,7 +50,11 @@ public class BoschIndegoBindingConstants { public static final String GARDEN_SIZE = "gardenSize"; public static final String GARDEN_MAP = "gardenMap"; - public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ACCOUNT, THING_TYPE_INDEGO); + // Device properties + public static final String PROPERTY_BARE_TOOL_NUMBER = "bareToolNumber"; + public static final String PROPERTY_SERVICE_COUNTER = "serviceCounter"; + public static final String PROPERTY_NEEDS_SERVICE = "needsService"; + public static final String PROPERTY_RENEW_DATE = "renewDate"; // Bosch SingleKey ID OAuth2 private static final String BSK_BASE_URI = "https://prodindego.b2clogin.com/prodindego.onmicrosoft.com/b2c_1a_signup_signin/oauth2/v2.0/"; diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoController.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoController.java index 5e1c716fb..cf6da6fa1 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoController.java +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoController.java @@ -15,6 +15,7 @@ package org.openhab.binding.boschindego.internal; import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.*; import java.io.IOException; +import java.time.Instant; import java.util.Arrays; import java.util.Collection; import java.util.concurrent.ExecutionException; @@ -31,8 +32,10 @@ import org.eclipse.jetty.client.util.StringContentProvider; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.boschindego.internal.dto.response.DevicePropertiesResponse; import org.openhab.binding.boschindego.internal.dto.response.ErrorResponse; import org.openhab.binding.boschindego.internal.dto.response.Mower; +import org.openhab.binding.boschindego.internal.dto.serialization.InstantDeserializer; import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException; import org.openhab.binding.boschindego.internal.exceptions.IndegoException; import org.openhab.binding.boschindego.internal.exceptions.IndegoInvalidCommandException; @@ -48,6 +51,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.gson.JsonParseException; /** @@ -62,11 +66,10 @@ public class IndegoController { private static final String BASE_URL = "https://api.indego-cloud.iot.bosch-si.com/api/v1/"; private static final String CONTENT_TYPE_HEADER = "application/json"; - private static final String BEARER = "Bearer "; private final Logger logger = LoggerFactory.getLogger(IndegoController.class); - private final Gson gson = new Gson(); + private final Gson gson = new GsonBuilder().registerTypeAdapter(Instant.class, new InstantDeserializer()).create(); private final HttpClient httpClient; private final OAuthClientService oAuthClientService; private final String userAgent; @@ -96,6 +99,19 @@ public class IndegoController { return Arrays.stream(mowers).map(m -> m.serialNumber).toList(); } + /** + * Queries the serial number and device service properties from the server. + * + * @param serialNumber the serial number of the device + * @return the device serial number and properties + * @throws IndegoAuthenticationException if request was rejected as unauthorized + * @throws IndegoException if any communication or parsing error occurred + */ + public DevicePropertiesResponse getDeviceProperties(String serialNumber) + throws IndegoAuthenticationException, IndegoException { + return getRequest(SERIAL_NUMBER_SUBPATH + serialNumber + "/", DevicePropertiesResponse.class); + } + private String getAuthorizationUrl() { try { return oAuthClientService.getAuthorizationUrl(BSK_REDIRECT_URI, BSK_SCOPE, null); diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoDeviceController.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoDeviceController.java index a6f016230..eb506b771 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoDeviceController.java +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoDeviceController.java @@ -24,6 +24,7 @@ import org.openhab.binding.boschindego.internal.dto.PredictiveAdjustment; import org.openhab.binding.boschindego.internal.dto.PredictiveStatus; import org.openhab.binding.boschindego.internal.dto.request.SetStateRequest; import org.openhab.binding.boschindego.internal.dto.response.DeviceCalendarResponse; +import org.openhab.binding.boschindego.internal.dto.response.DevicePropertiesResponse; 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.OperatingDataResponse; @@ -71,6 +72,17 @@ public class IndegoDeviceController extends IndegoController { this.serialNumber = serialNumber; } + /** + * Queries the serial number and device service properties from the server. + * + * @return the device serial number and properties + * @throws IndegoAuthenticationException if request was rejected as unauthorized + * @throws IndegoException if any communication or parsing error occurred + */ + public DevicePropertiesResponse getDeviceProperties() throws IndegoAuthenticationException, IndegoException { + return super.getDeviceProperties(serialNumber); + } + /** * Queries the device state from the server. * diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoTypeDatabase.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoTypeDatabase.java new file mode 100644 index 000000000..ccb5648e1 --- /dev/null +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoTypeDatabase.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2023 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; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Translates from tool number to model names. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class IndegoTypeDatabase { + + /** + * Return tool name from tool type number. + * + * @see https://www.boschtoolservice.com/gb/en/boschdiy/spareparts/search-results?q=Indego + * + * @param toolTypeNumber condensed tool type number, e.g. "3600HA2200" rather than "3 600 HA2 200". + * @return tool type name + */ + public static String nameFromTypeNumber(String toolTypeNumber) { + String name = switch (toolTypeNumber) { + case "3600HA2103" -> "800"; + case "3600HA2104" -> "850"; + case "3600HA2200", "3600HA2201" -> "1300"; + case "3600HA2300" -> "1000 Connect"; + case "3600HA2301" -> "1200 Connect"; + case "3600HA2302" -> "1100 Connect"; + case "3600HA2303" -> "13C"; + case "3600HA2304" -> "10C"; + case "3600HB0000" -> "350"; + case "3600HB0001" -> "400"; + case "3600HB0004" -> "XS 300"; + case "3600HB0006" -> "350"; + case "3600HB0007" -> "400"; + case "3600HB0100" -> "350 Connect"; + case "3600HB0101" -> "400 Connect"; + case "3600HB0102" -> "S+ 350"; + case "3600HB0103" -> "S+ 400"; + case "3600HB0105" -> "S+ 350"; + case "3600HB0106" -> "S+ 400"; + case "3600HB0201" -> "M"; + case "3600HB0202" -> "S 500"; + case "3600HB0203" -> "M 700"; + case "3600HB0301" -> "M+"; + case "3600HB0302" -> "S+ 500"; + case "3600HB0303" -> "M+ 700"; + default -> ""; + }; + + return (name.isEmpty() ? "Indego" : "Indego " + name); + } +} diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/discovery/IndegoDiscoveryService.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/discovery/IndegoDiscoveryService.java index 4d9d91140..311b09edf 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/discovery/IndegoDiscoveryService.java +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/discovery/IndegoDiscoveryService.java @@ -20,6 +20,8 @@ import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.boschindego.internal.IndegoTypeDatabase; +import org.openhab.binding.boschindego.internal.dto.response.DevicePropertiesResponse; import org.openhab.binding.boschindego.internal.exceptions.IndegoException; import org.openhab.binding.boschindego.internal.handler.BoschAccountHandler; import org.openhab.core.config.discovery.AbstractDiscoveryService; @@ -71,15 +73,15 @@ public class IndegoDiscoveryService extends AbstractDiscoveryService implements @Override public void startScan() { try { - Collection serialNumbers = accountHandler.getSerialNumbers(); + Collection devices = accountHandler.getDevices(); ThingUID bridgeUID = accountHandler.getThing().getUID(); - for (String serialNumber : serialNumbers) { - ThingUID thingUID = new ThingUID(THING_TYPE_INDEGO, bridgeUID, serialNumber); + for (DevicePropertiesResponse device : devices) { + ThingUID thingUID = new ThingUID(THING_TYPE_INDEGO, bridgeUID, device.serialNumber); DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID) - .withProperty(Thing.PROPERTY_SERIAL_NUMBER, serialNumber).withBridge(bridgeUID) + .withProperty(Thing.PROPERTY_SERIAL_NUMBER, device.serialNumber).withBridge(bridgeUID) .withRepresentationProperty(Thing.PROPERTY_SERIAL_NUMBER) - .withLabel("Indego (" + serialNumber + ")").build(); + .withLabel(IndegoTypeDatabase.nameFromTypeNumber(device.bareToolNumber)).build(); thingDiscovered(discoveryResult); } diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/DevicePropertiesResponse.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/DevicePropertiesResponse.java new file mode 100644 index 000000000..2083c146e --- /dev/null +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/DevicePropertiesResponse.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2023 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 com.google.gson.annotations.SerializedName; + +/** + * Response for serial number and other device service properties. + * + * @author Jacob Laursen - Initial contribution + */ +public class DevicePropertiesResponse { + + @SerializedName("alm_sn") + public String serialNumber = ""; + + @SerializedName("service_counter") + public int serviceCounter; + + @SerializedName("needs_service") + public boolean needsService; + + /** + * Mode: manual, smart + */ + @SerializedName("alm_mode") + public String mode; + + @SerializedName("bareToolnumber") + public String bareToolNumber; + + @SerializedName("alm_firmware_version") + public String firmwareVersion; + + @SerializedName("renew_date") + public Instant renewDate; +} diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/serialization/InstantDeserializer.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/serialization/InstantDeserializer.java new file mode 100644 index 000000000..bb3c11100 --- /dev/null +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/serialization/InstantDeserializer.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2023 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.serialization; + +import java.lang.reflect.Type; +import java.time.Instant; +import java.time.format.DateTimeParseException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; + +/** + * The {@link InstantDeserializer} converts a formatted UTC string to {@link Instant}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class InstantDeserializer implements JsonDeserializer { + + @Override + public @Nullable Instant deserialize(JsonElement element, Type arg1, JsonDeserializationContext arg2) + throws JsonParseException { + try { + return Instant.parse(element.getAsString()); + } catch (DateTimeParseException e) { + throw new JsonParseException("Could not parse as Instant: " + element.getAsString(), e); + } + } +} diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschAccountHandler.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschAccountHandler.java index 8b30b621e..69ffed8fd 100644 --- a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschAccountHandler.java +++ b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschAccountHandler.java @@ -15,6 +15,7 @@ package org.openhab.binding.boschindego.internal.handler; import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.*; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -22,6 +23,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.boschindego.internal.IndegoController; import org.openhab.binding.boschindego.internal.discovery.IndegoDiscoveryService; +import org.openhab.binding.boschindego.internal.dto.response.DevicePropertiesResponse; import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException; import org.openhab.binding.boschindego.internal.exceptions.IndegoException; import org.openhab.core.auth.client.oauth2.AccessTokenResponse; @@ -119,7 +121,18 @@ public class BoschAccountHandler extends BaseBridgeHandler { return oAuthClientService; } - public Collection getSerialNumbers() throws IndegoException { - return controller.getSerialNumbers(); + public Collection getDevices() throws IndegoException { + Collection serialNumbers = controller.getSerialNumbers(); + List devices = new ArrayList(serialNumbers.size()); + + for (String serialNumber : serialNumbers) { + DevicePropertiesResponse properties = controller.getDeviceProperties(serialNumber); + if (properties.serialNumber == null) { + properties.serialNumber = serialNumber; + } + devices.add(properties); + } + + return devices; } } 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 f57798bbc..672a9756c 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 @@ -17,8 +17,10 @@ import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstan import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; +import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; +import java.util.Map; import java.util.Optional; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -29,8 +31,10 @@ import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.boschindego.internal.BoschIndegoTranslationProvider; import org.openhab.binding.boschindego.internal.DeviceStatus; import org.openhab.binding.boschindego.internal.IndegoDeviceController; +import org.openhab.binding.boschindego.internal.IndegoTypeDatabase; import org.openhab.binding.boschindego.internal.config.BoschIndegoConfiguration; import org.openhab.binding.boschindego.internal.dto.DeviceCommand; +import org.openhab.binding.boschindego.internal.dto.response.DevicePropertiesResponse; import org.openhab.binding.boschindego.internal.dto.response.DeviceStateResponse; import org.openhab.binding.boschindego.internal.dto.response.OperatingDataResponse; import org.openhab.binding.boschindego.internal.exceptions.IndegoAuthenticationException; @@ -75,6 +79,7 @@ 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 Duration DEVICE_PROPERTIES_VALIDITY_PERIOD = Duration.ofDays(1); private static final Duration MAP_REFRESH_INTERVAL = Duration.ofDays(1); private static final Duration OPERATING_DATA_INACTIVE_REFRESH_INTERVAL = Duration.ofHours(6); @@ -87,6 +92,7 @@ public class BoschIndegoHandler extends BaseThingHandler { private final HttpClient httpClient; private final BoschIndegoTranslationProvider translationProvider; private final TimeZoneProvider timeZoneProvider; + private Instant devicePropertiesUpdated = Instant.MIN; private @NonNullByDefault({}) OAuthClientService oAuthClientService; private @NonNullByDefault({}) IndegoDeviceController controller; @@ -133,7 +139,8 @@ public class BoschIndegoHandler extends BaseThingHandler { return; } - this.updateProperty(Thing.PROPERTY_SERIAL_NUMBER, config.serialNumber); + devicePropertiesUpdated = Instant.MIN; + updateProperty(Thing.PROPERTY_SERIAL_NUMBER, config.serialNumber); controller = new IndegoDeviceController(httpClient, oAuthClientService, config.serialNumber); @@ -306,6 +313,10 @@ public class BoschIndegoHandler extends BaseThingHandler { DeviceStatus deviceStatus = DeviceStatus.fromCode(state.state); updateState(state); + if (devicePropertiesUpdated.isBefore(Instant.now().minus(DEVICE_PROPERTIES_VALIDITY_PERIOD))) { + refreshDeviceProperties(); + } + // Update map and start tracking positions if mower is active. if (state.mapUpdateAvailable) { cachedMapTimestamp = Instant.MIN; @@ -348,6 +359,26 @@ public class BoschIndegoHandler extends BaseThingHandler { rescheduleStatePollAccordingToState(deviceStatus); } + private void refreshDeviceProperties() throws IndegoAuthenticationException, IndegoException { + DevicePropertiesResponse deviceProperties = controller.getDeviceProperties(); + Map properties = editProperties(); + if (deviceProperties.firmwareVersion != null) { + properties.put(Thing.PROPERTY_FIRMWARE_VERSION, deviceProperties.firmwareVersion); + } + if (deviceProperties.bareToolNumber != null) { + properties.put(Thing.PROPERTY_MODEL_ID, + IndegoTypeDatabase.nameFromTypeNumber(deviceProperties.bareToolNumber)); + properties.put(PROPERTY_BARE_TOOL_NUMBER, deviceProperties.bareToolNumber); + } + properties.put(PROPERTY_SERVICE_COUNTER, String.valueOf(deviceProperties.serviceCounter)); + properties.put(PROPERTY_NEEDS_SERVICE, String.valueOf(deviceProperties.needsService)); + properties.put(PROPERTY_RENEW_DATE, + LocalDateTime.ofInstant(deviceProperties.renewDate, timeZoneProvider.getTimeZone()).toString()); + + updateProperties(properties); + devicePropertiesUpdated = Instant.now(); + } + private void rescheduleStatePollAccordingToState(DeviceStatus deviceStatus) { int refreshIntervalSeconds; if (deviceStatus.isActive()) { 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 85a005481..2fc4705ed 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 @@ -34,6 +34,10 @@ + + Bosch + + serialNumber