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";
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/";
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;
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;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
/**
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;
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);
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;
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.
*
--- /dev/null
+/**
+ * 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);
+ }
+}
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;
@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);
}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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);
+ }
+ }
+}
import static org.openhab.binding.boschindego.internal.BoschIndegoBindingConstants.*;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
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;
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;
}
}
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;
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;
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);
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;
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);
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;
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()) {
<channel id="gardenMap" typeId="gardenMap"/>
</channels>
+ <properties>
+ <property name="vendor">Bosch</property>
+ </properties>
+
<representation-property>serialNumber</representation-property>
<config-description>