]> git.basschouten.com Git - openhab-addons.git/commitdiff
[boschindego] Add new channels (#13040)
authorJacob Laursen <jacob-github@vindvejr.dk>
Sat, 2 Jul 2022 10:31:51 +0000 (12:31 +0200)
committerGitHub <noreply@github.com>
Sat, 2 Jul 2022 10:31:51 +0000 (12:31 +0200)
Fixes #12938

Fixes #13017

Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>
bundles/org.openhab.binding.boschindego/README.md
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/config/BoschIndegoConfiguration.java
bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/Battery.java [new file with mode: 0644]
bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/Garden.java [new file with mode: 0644]
bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/OperatingDataResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/exceptions/IndegoUnreachableException.java [new file with mode: 0644]
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/i18n/boschindego.properties
bundles/org.openhab.binding.boschindego/src/main/resources/OH-INF/thing/thing-types.xml

index 5dee2a5982348ff28e4caddee6ee1e98f73346ae..c4578670b7735b8f0ce2b42cb3ad7a9578679187 100644 (file)
@@ -8,25 +8,31 @@ 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:
 
-| Parameter          | Description                                                     | Default |
-|--------------------|-----------------------------------------------------------------|---------|
-| username           | Username for the Bosch Indego account                           |         |
-| password           | Password for the Bosch Indego account                           |         |
-| refresh            | The number of seconds between refreshing device state           | 180     |
-| cuttingTimeRefresh | The number of minutes between refreshing last/next cutting time | 60      |
+| Parameter             | Description                                                             | Default |
+|-----------------------|-------------------------------------------------------------------------|---------|
+| username              | Username for the Bosch Indego account                                   |         |
+| password              | Password for the Bosch Indego account                                   |         |
+| refresh               | The number of seconds between refreshing device state                   | 180     |
+| cuttingTimeMapRefresh | The number of minutes between refreshing last/next cutting time and map | 60      |
 
 ## Channels
 
-| Channel      | Item Type   | Description                                                                                                                         |
-|--------------|-------------|-------------------------------------------------------------------------------------------------------------------------------------|
-| state        | Number      | You can send commands to this channel to control the mower and read the simplified state from it (1=mow, 2=return to dock, 3=pause) |
-| errorcode    | Number      | Error code of the mower (0=no error, readonly)                                                                                      |
-| statecode    | Number      | Detailed state of the mower (readonly)                                                                                              |
-| textualstate | String      | State as a text. (readonly)                                                                                                         |
-| ready        | Number      | Shows if the mower is ready to mow (1=ready, 0=not ready, readonly)                                                                 |
-| mowed        | Dimmer      | Cut grass in percent (readonly)                                                                                                     |
-| lastCutting  | DateTime    | Last cutting time (readonly)                                                                                                        |
-| nextCutting  | DateTime    | Next scheduled cutting time (readonly)                                                                                              |
+| Channel            | Item Type                | Description                                                                                                                         | Writeable |
+|--------------------|--------------------------|-------------------------------------------------------------------------------------------------------------------------------------|-----------|
+| state              | Number                   | You can send commands to this channel to control the mower and read the simplified state from it (1=mow, 2=return to dock, 3=pause) | Yes       |
+| errorcode          | Number                   | Error code of the mower (0=no error)                                                                                                |           |
+| statecode          | Number                   | Detailed state of the mower                                                                                                         |           |
+| textualstate       | String                   | State as a text.                                                                                                                    |           |
+| ready              | Number                   | Shows if the mower is ready to mow (1=ready, 0=not ready)                                                                           |           |
+| mowed              | Dimmer                   | Cut grass in percent                                                                                                                |           |
+| lastCutting        | DateTime                 | Last cutting time                                                                                                                   |           |
+| nextCutting        | DateTime                 | Next scheduled cutting time                                                                                                         |           |
+| batteryVoltage     | Number:ElectricPotential | Battery voltage reported by the device                                                                                              |           |
+| batteryLevel       | Number                   | Battery level as a percentage (0-100%)                                                                                              |           |
+| lowBattery         | Switch                   | Low battery warning with possible values on (low battery) and off (battery ok)                                                      |           |
+| batteryTemperature | Number:Temperature       | Battery temperature reported by the device                                                                                          |           |
+| gardenSize         | Number:Area              | Garden size mapped by the device                                                                                                    |           |
+| gardenMap          | Image                    | Garden map mapped by the device                                                                                                     |           |
 
 ### State Codes
 
@@ -81,6 +87,12 @@ Number Indego_Ready { channel="boschindego:indego:lawnmower:ready" }
 Dimmer Indego_Mowed { channel="boschindego:indego:lawnmower:mowed" }
 DateTime Indego_LastCutting { channel="boschindego:indego:lawnmower:lastCutting" }
 DateTime Indego_NextCutting { channel="boschindego:indego:lawnmower:nextCutting" }
+Number:ElectricPotential Indego_BatteryVoltage { channel="boschindego:indego:lawnmower:batteryVoltage" }
+Number Indego_BatteryLevel { channel="boschindego:indego:lawnmower:batteryLevel" }
+Switch Indego_LowBattery { channel="boschindego:indego:lawnmower:lowBattery" }
+Number:Temperature Indego_BatteryTemperature { channel="boschindego:indego:lawnmower:batteryTemperature" }
+Number:Area Indego_GardenSize { channel="boschindego:indego:lawnmower:gardenSize" }
+Image Indego_GardenMap { channel="boschindego:indego:lawnmower:gardenMap" }
 ```
 
 ### `indego.sitemap` File
index e1c6ff3d58b63a9a6045b3a15435f5169398e7f3..d3902e77da70ac08f5dfefc4d2579e7e25980363 100644 (file)
@@ -40,6 +40,12 @@ public class BoschIndegoBindingConstants {
     public static final String READY = "ready";
     public static final String LAST_CUTTING = "lastCutting";
     public static final String NEXT_CUTTING = "nextCutting";
+    public static final String BATTERY_VOLTAGE = "batteryVoltage";
+    public static final String BATTERY_LEVEL = "batteryLevel";
+    public static final String LOW_BATTERY = "lowBattery";
+    public static final String BATTERY_TEMPERATURE = "batteryTemperature";
+    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_INDEGO);
 }
index dd50c8e7cf7fa09feb4df4d82c43f38c6f30f883..70c66c9f7b0f758a61597af03a40a18c1f851ce0 100644 (file)
@@ -38,12 +38,15 @@ import org.openhab.binding.boschindego.internal.dto.response.AuthenticationRespo
 import org.openhab.binding.boschindego.internal.dto.response.DeviceCalendarResponse;
 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;
 import org.openhab.binding.boschindego.internal.dto.response.PredictiveLastCuttingResponse;
 import org.openhab.binding.boschindego.internal.dto.response.PredictiveNextCuttingResponse;
 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.openhab.binding.boschindego.internal.exceptions.IndegoInvalidResponseException;
+import org.openhab.binding.boschindego.internal.exceptions.IndegoUnreachableException;
+import org.openhab.core.library.types.RawType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -216,6 +219,9 @@ public class IndegoController {
                 // This will currently not happen because "WWW-Authenticate" header is missing; see below.
                 throw new IndegoAuthenticationException("Context rejected");
             }
+            if (status == HttpStatus.GATEWAY_TIMEOUT_504) {
+                throw new IndegoUnreachableException("Gateway timeout");
+            }
             if (!HttpStatus.isSuccess(status)) {
                 throw new IndegoException("The request failed with error: " + status);
             }
@@ -256,6 +262,93 @@ public class IndegoController {
         }
     }
 
+    /**
+     * Wraps {@link #getRawRequest(String)} into an authenticated session.
+     *
+     * @param path the relative path to which the request should be sent
+     * @return the raw data from the response
+     * @throws IndegoAuthenticationException if request was rejected as unauthorized
+     * @throws IndegoException if any communication or parsing error occurred
+     */
+    private RawType getRawRequestWithAuthentication(String path) throws IndegoAuthenticationException, IndegoException {
+        if (!session.isValid()) {
+            authenticate();
+        }
+        try {
+            logger.debug("Session {} valid, skipping authentication", session);
+            return getRawRequest(path);
+        } catch (IndegoAuthenticationException e) {
+            if (logger.isTraceEnabled()) {
+                logger.trace("Context rejected", e);
+            } else {
+                logger.debug("Context rejected: {}", e.getMessage());
+            }
+            session.invalidate();
+            authenticate();
+            return getRawRequest(path);
+        }
+    }
+
+    /**
+     * Sends a GET request to the server and returns the raw response.
+     * 
+     * @param path the relative path to which the request should be sent
+     * @return the raw data from the response
+     * @throws IndegoAuthenticationException if request was rejected as unauthorized
+     * @throws IndegoException if any communication or parsing error occurred
+     */
+    private RawType getRawRequest(String path) throws IndegoAuthenticationException, IndegoException {
+        try {
+            Request request = httpClient.newRequest(BASE_URL + path).method(HttpMethod.GET).header(CONTEXT_HEADER_NAME,
+                    session.getContextId());
+            if (logger.isTraceEnabled()) {
+                logger.trace("GET request for {}", BASE_URL + path);
+            }
+            ContentResponse response = sendRequest(request);
+            int status = response.getStatus();
+            if (status == HttpStatus.UNAUTHORIZED_401) {
+                // This will currently not happen because "WWW-Authenticate" header is missing; see below.
+                throw new IndegoAuthenticationException("Context rejected");
+            }
+            if (!HttpStatus.isSuccess(status)) {
+                throw new IndegoException("The request failed with error: " + status);
+            }
+            byte[] data = response.getContent();
+            if (data == null) {
+                throw new IndegoInvalidResponseException("No data returned");
+            }
+            String contentType = response.getMediaType();
+            if (contentType == null || contentType.isEmpty()) {
+                throw new IndegoInvalidResponseException("No content-type returned");
+            }
+            logger.debug("Media download response: type {}, length {}", contentType, data.length);
+
+            return new RawType(data, contentType);
+        } catch (JsonParseException e) {
+            throw new IndegoInvalidResponseException("Error parsing response", e);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new IndegoException(e);
+        } catch (TimeoutException e) {
+            throw new IndegoException(e);
+        } catch (ExecutionException e) {
+            Throwable cause = e.getCause();
+            if (cause != null && cause instanceof HttpResponseException) {
+                Response response = ((HttpResponseException) cause).getResponse();
+                if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
+                    /*
+                     * When contextId is not valid, the service will respond with HTTP code 401 without
+                     * any "WWW-Authenticate" header, violating RFC 7235. Jetty will then throw
+                     * HttpResponseException. We need to handle this in order to attempt
+                     * reauthentication.
+                     */
+                    throw new IndegoAuthenticationException("Context rejected", e);
+                }
+            }
+            throw new IndegoException(e);
+        }
+    }
+
     /**
      * Wraps {@link #putRequest(String, Object)} into an authenticated session.
      * 
@@ -381,6 +474,30 @@ public class IndegoController {
                 DeviceStateResponse.class);
     }
 
+    /**
+     * Queries the device operating data from the server.
+     * Server will request this directly from the device, so operation might be slow.
+     * 
+     * @return the device state
+     * @throws IndegoAuthenticationException if request was rejected as unauthorized
+     * @throws IndegoException if any communication or parsing error occurred
+     */
+    public OperatingDataResponse getOperatingData() throws IndegoAuthenticationException, IndegoException {
+        return getRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/operatingData",
+                OperatingDataResponse.class);
+    }
+
+    /**
+     * Queries the map generated by the device from the server.
+     * 
+     * @return the garden map
+     * @throws IndegoAuthenticationException if request was rejected as unauthorized
+     * @throws IndegoException if any communication or parsing error occurred
+     */
+    public RawType getMap() throws IndegoAuthenticationException, IndegoException {
+        return getRawRequestWithAuthentication(SERIAL_NUMBER_SUBPATH + this.getSerialNumber() + "/map");
+    }
+
     /**
      * Queries the calendar.
      * 
index 03148db98a933567031aa5a2119be3d4192badb6..ef34f68b34170910bd467a34c3befe22dabb697b 100644 (file)
@@ -25,5 +25,5 @@ public class BoschIndegoConfiguration {
     public @Nullable String username;
     public @Nullable String password;
     public long refresh = 180;
-    public long cuttingTimeRefresh = 60;
+    public long cuttingTimeMapRefresh = 60;
 }
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/Battery.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/Battery.java
new file mode 100644 (file)
index 0000000..8aef3ce
--- /dev/null
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2022 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;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Battery data.
+ * 
+ * @author Jacob Laursen - Initial contribution
+ */
+public class Battery {
+    public double voltage;
+
+    public int cycles;
+
+    public double discharge;
+
+    @SerializedName("ambient_temp")
+    public int ambientTemperature;
+
+    @SerializedName("battery_temp")
+    public int batteryTemperature;
+
+    public int percent;
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/Garden.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/Garden.java
new file mode 100644 (file)
index 0000000..1a6f198
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2010-2022 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;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Garden data.
+ * 
+ * @author Jacob Laursen - Initial contribution
+ */
+public class Garden {
+    public long id;
+
+    public String name;
+
+    @SerializedName("signal_id")
+    public byte signalId;
+
+    public int size;
+
+    @SerializedName("inner_bounds")
+    public int innerBounds;
+
+    public int cuts;
+
+    public int runtime;
+
+    public int charge;
+
+    public int bumps;
+
+    public int stops;
+
+    @SerializedName("last_mow")
+    public int lastMow;
+
+    @SerializedName("map_cell_size")
+    public int mapCellSize;
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/OperatingDataResponse.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/dto/response/OperatingDataResponse.java
new file mode 100644 (file)
index 0000000..385f3a8
--- /dev/null
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2022 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 org.openhab.binding.boschindego.internal.dto.Battery;
+import org.openhab.binding.boschindego.internal.dto.Garden;
+import org.openhab.binding.boschindego.internal.dto.response.runtime.DeviceStateRuntimes;
+
+/**
+ * Response for operating data.
+ * 
+ * @author Jacob Laursen - Initial contribution
+ */
+public class OperatingDataResponse {
+    public DeviceStateRuntimes runtime;
+
+    public Battery battery;
+
+    public Garden garden;
+}
diff --git a/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/exceptions/IndegoUnreachableException.java b/bundles/org.openhab.binding.boschindego/src/main/java/org/openhab/binding/boschindego/internal/exceptions/IndegoUnreachableException.java
new file mode 100644 (file)
index 0000000..1af704f
--- /dev/null
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2022 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.exceptions;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * {@link IndegoUnreachableException} is thrown on gateway timeout, which
+ * means that Bosch services cannot connect to the device.
+ * 
+ * @author Jacob Laursen - Initial contribution
+ */
+@NonNullByDefault
+public class IndegoUnreachableException extends IndegoException {
+
+    private static final long serialVersionUID = -7952585411438042139L;
+
+    public IndegoUnreachableException(String message) {
+        super(message);
+    }
+
+    public IndegoUnreachableException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
index da247be3399eb59e216d2cf6f14a807c69f4e7e8..0dec921f0763db08e516ee6b88a56ceb6e018f8c 100644 (file)
@@ -28,13 +28,18 @@ import org.openhab.binding.boschindego.internal.IndegoController;
 import org.openhab.binding.boschindego.internal.config.BoschIndegoConfiguration;
 import org.openhab.binding.boschindego.internal.dto.DeviceCommand;
 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;
 import org.openhab.binding.boschindego.internal.exceptions.IndegoException;
 import org.openhab.core.i18n.TimeZoneProvider;
 import org.openhab.core.library.types.DateTimeType;
 import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
 import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.QuantityType;
 import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.library.unit.Units;
 import org.openhab.core.thing.ChannelUID;
 import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingStatus;
@@ -63,7 +68,7 @@ public class BoschIndegoHandler extends BaseThingHandler {
 
     private @NonNullByDefault({}) IndegoController controller;
     private @Nullable ScheduledFuture<?> statePollFuture;
-    private @Nullable ScheduledFuture<?> cuttingTimePollFuture;
+    private @Nullable ScheduledFuture<?> cuttingTimeMapPollFuture;
     private boolean propertiesInitialized;
     private int previousStateCode;
 
@@ -96,10 +101,11 @@ public class BoschIndegoHandler extends BaseThingHandler {
         controller = new IndegoController(httpClient, username, password);
 
         updateStatus(ThingStatus.UNKNOWN);
-        this.statePollFuture = scheduler.scheduleWithFixedDelay(this::refreshStateWithExceptionHandling, 0,
-                config.refresh, TimeUnit.SECONDS);
-        this.cuttingTimePollFuture = scheduler.scheduleWithFixedDelay(this::refreshCuttingTimesWithExceptionHandling, 0,
-                config.cuttingTimeRefresh, TimeUnit.MINUTES);
+        this.statePollFuture = scheduler.scheduleWithFixedDelay(this::refreshStateAndOperatingDataWithExceptionHandling,
+                0, config.refresh, TimeUnit.SECONDS);
+        this.cuttingTimeMapPollFuture = scheduler.scheduleWithFixedDelay(
+                this::refreshCuttingTimesAndMapWithExceptionHandling, 0, config.cuttingTimeMapRefresh,
+                TimeUnit.MINUTES);
     }
 
     @Override
@@ -110,21 +116,21 @@ public class BoschIndegoHandler extends BaseThingHandler {
             pollFuture.cancel(true);
         }
         this.statePollFuture = null;
-        pollFuture = this.cuttingTimePollFuture;
+        pollFuture = this.cuttingTimeMapPollFuture;
         if (pollFuture != null) {
             pollFuture.cancel(true);
         }
-        this.cuttingTimePollFuture = null;
+        this.cuttingTimeMapPollFuture = null;
     }
 
     @Override
     public void handleCommand(ChannelUID channelUID, Command command) {
+        logger.debug("handleCommand {} for channel {}", command, channelUID);
         try {
             if (command == RefreshType.REFRESH) {
                 handleRefreshCommand(channelUID.getId());
                 return;
             }
-
             if (command instanceof DecimalType && channelUID.getId().equals(STATE)) {
                 sendCommand(((DecimalType) command).intValue());
             }
@@ -144,11 +150,21 @@ public class BoschIndegoHandler extends BaseThingHandler {
             case ERRORCODE:
             case STATECODE:
             case READY:
-                this.refreshState();
+                refreshState();
                 break;
             case LAST_CUTTING:
             case NEXT_CUTTING:
-                this.refreshCuttingTimes();
+                refreshCuttingTimes();
+                break;
+            case BATTERY_LEVEL:
+            case LOW_BATTERY:
+            case BATTERY_VOLTAGE:
+            case BATTERY_TEMPERATURE:
+            case GARDEN_SIZE:
+                refreshOperatingData();
+                break;
+            case GARDEN_MAP:
+                refreshMap();
                 break;
         }
     }
@@ -178,14 +194,13 @@ public class BoschIndegoHandler extends BaseThingHandler {
         logger.debug("Sending command {}", command);
         updateState(TEXTUAL_STATE, UnDefType.UNDEF);
         controller.sendCommand(command);
-        state = controller.getState();
-        updateStatus(ThingStatus.ONLINE);
-        updateState(state);
+        refreshState();
     }
 
-    private void refreshStateWithExceptionHandling() {
+    private void refreshStateAndOperatingDataWithExceptionHandling() {
         try {
             refreshState();
+            refreshOperatingData();
         } catch (IndegoAuthenticationException e) {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
                     "@text/offline.comm-error.authentication-failure");
@@ -201,7 +216,6 @@ public class BoschIndegoHandler extends BaseThingHandler {
         }
 
         DeviceStateResponse state = controller.getState();
-        updateStatus(ThingStatus.ONLINE);
         updateState(state);
 
         // When state code changed, refresh cutting times immediately.
@@ -211,15 +225,9 @@ public class BoschIndegoHandler extends BaseThingHandler {
         }
     }
 
-    private void refreshCuttingTimesWithExceptionHandling() {
-        try {
-            refreshCuttingTimes();
-        } 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());
-        }
+    private void refreshOperatingData() throws IndegoAuthenticationException, IndegoException {
+        updateOperatingData(controller.getOperatingData());
+        updateStatus(ThingStatus.ONLINE);
     }
 
     private void refreshCuttingTimes() throws IndegoAuthenticationException, IndegoException {
@@ -244,6 +252,24 @@ public class BoschIndegoHandler extends BaseThingHandler {
         }
     }
 
+    private void refreshCuttingTimesAndMapWithExceptionHandling() {
+        try {
+            refreshCuttingTimes();
+            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());
+        }
+    }
+
+    private void refreshMap() throws IndegoAuthenticationException, IndegoException {
+        if (isLinked(GARDEN_MAP)) {
+            updateState(GARDEN_MAP, controller.getMap());
+        }
+    }
+
     private void updateState(DeviceStateResponse state) {
         DeviceStatus deviceStatus = DeviceStatus.fromCode(state.state);
         int status = getStatusFromCommand(deviceStatus.getAssociatedCommand());
@@ -260,6 +286,14 @@ public class BoschIndegoHandler extends BaseThingHandler {
         updateState(TEXTUAL_STATE, new StringType(deviceStatus.getMessage(translationProvider)));
     }
 
+    private void updateOperatingData(OperatingDataResponse operatingData) {
+        updateState(BATTERY_VOLTAGE, new QuantityType<>(operatingData.battery.voltage, Units.VOLT));
+        updateState(BATTERY_LEVEL, new DecimalType(operatingData.battery.percent));
+        updateState(LOW_BATTERY, OnOffType.from(operatingData.battery.percent < 20));
+        updateState(BATTERY_TEMPERATURE, new QuantityType<>(operatingData.battery.batteryTemperature, SIUnits.CELSIUS));
+        updateState(GARDEN_SIZE, new QuantityType<>(operatingData.garden.size, SIUnits.SQUARE_METRE));
+    }
+
     private boolean isReadyToMow(DeviceStatus deviceStatus, int error) {
         return deviceStatus.isReadyToMow() && error == 0;
     }
index 158fba4601d12c4fa38a951885f2172ee64c81e7..0cdf3ef844d4ec686411a6dd23d5d0659315e62a 100644 (file)
@@ -10,8 +10,8 @@ thing-type.boschindego.indego.description = Indego which supports the connect fe
 
 # thing types config
 
-thing-type.config.boschindego.indego.cuttingTimeRefresh.label = Cutting Time Refresh Interval
-thing-type.config.boschindego.indego.cuttingTimeRefresh.description = The number of minutes between refreshing last/next cutting time.
+thing-type.config.boschindego.indego.cuttingTimeMapRefresh.label = Cutting Time/Map 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.password.label = Password
 thing-type.config.boschindego.indego.password.description = Password for the Bosch Indego account.
 thing-type.config.boschindego.indego.refresh.label = Refresh Interval
@@ -21,8 +21,16 @@ thing-type.config.boschindego.indego.username.description = Username for the Bos
 
 # channel types
 
+channel-type.boschindego.batteryTemperature.label = Battery Temperature
+channel-type.boschindego.batteryTemperature.description = Battery temperature reported by the device
+channel-type.boschindego.batteryVoltage.label = Battery Voltage
+channel-type.boschindego.batteryVoltage.description = Battery voltage reported by the device
 channel-type.boschindego.errorcode.label = Error Code
 channel-type.boschindego.errorcode.description = 0 = no error
+channel-type.boschindego.gardenMap.label = Garden Map
+channel-type.boschindego.gardenMap.description = Garden map mapped by the device
+channel-type.boschindego.gardenSize.label = Garden Size
+channel-type.boschindego.gardenSize.description = Garden size mapped by the device
 channel-type.boschindego.lastCutting.label = Last Cutting
 channel-type.boschindego.lastCutting.description = Last cutting time
 channel-type.boschindego.mowed.label = Cut Grass
@@ -44,6 +52,7 @@ channel-type.boschindego.textualstate.label = Textual State
 # thing status descriptions
 
 offline.comm-error.authentication-failure = The login credentials are wrong or another client is connected to your Indego account
+offline.comm-error.unreachable = Device is unreachable
 offline.conf-error.missing-password = Password missing
 offline.conf-error.missing-username = Username missing
 
index 03d63cca45f1bb56172dd921242f8491b0ede7c6..7e108f0b4bb0e3133cb66cf78def6d7c76b42c64 100644 (file)
                        <channel id="ready" typeId="ready"/>
                        <channel id="lastCutting" typeId="lastCutting"/>
                        <channel id="nextCutting" typeId="nextCutting"/>
+                       <channel id="batteryVoltage" typeId="batteryVoltage"/>
+                       <channel id="batteryLevel" typeId="system.battery-level"/>
+                       <channel id="lowBattery" typeId="system.low-battery"/>
+                       <channel id="batteryTemperature" typeId="batteryTemperature"/>
+                       <channel id="gardenSize" typeId="gardenSize"/>
+                       <channel id="gardenMap" typeId="gardenMap"/>
                </channels>
                <config-description>
                        <parameter name="username" type="text" required="true">
@@ -32,9 +38,9 @@
                                <description>The number of seconds between refreshing device state.</description>
                                <default>180</default>
                        </parameter>
-                       <parameter name="cuttingTimeRefresh" type="integer" min="1">
-                               <label>Cutting Time Refresh Interval</label>
-                               <description>The number of minutes between refreshing last/next cutting time.</description>
+                       <parameter name="cuttingTimeMapRefresh" type="integer" min="1">
+                               <label>Cutting Time/Map Refresh Interval</label>
+                               <description>The number of minutes between refreshing last/next cutting time and map.</description>
                                <advanced>true</advanced>
                                <default>60</default>
                        </parameter>
                <category>Time</category>
                <state readOnly="true"/>
        </channel-type>
+       <channel-type id="batteryVoltage" advanced="true">
+               <item-type>Number:ElectricPotential</item-type>
+               <label>Battery Voltage</label>
+               <description>Battery voltage reported by the device</description>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="batteryTemperature" advanced="true">
+               <item-type>Number:Temperature</item-type>
+               <label>Battery Temperature</label>
+               <description>Battery temperature reported by the device</description>
+               <category>Temperature</category>
+               <tags>
+                       <tag>Measurement</tag>
+                       <tag>Temperature</tag>
+               </tags>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="gardenSize">
+               <item-type>Number:Area</item-type>
+               <label>Garden Size</label>
+               <description>Garden size mapped by the device</description>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="gardenMap">
+               <item-type>Image</item-type>
+               <label>Garden Map</label>
+               <description>Garden map mapped by the device</description>
+               <state readOnly="true"/>
+       </channel-type>
 
 </thing:thing-descriptions>