]> git.basschouten.com Git - openhab-addons.git/commitdiff
[boschindego] Add device properties (#14829)
authorJacob Laursen <jacob-github@vindvejr.dk>
Sat, 22 Apr 2023 20:59:23 +0000 (22:59 +0200)
committerGitHub <noreply@github.com>
Sat, 22 Apr 2023 20:59:23 +0000 (22:59 +0200)
* Add device properties
* Add vendor and model properties
* Use model as label in discovery

Resolves #14828

---------

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/BoschIndegoBindingConstants.java
bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoController.java
bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoDeviceController.java
bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/IndegoTypeDatabase.java [new file with mode: 0644]
bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/discovery/IndegoDiscoveryService.java
bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/DevicePropertiesResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/serialization/InstantDeserializer.java [new file with mode: 0644]
bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschAccountHandler.java
bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/handler/BoschIndegoHandler.java
bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/thing/thing-types.xml

index 11be02c7dae8b57228fef40227f880337b39e94b..f24eff511c8c2c1eda939704a9e8d396322a891d 100644 (file)
@@ -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<ThingTypeUID> 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<ThingTypeUID> 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/";
index 5e1c716fb062549b48fc96e29efac07f6156de0f..cf6da6fa165731915a3967700d44ca783919a6b1 100644 (file)
@@ -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);
index a6f016230f82d4e510dd2f0e0294dc13ef50c8f0..eb506b771667b5a47f7a838125a047d0572a56b4 100644 (file)
@@ -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 (file)
index 0000000..ccb5648
--- /dev/null
@@ -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);
+    }
+}
index 4d9d911403e92abd23b6816368a0bc62bdaa1fba..311b09edf2ada1d2736ca166c9aaa9ad783ae4ff 100644 (file)
@@ -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<String> serialNumbers = accountHandler.getSerialNumbers();
+            Collection<DevicePropertiesResponse> 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 (file)
index 0000000..2083c14
--- /dev/null
@@ -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 (file)
index 0000000..bb3c111
--- /dev/null
@@ -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<Instant> {
+
+    @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);
+        }
+    }
+}
index 8b30b621e26abe7d662d361337865a62a64f92e2..69ffed8fd54218a0b59c07de6d56c1aeb7ae48f1 100644 (file)
@@ -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<String> getSerialNumbers() throws IndegoException {
-        return controller.getSerialNumbers();
+    public Collection<DevicePropertiesResponse> getDevices() throws IndegoException {
+        Collection<String> serialNumbers = controller.getSerialNumbers();
+        List<DevicePropertiesResponse> devices = new ArrayList<DevicePropertiesResponse>(serialNumbers.size());
+
+        for (String serialNumber : serialNumbers) {
+            DevicePropertiesResponse properties = controller.getDeviceProperties(serialNumber);
+            if (properties.serialNumber == null) {
+                properties.serialNumber = serialNumber;
+            }
+            devices.add(properties);
+        }
+
+        return devices;
     }
 }
index f57798bbca76f83af3926bef0a131b581db58939..672a9756cd75745c6fb0a659a2bedd3d0c6cab82 100644 (file)
@@ -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<String, String> 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()) {
index 85a005481f8d0cead32d31d4e98d264216259da5..2fc4705ede2a1a9f0e598882df91033eb77fcb4e 100644 (file)
                        <channel id="gardenMap" typeId="gardenMap"/>
                </channels>
 
+               <properties>
+                       <property name="vendor">Bosch</property>
+               </properties>
+
                <representation-property>serialNumber</representation-property>
 
                <config-description>