[boschindego] Add device properties (#14829)

* 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>
This commit is contained in:
Jacob Laursen 2023-04-22 22:59:23 +02:00 committed by GitHub
parent b21913f5be
commit 275329d485
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 253 additions and 11 deletions

View 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/";

View 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);

View 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.
*

View File

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

View 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);
}

View File

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

View File

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

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

View 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()) {

View File

@ -34,6 +34,10 @@
<channel id="gardenMap" typeId="gardenMap"/>
</channels>
<properties>
<property name="vendor">Bosch</property>
</properties>
<representation-property>serialNumber</representation-property>
<config-description>