]> git.basschouten.com Git - openhab-addons.git/commitdiff
[boschshc] Support obtaining battery states (#13461) (#13631)
authorDavid Pace <dev@davidpace.de>
Fri, 11 Nov 2022 07:23:48 +0000 (08:23 +0100)
committerGitHub <noreply@github.com>
Fri, 11 Nov 2022 07:23:48 +0000 (08:23 +0100)
This change adds support for obtaining the battery state for the
following devices:

* Motion Detector
* Thermostat
* Twinguard
* Wall Thermostat
* Window/Door Contact

The following changes were made:

* Add system.battery-level and system.low-battery channels to Motion
Detector, Thermostat, Twinguard, Wall Thermostat and Window/Door Contact
* Add constant for battery-level and low-battery channels in
BoschSHCBindingConstants
* Implement abstract handler and service for battery-powered devices
* Let appropriate devices inherit the abstract implementation
* Add missing super calls in initializeServices() methods
* Rename existing getServiceURL() to getServiceStateURL() in HTTP client
* Add methods to retrieve service states without the suffix "/state" in
the URL
* Rename DeviceStatusUpdate to DeviceServiceData
* Let DeviceServiceData extend BoschSHCServiceState
* Extend DeviceServiceData DTO with model for faults
* Enhance bridge handler: handle updates without state sub-objects,
extract methods to enhance readability
* Add unit tests for all affected devices
* Minor code enhancements
* Update documentation

Signed-off-by: David Pace <dev@davidpace.de>
31 files changed:
bundles/org.openhab.binding.boschshc/README.md
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/AbstractBatteryPoweredDeviceHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/DeviceServiceData.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/DeviceStatusUpdate.java [deleted file]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Fault.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Faults.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResult.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/motiondetector/MotionDetectorHandler.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandler.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/TwinguardHandler.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/wallthermostat/WallThermostatHandler.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/windowcontact/WindowContactHandler.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/BoschSHCService.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/batterylevel/BatteryLevel.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/batterylevel/BatteryLevelService.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceState.java
bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBatteryPoweredDeviceHandlerTest.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractSHCHandlerTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/motiondetector/MotionDetectorHandlerTest.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandlerTest.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/twinguard/TwinguardHandlerTest.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/wallthermostat/WallThermostatHandlerTest.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/windowcontact/WindowContactHandlerTest.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/batterylevel/BatteryLevelTest.java [new file with mode: 0644]

index 979f54659e49664a4d892ab888b43872a9817674..f5ba3b26141c44a9a88484be8f88ccfb2f1c8536 100644 (file)
@@ -49,7 +49,7 @@ A compact smart plug with energy monitoring capabilities.
 | power-consumption  | Number:Power  | &#9744;  | Current power consumption (W) of the device.     |
 | energy-consumption | Number:Energy | &#9744;  | Cumulated energy consumption (Wh) of the device. |
 
-### Twinguard smoke detector
+### Twinguard Smoke Detector
 
 The Twinguard smoke detector warns you in case of fire and constantly monitors the air.
 
@@ -65,6 +65,8 @@ The Twinguard smoke detector warns you in case of fire and constantly monitors t
 | purity-rating      | String               | &#9744;  | Rating of current measured purity.                                                                |
 | air-description    | String               | &#9744;  | Overall description of the air quality.                                                           |
 | combined-rating    | String               | &#9744;  | Combined rating of the air quality.                                                               |
+| battery-level      | Number               | &#9744;  | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. |
+| low-battery        | Switch               | &#9744;  | Indicates whether the battery is low (`ON`) or OK (`OFF`). |
 
 ### Door/Window Contact
 
@@ -75,6 +77,8 @@ Detects open windows and doors.
 | Channel Type ID | Item Type | Writable | Description                  |
 | --------------- | --------- | :------: | ---------------------------- |
 | contact         | Contact   | &#9744;  | Contact state of the device. |
+| battery-level   | Number    | &#9744;  | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. |
+| low-battery     | Switch    | &#9744;  | Indicates whether the battery is low (`ON`) or OK (`OFF`). |
 
 ### Motion Detector
 
@@ -85,6 +89,8 @@ Detects every movement through an intelligent combination of passive infra-red t
 | Channel Type ID | Item Type | Writable | Description                    |
 | --------------- | --------- | :------: | ------------------------------ |
 | latest-motion   | DateTime  | &#9744;  | The date of the latest motion. |
+| battery-level   | Number    | &#9744;  | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. |
+| low-battery     | Switch    | &#9744;  | Indicates whether the battery is low (`ON`) or OK (`OFF`). |
 
 ### Shutter Control
 
@@ -107,6 +113,8 @@ Radiator thermostat
 | temperature           | Number:Temperature   | &#9744;  | Current measured temperature.                  |
 | valve-tappet-position | Number:Dimensionless | &#9744;  | Current open ratio of valve tappet (0 to 100). |
 | child-lock            | Switch               | &#9745;  | Indicates if child lock is active.             |
+| battery-level         | Number               | &#9744;  | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. |
+| low-battery           | Switch               | &#9744;  | Indicates whether the battery is low (`ON`) or OK (`OFF`). |
 
 ### Climate Control
 
@@ -129,6 +137,8 @@ Display of the current room temperature as well as the relative humidity in the
 | --------------- | -------------------- | :------: | ------------------------------------- |
 | temperature     | Number:Temperature   | &#9744;  | Current measured temperature.         |
 | humidity        | Number:Dimensionless | &#9744;  | Current measured humidity (0 to 100). |
+| battery-level   | Number               | &#9744;  | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. |
+| low-battery     | Switch               | &#9744;  | Indicates whether the battery is low (`ON`) or OK (`OFF`). |
 
 ### Security Camera 360
 
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/AbstractBatteryPoweredDeviceHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/AbstractBatteryPoweredDeviceHandler.java
new file mode 100644 (file)
index 0000000..4e87b10
--- /dev/null
@@ -0,0 +1,58 @@
+/**
+ * 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.boschshc.internal.devices;
+
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.openhab.binding.boschshc.internal.services.batterylevel.BatteryLevel;
+import org.openhab.binding.boschshc.internal.services.batterylevel.BatteryLevelService;
+import org.openhab.core.thing.Thing;
+
+/**
+ * Abstract implementation for battery-powered devices.
+ *
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class AbstractBatteryPoweredDeviceHandler extends BoschSHCDeviceHandler {
+
+    /**
+     * Service to monitor the battery level of the device
+     */
+    private final BatteryLevelService batteryLevelService;
+
+    public AbstractBatteryPoweredDeviceHandler(Thing thing) {
+        super(thing);
+        this.batteryLevelService = new BatteryLevelService();
+    }
+
+    @Override
+    protected void initializeServices() throws BoschSHCException {
+        super.initializeServices();
+
+        registerService(batteryLevelService, this::updateChannels, List.of(CHANNEL_BATTERY_LEVEL, CHANNEL_LOW_BATTERY),
+                true);
+    }
+
+    private void updateChannels(DeviceServiceData deviceServiceData) {
+        BatteryLevel batteryLevel = BatteryLevel.fromDeviceServiceData(deviceServiceData);
+        super.updateState(CHANNEL_BATTERY_LEVEL, batteryLevel.toState());
+        super.updateState(CHANNEL_LOW_BATTERY, batteryLevel.toLowBatteryState());
+    }
+}
index 7097deba80a60e6998aa0b976c60c03c56d2cf10..56f61ce5c108d2b00eb8d4297994f6cefce2904b 100644 (file)
@@ -74,6 +74,8 @@ public class BoschSHCBindingConstants {
     public static final String CHANNEL_ARM_ACTION = "arm-action";
     public static final String CHANNEL_DISARM_ACTION = "disarm-action";
     public static final String CHANNEL_MUTE_ACTION = "mute-action";
+    public static final String CHANNEL_BATTERY_LEVEL = "battery-level";
+    public static final String CHANNEL_LOW_BATTERY = "low-battery";
 
     // static device/service names
     public static final String SERVICE_INTRUSION_DETECTION = "intrusionDetectionSystem";
index 9e3d7983699878b088f40cb62c88ea2a7d26799e..a4c8e596685e5db0a54a4f19747e1590ae9f356a 100644 (file)
@@ -161,7 +161,7 @@ public abstract class BoschSHCHandler extends BaseThingHandler {
      * @param serviceName Name of service the update came from.
      * @param stateData Current state of device service. Serialized as JSON.
      */
-    public void processUpdate(String serviceName, JsonElement stateData) {
+    public void processUpdate(String serviceName, @Nullable JsonElement stateData) {
         // Check services of device to correctly
         for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) {
             BoschSHCService<? extends BoschSHCServiceState> service = deviceService.service;
index fd372f5f938d5bb38328e54b0f7ffffb1b15c666..7c214a7de285fda89de08ad65ccc4297e5a84994 100644 (file)
@@ -77,7 +77,7 @@ public class BoschHttpClient extends HttpClient {
     /**
      * Returns the pairing URL for the Bosch SHC clients, using port 8443.
      * See https://github.com/BoschSmartHome/bosch-shc-api-docs/blob/master/postman/README.md
-     * 
+     *
      * @return URL for pairing
      */
     public String getPairingUrl() {
@@ -86,7 +86,7 @@ public class BoschHttpClient extends HttpClient {
 
     /**
      * Returns a Bosch SHC URL for the endpoint, using port 8444.
-     * 
+     *
      * @param endpoint a endpoint, see https://apidocs.bosch-smarthome.com/local/index.html
      * @return Bosch SHC URL for passed endpoint
      */
@@ -96,7 +96,7 @@ public class BoschHttpClient extends HttpClient {
 
     /**
      * Returns a SmartHome URL for the endpoint - shortcut of {@link BoschSslUtil::getBoschShcUrl()}
-     * 
+     *
      * @param endpoint a endpoint, see https://apidocs.bosch-smarthome.com/local/index.html
      * @return SmartHome URL for passed endpoint
      */
@@ -105,17 +105,43 @@ public class BoschHttpClient extends HttpClient {
     }
 
     /**
-     * Returns a device & service URL.
+     * Returns a URL to get or put a service state.
+     * <p>
+     * Example:
+     *
+     * <pre>
+     * https://localhost:8444/smarthome/devices/hdm:ZigBee:000d6f0016d1cdae/services/AirQualityLevel/state
+     * </pre>
+     *
      * see https://apidocs.bosch-smarthome.com/local/index.html
-     * 
+     *
      * @param serviceName the name of the service
      * @param deviceId the device identifier
-     * @return SmartHome URL for passed endpoint
+     * @return a URL to get or put a service state
      */
-    public String getServiceUrl(String serviceName, String deviceId) {
+    public String getServiceStateUrl(String serviceName, String deviceId) {
         return this.getBoschSmartHomeUrl(String.format("devices/%s/services/%s/state", deviceId, serviceName));
     }
 
+    /**
+     * Returns a URL to get general information about a service.
+     * <p>
+     * Example:
+     *
+     * <pre>
+     * https://localhost:8444/smarthome/devices/hdm:ZigBee:000d6f0016d1cdae/services/BatteryLevel
+     * </pre>
+     *
+     * In some cases this URL has to be used to get the service state, for example for battery levels.
+     *
+     * @param serviceName the name of the service
+     * @param deviceId the device identifier
+     * @return a URL to retrieve general service information
+     */
+    public String getServiceUrl(String serviceName, String deviceId) {
+        return this.getBoschSmartHomeUrl(String.format("devices/%s/services/%s", deviceId, serviceName));
+    }
+
     /**
      * Checks if the Bosch SHC is online.
      *
@@ -177,7 +203,7 @@ public class BoschHttpClient extends HttpClient {
     /**
      * Pairs this client with the Bosch SHC.
      * Press pairing button on the Bosch Smart Home Controller!
-     * 
+     *
      * @return true if pairing was successful, otherwise false
      * @throws InterruptedException in case of an interrupt
      */
@@ -228,7 +254,7 @@ public class BoschHttpClient extends HttpClient {
 
     /**
      * Creates a HTTP request.
-     * 
+     *
      * @param url for the HTTP request
      * @param method for the HTTP request
      * @return created HTTP request instance
@@ -239,7 +265,7 @@ public class BoschHttpClient extends HttpClient {
 
     /**
      * Creates a HTTP request.
-     * 
+     *
      * @param url for the HTTP request
      * @param method for the HTTP request
      * @param content for the HTTP request
@@ -265,7 +291,7 @@ public class BoschHttpClient extends HttpClient {
 
     /**
      * Sends a request and expects a response of the specified type.
-     * 
+     *
      * @param request Request to send
      * @param responseContentClass Type of expected response
      * @param contentValidator Checks if the parsed response is valid
index a7a134ed049eb3a21b1d091540c40c4af6356531..a7f3e6bab91e06470640125c87a637425a11c938 100644 (file)
@@ -30,7 +30,7 @@ import org.eclipse.jetty.http.HttpStatus;
 import org.eclipse.jetty.util.ssl.SslContextFactory;
 import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
-import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceStatusUpdate;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room;
 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
@@ -51,6 +51,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.gson.Gson;
+import com.google.gson.JsonElement;
 import com.google.gson.reflect.TypeToken;
 
 /**
@@ -299,53 +300,97 @@ public class BridgeHandler extends BaseBridgeHandler {
     /**
      * Bridge callback handler for the results of long polls.
      *
-     * It will check the result and
-     * forward the received to the bosch thing handlers.
+     * It will check the results and
+     * forward the received states to the Bosch thing handlers.
      *
      * @param result Results from Long Polling
      */
     private void handleLongPollResult(LongPollResult result) {
-        for (DeviceStatusUpdate update : result.result) {
-            if (update != null && update.state != null) {
-                logger.debug("Got update for service {} of type {}: {}", update.id, update.type, update.state);
+        for (DeviceServiceData deviceServiceData : result.result) {
+            handleDeviceServiceData(deviceServiceData);
+        }
+    }
 
-                var updateDeviceId = update.deviceId;
-                if (updateDeviceId == null) {
-                    continue;
-                }
+    /**
+     * Processes a single long poll result.
+     *
+     * @param deviceServiceData object representing a single long poll result
+     */
+    private void handleDeviceServiceData(@Nullable DeviceServiceData deviceServiceData) {
+        if (deviceServiceData != null) {
+            JsonElement state = obtainState(deviceServiceData);
 
-                logger.debug("Got update for device {}", updateDeviceId);
+            logger.debug("Got update for service {} of type {}: {}", deviceServiceData.id, deviceServiceData.type,
+                    state);
 
-                boolean handled = false;
+            var updateDeviceId = deviceServiceData.deviceId;
+            if (updateDeviceId == null || state == null) {
+                return;
+            }
 
-                Bridge bridge = this.getThing();
-                for (Thing childThing : bridge.getThings()) {
-                    // All children of this should implement BoschSHCHandler
-                    @Nullable
-                    ThingHandler baseHandler = childThing.getHandler();
-                    if (baseHandler != null && baseHandler instanceof BoschSHCHandler) {
-                        BoschSHCHandler handler = (BoschSHCHandler) baseHandler;
-                        @Nullable
-                        String deviceId = handler.getBoschID();
+            logger.debug("Got update for device {}", updateDeviceId);
 
-                        handled = true;
-                        logger.debug("Registered device: {} - looking for {}", deviceId, updateDeviceId);
+            forwardStateToHandlers(deviceServiceData, state, updateDeviceId);
+        }
+    }
 
-                        if (deviceId != null && updateDeviceId.equals(deviceId)) {
-                            logger.debug("Found child: {} - calling processUpdate (id: {}) with {}", handler, update.id,
-                                    update.state);
-                            handler.processUpdate(update.id, update.state);
-                        }
-                    } else {
-                        logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler);
-                    }
-                }
+    /**
+     * Extracts the actual state object from the given {@link DeviceServiceData} instance.
+     * <p>
+     * In some special cases like the <code>BatteryLevel</code> service the {@link DeviceServiceData} object itself
+     * contains the state.
+     * In all other cases, the state is contained in a sub-object named <code>state</code>.
+     *
+     * @param deviceServiceData the {@link DeviceServiceData} object from which the state should be obtained
+     * @return the state sub-object or the {@link DeviceServiceData} object itself
+     */
+    @Nullable
+    private JsonElement obtainState(DeviceServiceData deviceServiceData) {
+        // the battery level service receives no individual state object but rather requires the DeviceServiceData
+        // structure
+        if ("BatteryLevel".equals(deviceServiceData.id)) {
+            return gson.toJsonTree(deviceServiceData);
+        }
+
+        return deviceServiceData.state;
+    }
 
-                if (!handled) {
-                    logger.debug("Could not find a thing for device ID: {}", updateDeviceId);
+    /**
+     * Tries to find handlers for the device with the given ID and forwards the received state to the handlers.
+     *
+     * @param deviceServiceData object representing updates received in long poll results
+     * @param state the received state object as JSON element
+     * @param updateDeviceId the ID of the device for which the state update was received
+     */
+    private void forwardStateToHandlers(DeviceServiceData deviceServiceData, JsonElement state, String updateDeviceId) {
+        boolean handled = false;
+
+        Bridge bridge = this.getThing();
+        for (Thing childThing : bridge.getThings()) {
+            // All children of this should implement BoschSHCHandler
+            @Nullable
+            ThingHandler baseHandler = childThing.getHandler();
+            if (baseHandler != null && baseHandler instanceof BoschSHCHandler) {
+                BoschSHCHandler handler = (BoschSHCHandler) baseHandler;
+                @Nullable
+                String deviceId = handler.getBoschID();
+
+                handled = true;
+                logger.debug("Registered device: {} - looking for {}", deviceId, updateDeviceId);
+
+                if (deviceId != null && updateDeviceId.equals(deviceId)) {
+                    logger.debug("Found child: {} - calling processUpdate (id: {}) with {}", handler,
+                            deviceServiceData.id, state);
+                    handler.processUpdate(deviceServiceData.id, state);
                 }
+            } else {
+                logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler);
             }
         }
+
+        if (!handled) {
+            logger.debug("Could not find a thing for device ID: {}", updateDeviceId);
+        }
     }
 
     /**
@@ -447,7 +492,7 @@ public class BridgeHandler extends BaseBridgeHandler {
      * Query the Bosch Smart Home Controller for the state of the given device.
      * <p>
      * The URL used for retrieving the state has the following structure:
-     * 
+     *
      * <pre>
      * https://{IP}:8444/smarthome/devices/{deviceId}/services/{serviceName}/state
      * </pre>
@@ -470,14 +515,14 @@ public class BridgeHandler extends BaseBridgeHandler {
             return null;
         }
 
-        String url = httpClient.getServiceUrl(stateName, deviceId);
+        String url = httpClient.getServiceStateUrl(stateName, deviceId);
         logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", stateName, deviceId, url);
         return getState(httpClient, url, stateClass);
     }
 
     /**
      * Queries the Bosch Smart Home Controller for the state using an explicit endpoint.
-     * 
+     *
      * @param <T> Type to which the resulting JSON should be deserialized to
      * @param endpoint The destination endpoint part of the URL
      * @param stateClass Class to convert the resulting JSON to
@@ -503,7 +548,7 @@ public class BridgeHandler extends BaseBridgeHandler {
 
     /**
      * Sends a HTTP GET request in order to retrieve a state from the Bosch Smart Home Controller.
-     * 
+     *
      * @param <T> Type to which the resulting JSON should be deserialized to
      * @param httpClient HTTP client used for sending the request
      * @param url URL at which the state should be retrieved
@@ -566,7 +611,7 @@ public class BridgeHandler extends BaseBridgeHandler {
         }
 
         // Create request
-        String url = httpClient.getServiceUrl(serviceName, deviceId);
+        String url = httpClient.getServiceStateUrl(serviceName, deviceId);
         Request request = httpClient.createRequest(url, PUT, state);
 
         // Send request
@@ -575,7 +620,7 @@ public class BridgeHandler extends BaseBridgeHandler {
 
     /**
      * Sends a HTTP POST request without a request body to the given endpoint.
-     * 
+     *
      * @param endpoint The destination endpoint part of the URL
      * @return the HTTP response
      * @throws InterruptedException
@@ -589,7 +634,7 @@ public class BridgeHandler extends BaseBridgeHandler {
 
     /**
      * Sends a HTTP POST request with a request body to the given endpoint.
-     * 
+     *
      * @param <T> Type of the request
      * @param endpoint The destination endpoint part of the URL
      * @param requestBody object representing the request body to be sent, may be <code>null</code>
@@ -611,4 +656,18 @@ public class BridgeHandler extends BaseBridgeHandler {
         Request request = httpClient.createRequest(url, POST, requestBody);
         return request.send();
     }
+
+    public @Nullable DeviceServiceData getServiceData(String deviceId, String serviceName)
+            throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+        @Nullable
+        BoschHttpClient httpClient = this.httpClient;
+        if (httpClient == null) {
+            logger.warn("HttpClient not initialized");
+            return null;
+        }
+
+        String url = httpClient.getServiceUrl(serviceName, deviceId);
+        logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", serviceName, deviceId, url);
+        return getState(httpClient, url, DeviceServiceData.class);
+    }
 }
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/DeviceServiceData.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/DeviceServiceData.java
new file mode 100644 (file)
index 0000000..82f5d49
--- /dev/null
@@ -0,0 +1,64 @@
+/**
+ * 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.boschshc.internal.devices.bridge.dto;
+
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
+
+import com.google.gson.JsonElement;
+
+/**
+ * Represents a device status update as represented by the Smart Home
+ * Controller.
+ *
+ * @author Stefan Kästle - Initial contribution
+ * @author Christian Oeing - refactorings of e.g. server registration
+ */
+public class DeviceServiceData extends BoschSHCServiceState {
+
+    /**
+     * Url path of the service the update came from.
+     */
+    public String path;
+
+    /**
+     * Name of service the update came from.
+     */
+    public String id;
+
+    /**
+     * Current state of device. Serialized as JSON.
+     */
+    public @Nullable JsonElement state;
+
+    /**
+     * Id of device the update is for.
+     */
+    public @Nullable String deviceId;
+
+    /**
+     * An optional object containing information about device faults.
+     * <p>
+     * Example: low battery warnings are stored as faults with category <code>WARNING</code>
+     */
+    public @Nullable Faults faults;
+
+    public DeviceServiceData() {
+        super("DeviceServiceData");
+    }
+
+    @Override
+    public String toString() {
+        return this.deviceId + " state: " + this.type;
+    }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/DeviceStatusUpdate.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/DeviceStatusUpdate.java
deleted file mode 100644 (file)
index 0da4e24..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * 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.boschshc.internal.devices.bridge.dto;
-
-import org.eclipse.jdt.annotation.Nullable;
-
-import com.google.gson.JsonElement;
-import com.google.gson.annotations.SerializedName;
-
-/**
- * Represents a device status update as represented by the Smart Home
- * Controller.
- *
- * @author Stefan Kästle - Initial contribution
- * @author Christian Oeing - refactorings of e.g. server registration
- */
-public class DeviceStatusUpdate {
-    /**
-     * Url path of the service the update came from.
-     */
-    public String path;
-
-    /**
-     * The type of message.
-     */
-    @SerializedName("@type")
-    public String type;
-
-    /**
-     * Name of service the update came from.
-     */
-    public String id;
-
-    /**
-     * Current state of device. Serialized as JSON.
-     */
-    public JsonElement state;
-
-    /**
-     * Id of device the update is for.
-     */
-    public @Nullable String deviceId;
-
-    @Override
-    public String toString() {
-        return this.deviceId + "state: " + this.type;
-    }
-}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Fault.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Fault.java
new file mode 100644 (file)
index 0000000..070c8f8
--- /dev/null
@@ -0,0 +1,33 @@
+/**
+ * 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.boschshc.internal.devices.bridge.dto;
+
+/**
+ * A fault entry containing a category and a type.
+ * <p>
+ * Example JSON:
+ *
+ * <pre>
+ * {
+ *   "type":"LOW_BATTERY",
+ *   "category":"WARNING"
+ * }
+ * </pre>
+ *
+ * @author David Pace - Initial contribution
+ *
+ */
+public class Fault {
+    public String type;
+    public String category;
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Faults.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Faults.java
new file mode 100644 (file)
index 0000000..3e3df0d
--- /dev/null
@@ -0,0 +1,39 @@
+/**
+ * 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.boschshc.internal.devices.bridge.dto;
+
+import java.util.List;
+
+/**
+ * A container object for faults that can be contained in {@link DeviceServiceData}.
+ * <p>
+ * Example JSON:
+ *
+ * <pre>
+ * "faults": {
+ *   "entries": [
+ *     {
+ *       "type":"LOW_BATTERY",
+ *       "category":"WARNING"
+ *     }
+ *   ]
+   }
+ * </pre>
+ *
+ * @author David Pace - Initial contribution
+ *
+ */
+public class Faults {
+
+    public List<Fault> entries;
+}
index ec56a13ca5df93f66117a6c43992dd8ff7df54e7..ea830b35fa3ba5e1c97c6a5274fa3d8216109cf4 100644 (file)
@@ -35,6 +35,6 @@ public class LongPollResult {
      * ],"jsonrpc":"2.0"}
      */
 
-    public ArrayList<DeviceStatusUpdate> result;
+    public ArrayList<DeviceServiceData> result;
     public String jsonrpc;
 }
index f4a088ceca3644d0002c6e855fc1fadc6d4cf2ab..64b16712efe850e4200730ba67d649587b0da62b 100644 (file)
@@ -17,7 +17,7 @@ import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConst
 import java.util.List;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.boschshc.internal.devices.BoschSHCDeviceHandler;
+import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandler;
 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
 import org.openhab.binding.boschshc.internal.services.latestmotion.LatestMotionService;
 import org.openhab.binding.boschshc.internal.services.latestmotion.dto.LatestMotionServiceState;
@@ -32,7 +32,7 @@ import org.openhab.core.thing.Thing;
  * @author Christian Oeing - Use service instead of custom logic
  */
 @NonNullByDefault
-public class MotionDetectorHandler extends BoschSHCDeviceHandler {
+public class MotionDetectorHandler extends AbstractBatteryPoweredDeviceHandler {
 
     public MotionDetectorHandler(Thing thing) {
         super(thing);
index ff00373bea3ac45839e38199237287ff21d8f8e3..b0f956a6517d2cb5e7cfb0ea0f88ed5680ebcafb 100644 (file)
  */
 package org.openhab.binding.boschshc.internal.devices.thermostat;
 
-import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_CHILD_LOCK;
-import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_TEMPERATURE;
-import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_VALVE_TAPPET_POSITION;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*;
 
 import java.util.List;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.boschshc.internal.devices.BoschSHCDeviceHandler;
+import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandler;
 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
 import org.openhab.binding.boschshc.internal.services.childlock.ChildLockService;
 import org.openhab.binding.boschshc.internal.services.childlock.dto.ChildLockServiceState;
@@ -33,11 +31,11 @@ import org.openhab.core.types.Command;
 
 /**
  * Handler for a thermostat device.
- * 
+ *
  * @author Christian Oeing - Initial contribution
  */
 @NonNullByDefault
-public final class ThermostatHandler extends BoschSHCDeviceHandler {
+public final class ThermostatHandler extends AbstractBatteryPoweredDeviceHandler {
 
     private ChildLockService childLockService;
 
@@ -48,6 +46,8 @@ public final class ThermostatHandler extends BoschSHCDeviceHandler {
 
     @Override
     protected void initializeServices() throws BoschSHCException {
+        super.initializeServices();
+
         this.createService(TemperatureLevelService::new, this::updateChannels, List.of(CHANNEL_TEMPERATURE));
         this.createService(ValveTappetService::new, this::updateChannels, List.of(CHANNEL_VALVE_TAPPET_POSITION));
         this.registerService(this.childLockService, this::updateChannels, List.of(CHANNEL_CHILD_LOCK));
@@ -67,7 +67,7 @@ public final class ThermostatHandler extends BoschSHCDeviceHandler {
     /**
      * Updates the channels which are linked to the {@link TemperatureLevelService}
      * of the device.
-     * 
+     *
      * @param state Current state of {@link TemperatureLevelService}.
      */
     private void updateChannels(TemperatureLevelServiceState state) {
@@ -77,7 +77,7 @@ public final class ThermostatHandler extends BoschSHCDeviceHandler {
     /**
      * Updates the channels which are linked to the {@link ValveTappetService} of
      * the device.
-     * 
+     *
      * @param state Current state of {@link ValveTappetService}.
      */
     private void updateChannels(ValveTappetServiceState state) {
@@ -87,7 +87,7 @@ public final class ThermostatHandler extends BoschSHCDeviceHandler {
     /**
      * Updates the channels which are linked to the {@link ChildLockService} of the
      * device.
-     * 
+     *
      * @param state Current state of {@link ChildLockService}.
      */
     private void updateChannels(ChildLockServiceState state) {
index d155c18965fb829d6e5963eb59e02bff051db78b..a4b0f3ac8244d7bb3422943c279d6487cfa7fb16 100644 (file)
  */
 package org.openhab.binding.boschshc.internal.devices.twinguard;
 
-import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_AIR_DESCRIPTION;
-import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_COMBINED_RATING;
-import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_HUMIDITY;
-import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_HUMIDITY_RATING;
-import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_PURITY;
-import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_PURITY_RATING;
-import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_TEMPERATURE;
-import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_TEMPERATURE_RATING;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*;
 
 import java.util.List;
 
@@ -27,7 +20,7 @@ import javax.measure.quantity.Dimensionless;
 import javax.measure.quantity.Temperature;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.boschshc.internal.devices.BoschSHCDeviceHandler;
+import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandler;
 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
 import org.openhab.binding.boschshc.internal.services.airqualitylevel.AirQualityLevelService;
 import org.openhab.binding.boschshc.internal.services.airqualitylevel.dto.AirQualityLevelServiceState;
@@ -44,7 +37,7 @@ import org.openhab.core.thing.Thing;
  * @author Christian Oeing - Use service instead of custom logic
  */
 @NonNullByDefault
-public class TwinguardHandler extends BoschSHCDeviceHandler {
+public class TwinguardHandler extends AbstractBatteryPoweredDeviceHandler {
 
     public TwinguardHandler(Thing thing) {
         super(thing);
index 411c52ebb7a5286b9810048f3ef8a7cc94928e44..96de7355bdf308de261c007be6087e2ba604bcef 100644 (file)
  */
 package org.openhab.binding.boschshc.internal.devices.wallthermostat;
 
-import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_HUMIDITY;
-import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_TEMPERATURE;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*;
 
 import java.util.List;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.boschshc.internal.devices.BoschSHCDeviceHandler;
+import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandler;
 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
 import org.openhab.binding.boschshc.internal.services.humiditylevel.HumidityLevelService;
 import org.openhab.binding.boschshc.internal.services.humiditylevel.dto.HumidityLevelServiceState;
@@ -28,11 +27,11 @@ import org.openhab.core.thing.Thing;
 
 /**
  * Handler for a wall thermostat device.
- * 
+ *
  * @author Christian Oeing - Initial contribution
  */
 @NonNullByDefault
-public final class WallThermostatHandler extends BoschSHCDeviceHandler {
+public final class WallThermostatHandler extends AbstractBatteryPoweredDeviceHandler {
 
     public WallThermostatHandler(Thing thing) {
         super(thing);
@@ -40,13 +39,15 @@ public final class WallThermostatHandler extends BoschSHCDeviceHandler {
 
     @Override
     protected void initializeServices() throws BoschSHCException {
+        super.initializeServices();
+
         this.createService(TemperatureLevelService::new, this::updateChannels, List.of(CHANNEL_TEMPERATURE));
         this.createService(HumidityLevelService::new, this::updateChannels, List.of(CHANNEL_HUMIDITY));
     }
 
     /**
      * Updates the channels which are linked to the {@link TemperatureLevelService} of the device.
-     * 
+     *
      * @param state Current state of {@link TemperatureLevelService}.
      */
     private void updateChannels(TemperatureLevelServiceState state) {
@@ -55,7 +56,7 @@ public final class WallThermostatHandler extends BoschSHCDeviceHandler {
 
     /**
      * Updates the channels which are linked to the {@link HumidityLevelService} of the device.
-     * 
+     *
      * @param state Current state of {@link HumidityLevelService}.
      */
     private void updateChannels(HumidityLevelServiceState state) {
index 08a34099cae7803a55a670f1e2ab2d16d289fe43..1e79dea1ee7424864b4b72c4a52850219f9b838d 100644 (file)
@@ -17,7 +17,7 @@ import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConst
 import java.util.List;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.boschshc.internal.devices.BoschSHCDeviceHandler;
+import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandler;
 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
 import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactService;
 import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactState;
@@ -32,7 +32,7 @@ import org.openhab.core.types.State;
  * @author Stefan Kästle - Initial contribution
  */
 @NonNullByDefault
-public class WindowContactHandler extends BoschSHCDeviceHandler {
+public class WindowContactHandler extends AbstractBatteryPoweredDeviceHandler {
 
     public WindowContactHandler(Thing thing) {
         super(thing);
@@ -40,6 +40,8 @@ public class WindowContactHandler extends BoschSHCDeviceHandler {
 
     @Override
     protected void initializeServices() throws BoschSHCException {
+        super.initializeServices();
+
         this.createService(ShutterContactService::new, this::updateChannels, List.of(CHANNEL_CONTACT));
     }
 
index d3a0db77dfbf2bb676662dc89bee3e4ffff67941..f0b7bf76827a87eb65cb89e4f603c0475bdb98b9 100644 (file)
@@ -32,17 +32,17 @@ import com.google.gson.JsonElement;
  * same endpoint.
  * <p>
  * The endpoints of this service have the following URL structure:
- * 
+ *
  * <pre>
  * https://{IP}:8444/smarthome/devices/{deviceId}/services/{serviceName}/state
  * </pre>
- * 
+ *
  * The HTTP client of the bridge will use <code>GET</code> requests to retrieve the state and <code>PUT</code> requests
  * to set the state.
  * <p>
  * The services of the devices and their official APIs can be found
  * <a href="https://apidocs.bosch-smarthome.com/local/">here</a>.
- * 
+ *
  * @author Christian Oeing - Initial contribution
  * @author David Pace - Service abstraction
  */
@@ -63,7 +63,7 @@ public abstract class BoschSHCService<TState extends BoschSHCServiceState> exten
 
     /**
      * Constructor
-     * 
+     *
      * @param serviceName Unique name of the service.
      * @param stateClass State class that this service uses for data transfers
      *            from/to the device.
@@ -75,7 +75,7 @@ public abstract class BoschSHCService<TState extends BoschSHCServiceState> exten
 
     /**
      * Initializes the service
-     * 
+     *
      * @param bridgeHandler Bridge to use for communication from/to the device
      * @param deviceId Id of device this service is for
      * @param stateUpdateListener Function to call when a state update was received
@@ -89,7 +89,7 @@ public abstract class BoschSHCService<TState extends BoschSHCServiceState> exten
 
     /**
      * Returns the class of the state this service provides.
-     * 
+     *
      * @return Class of the state this service provides.
      */
     public Class<TState> getStateClass() {
@@ -98,7 +98,7 @@ public abstract class BoschSHCService<TState extends BoschSHCServiceState> exten
 
     /**
      * Requests the current state of the service and updates it.
-     * 
+     *
      * @throws ExecutionException
      * @throws TimeoutException
      * @throws InterruptedException
@@ -114,7 +114,7 @@ public abstract class BoschSHCService<TState extends BoschSHCServiceState> exten
 
     /**
      * Requests the current state of the device with the specified id.
-     * 
+     *
      * @return Current state of the device.
      * @throws ExecutionException
      * @throws TimeoutException
@@ -136,7 +136,7 @@ public abstract class BoschSHCService<TState extends BoschSHCServiceState> exten
 
     /**
      * Sets the state of the device with the specified id.
-     * 
+     *
      * @param state State to set.
      * @throws InterruptedException
      * @throws ExecutionException
@@ -156,10 +156,10 @@ public abstract class BoschSHCService<TState extends BoschSHCServiceState> exten
 
     /**
      * A state update was received from the bridge
-     * 
+     *
      * @param stateData Current state of service. Serialized as JSON.
      */
-    public void onStateUpdate(JsonElement stateData) {
+    public void onStateUpdate(@Nullable JsonElement stateData) {
         @Nullable
         TState state = BoschSHCServiceState.fromJson(stateData, this.stateClass);
         if (state == null) {
@@ -171,7 +171,7 @@ public abstract class BoschSHCService<TState extends BoschSHCServiceState> exten
 
     /**
      * A state update was received from the bridge.
-     * 
+     *
      * @param state Current state of service as an instance of the state class.
      */
     private void onStateUpdate(TState state) {
@@ -184,7 +184,7 @@ public abstract class BoschSHCService<TState extends BoschSHCServiceState> exten
     /**
      * Allows a service to handle a command and create a new state out of it.
      * The new state still has to be set via setState.
-     * 
+     *
      * @param command Command to handle
      * @throws BoschSHCException If service can not handle command
      */
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/batterylevel/BatteryLevel.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/batterylevel/BatteryLevel.java
new file mode 100644 (file)
index 0000000..de58032
--- /dev/null
@@ -0,0 +1,119 @@
+/**
+ * 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.boschshc.internal.services.batterylevel;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Fault;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Faults;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * Possible battery levels.
+ *
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+public enum BatteryLevel {
+    OK,
+    LOW_BATTERY,
+    CRITICAL_LOW,
+    CRITICALLY_LOW_BATTERY,
+    NOT_AVAILABLE;
+
+    /**
+     * Derives a battery level by analyzing the fault elements in the given device service data.
+     * <p>
+     * Note that no fault elements are present when the battery level is OK.
+     *
+     * @param deviceServiceData a device service data model
+     * @return the derived battery level
+     */
+    public static BatteryLevel fromDeviceServiceData(DeviceServiceData deviceServiceData) {
+        Faults faults = deviceServiceData.faults;
+        if (faults == null || faults.entries == null || faults.entries.isEmpty()) {
+            return OK;
+        }
+
+        for (Fault faultEntry : faults.entries) {
+            if ("warning".equalsIgnoreCase(faultEntry.category)) {
+                BatteryLevel batteryLevelState = BatteryLevel.get(faultEntry.type);
+                if (batteryLevelState != null) {
+                    return batteryLevelState;
+                }
+            }
+        }
+
+        return OK;
+    }
+
+    /**
+     * Returns the corresponding battery level for the given string or <code>null</code> if no state matches.
+     *
+     * @param identifier the battery level identifier
+     *
+     * @return the matching battery level or <code>null</code>
+     */
+    public static @Nullable BatteryLevel get(String identifier) {
+        for (BatteryLevel batteryLevelState : values()) {
+            if (batteryLevelState.toString().equalsIgnoreCase(identifier)) {
+                return batteryLevelState;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Transforms a Bosch-specific battery level to a percentage for the <code>system.battery-level</code> channel.
+     *
+     * @return a percentage between 0 and 100 as integer
+     */
+    public State toState() {
+        switch (this) {
+            case LOW_BATTERY:
+                return new DecimalType(10);
+            case CRITICAL_LOW:
+            case CRITICALLY_LOW_BATTERY:
+                return new DecimalType(1);
+            case NOT_AVAILABLE:
+                return UnDefType.UNDEF;
+            default:
+                return new DecimalType(100);
+        }
+    }
+
+    /**
+     * Transforms a Bosch-specific battery level to an <code>ON</code>/<code>OFF</code> state for the
+     * <code>system.low-battery</code> channel.
+     * <p>
+     * If the result is <code>ON</code>, the battery is low; if the result is <code>OFF</code> the battery level is OK.
+     *
+     * @return
+     */
+    public OnOffType toLowBatteryState() {
+        switch (this) {
+            case LOW_BATTERY:
+            case CRITICAL_LOW:
+            case CRITICALLY_LOW_BATTERY:
+                return OnOffType.ON;
+            default:
+                return OnOffType.OFF;
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/batterylevel/BatteryLevelService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/batterylevel/BatteryLevelService.java
new file mode 100644 (file)
index 0000000..a4a6b4d
--- /dev/null
@@ -0,0 +1,53 @@
+/**
+ * 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.boschshc.internal.services.batterylevel;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.boschshc.internal.devices.bridge.BridgeHandler;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.openhab.binding.boschshc.internal.services.BoschSHCService;
+
+/**
+ * Service to retrieve battery levels.
+ *
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class BatteryLevelService extends BoschSHCService<DeviceServiceData> {
+
+    public BatteryLevelService() {
+        super("BatteryLevel", DeviceServiceData.class);
+    }
+
+    @Override
+    public @Nullable DeviceServiceData getState()
+            throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+
+        String deviceId = getDeviceId();
+        if (deviceId == null) {
+            return null;
+        }
+
+        BridgeHandler bridgeHandler = getBridgeHandler();
+        if (bridgeHandler == null) {
+            return null;
+        }
+        return bridgeHandler.getServiceData(deviceId, getServiceName());
+    }
+}
index 81d37ae924fc0b26e6e939ebd60c22807113e86f..190b7ceb7e7f2b2b07b196ffd6dbfedd34a443e5 100644 (file)
@@ -22,7 +22,7 @@ import com.google.gson.annotations.SerializedName;
 
 /**
  * Base Bosch Smart Home Controller service state.
- * 
+ *
  * @author Christian Oeing - Initial contribution
  */
 public class BoschSHCServiceState {
@@ -40,7 +40,7 @@ public class BoschSHCServiceState {
     private @Nullable String stateType = null;
 
     @SerializedName("@type")
-    private final String type;
+    public final String type;
 
     protected BoschSHCServiceState(String type) {
         this.type = type;
index 12c1dda89eacb36b142fe494f19149e47c1c7d76..5f3095f76da1e42eb1b87be605d97341cd4ba989 100644 (file)
@@ -53,7 +53,7 @@
                        <bridge-type-ref id="shc"/>
                </supported-bridge-type-refs>
 
-               <label>TwinGuard</label>
+               <label>Twinguard</label>
                <description>The Twinguard smoke detector warns you in case of fire and constantly monitors the air.</description>
 
                <channels>
@@ -65,6 +65,8 @@
                        <channel id="air-description" typeId="air-description"/>
                        <channel id="purity-rating" typeId="purity-rating"/>
                        <channel id="combined-rating" typeId="combined-rating"/>
+                       <channel id="battery-level" typeId="system.battery-level"/>
+                       <channel id="low-battery" typeId="system.low-battery"/>
                </channels>
 
                <config-description-ref uri="thing-type:boschshc:device"/>
@@ -81,6 +83,8 @@
 
                <channels>
                        <channel id="contact" typeId="contact"/>
+                       <channel id="battery-level" typeId="system.battery-level"/>
+                       <channel id="low-battery" typeId="system.low-battery"/>
                </channels>
 
                <config-description-ref uri="thing-type:boschshc:device"/>
 
                <channels>
                        <channel id="latest-motion" typeId="latest-motion"/>
+                       <channel id="battery-level" typeId="system.battery-level"/>
+                       <channel id="low-battery" typeId="system.low-battery"/>
                </channels>
 
                <config-description-ref uri="thing-type:boschshc:device"/>
                        <channel id="temperature" typeId="temperature"/>
                        <channel id="valve-tappet-position" typeId="valve-tappet-position"/>
                        <channel id="child-lock" typeId="child-lock"/>
+                       <channel id="battery-level" typeId="system.battery-level"/>
+                       <channel id="low-battery" typeId="system.low-battery"/>
                </channels>
 
                <config-description-ref uri="thing-type:boschshc:device"/>
                <channels>
                        <channel id="temperature" typeId="temperature"/>
                        <channel id="humidity" typeId="humidity"/>
+                       <channel id="battery-level" typeId="system.battery-level"/>
+                       <channel id="low-battery" typeId="system.low-battery"/>
                </channels>
 
                <config-description-ref uri="thing-type:boschshc:device"/>
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBatteryPoweredDeviceHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBatteryPoweredDeviceHandlerTest.java
new file mode 100644 (file)
index 0000000..6f6707e
--- /dev/null
@@ -0,0 +1,113 @@
+/**
+ * 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.boschshc.internal.devices;
+
+import static org.mockito.Mockito.verify;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.UnDefType;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+
+/**
+ * Abstract test implementation for battery-powered devices.
+ *
+ * @author David Pace - Initial contribution
+ *
+ * @param <T> type of the battery-powered device to be tested
+ */
+@NonNullByDefault
+public abstract class AbstractBatteryPoweredDeviceHandlerTest<T extends AbstractBatteryPoweredDeviceHandler>
+        extends AbstractBoschSHCDeviceHandlerTest<T> {
+
+    @Test
+    public void testProcessUpdate_BatteryLevel_LowBattery() {
+        JsonElement deviceServiceData = JsonParser.parseString("{ \n" + "    \"@type\":\"DeviceServiceData\",\n"
+                + "    \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n"
+                + "    \"id\":\"BatteryLevel\",\n" + "    \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\",\n"
+                + "    \"faults\":{ \n" + "        \"entries\":[\n" + "          {\n"
+                + "            \"type\":\"LOW_BATTERY\",\n" + "            \"category\":\"WARNING\"\n" + "          }\n"
+                + "        ]\n" + "    }\n" + "}");
+        getFixture().processUpdate("BatteryLevel", deviceServiceData);
+        verify(getCallback()).stateUpdated(
+                new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL),
+                new DecimalType(10));
+        verify(getCallback()).stateUpdated(
+                new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.ON);
+    }
+
+    @Test
+    public void testProcessUpdate_BatteryLevel_CriticalLow() {
+        JsonElement deviceServiceData = JsonParser.parseString("{ \n" + "    \"@type\":\"DeviceServiceData\",\n"
+                + "    \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n"
+                + "    \"id\":\"BatteryLevel\",\n" + "    \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\",\n"
+                + "    \"faults\":{ \n" + "        \"entries\":[\n" + "          {\n"
+                + "            \"type\":\"CRITICAL_LOW\",\n" + "            \"category\":\"WARNING\"\n"
+                + "          }\n" + "        ]\n" + "    }\n" + "}");
+        getFixture().processUpdate("BatteryLevel", deviceServiceData);
+        verify(getCallback()).stateUpdated(
+                new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL),
+                new DecimalType(1));
+        verify(getCallback()).stateUpdated(
+                new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.ON);
+    }
+
+    @Test
+    public void testProcessUpdate_BatteryLevel_CriticallyLowBattery() {
+        JsonElement deviceServiceData = JsonParser.parseString("{ \n" + "    \"@type\":\"DeviceServiceData\",\n"
+                + "    \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n"
+                + "    \"id\":\"BatteryLevel\",\n" + "    \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\",\n"
+                + "    \"faults\":{ \n" + "        \"entries\":[\n" + "          {\n"
+                + "            \"type\":\"CRITICALLY_LOW_BATTERY\",\n" + "            \"category\":\"WARNING\"\n"
+                + "          }\n" + "        ]\n" + "    }\n" + "}");
+        getFixture().processUpdate("BatteryLevel", deviceServiceData);
+        verify(getCallback()).stateUpdated(
+                new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL),
+                new DecimalType(1));
+        verify(getCallback()).stateUpdated(
+                new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.ON);
+    }
+
+    @Test
+    public void testProcessUpdate_BatteryLevel_OK() {
+        JsonElement deviceServiceData = JsonParser.parseString("{ \n" + "    \"@type\":\"DeviceServiceData\",\n"
+                + "    \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n"
+                + "    \"id\":\"BatteryLevel\",\n" + "    \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\" }");
+        getFixture().processUpdate("BatteryLevel", deviceServiceData);
+        verify(getCallback()).stateUpdated(
+                new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL),
+                new DecimalType(100));
+        verify(getCallback()).stateUpdated(
+                new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.OFF);
+    }
+
+    @Test
+    public void testProcessUpdate_BatteryLevel_NotAvailable() {
+        JsonElement deviceServiceData = JsonParser.parseString("{ \n" + "    \"@type\":\"DeviceServiceData\",\n"
+                + "    \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n"
+                + "    \"id\":\"BatteryLevel\",\n" + "    \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\",\n"
+                + "    \"faults\":{ \n" + "        \"entries\":[\n" + "          {\n"
+                + "            \"type\":\"NOT_AVAILABLE\",\n" + "            \"category\":\"WARNING\"\n"
+                + "          }\n" + "        ]\n" + "    }\n" + "}");
+        getFixture().processUpdate("BatteryLevel", deviceServiceData);
+        verify(getCallback()).stateUpdated(
+                new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL), UnDefType.UNDEF);
+        verify(getCallback()).stateUpdated(
+                new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.OFF);
+    }
+}
index 38432d552aab429326ef8c6e9b0c193703eeef86..f3bd2ecda33e066d99b27ac0e138f381a2666468 100644 (file)
@@ -31,7 +31,6 @@ import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitc
 import org.openhab.core.library.types.OnOffType;
 import org.openhab.core.library.types.QuantityType;
 import org.openhab.core.thing.ChannelUID;
-import org.openhab.core.thing.ThingTypeUID;
 import org.openhab.core.thing.ThingUID;
 
 import com.google.gson.JsonElement;
@@ -60,8 +59,6 @@ public abstract class AbstractPowerSwitchHandlerTest<T extends AbstractPowerSwit
     public void testHandleCommand()
             throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
 
-        when(getThing().getUID()).thenReturn(new ThingUID("boschshc", "abcdef"));
-
         getFixture().handleCommand(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_POWER_SWITCH),
                 OnOffType.ON);
         verify(getBridgeHandler()).putState(eq(getDeviceID()), eq("PowerSwitch"), serviceStateCaptor.capture());
@@ -76,12 +73,8 @@ public abstract class AbstractPowerSwitchHandlerTest<T extends AbstractPowerSwit
         assertSame(PowerSwitchState.OFF, state.switchState);
     }
 
-    protected abstract ThingTypeUID getThingTypeUID();
-
     @Test
     public void testUpdateChannel_PowerSwitchState() {
-        when(getThing().getUID()).thenReturn(new ThingUID("boschshc", "abcdef"));
-
         JsonElement jsonObject = JsonParser
                 .parseString("{\n" + "  \"@type\": \"powerSwitchState\",\n" + "  \"switchState\": \"ON\"\n" + "}");
         getFixture().processUpdate("PowerSwitch", jsonObject);
@@ -97,8 +90,6 @@ public abstract class AbstractPowerSwitchHandlerTest<T extends AbstractPowerSwit
 
     @Test
     public void testUpdateChannel_PowerMeterServiceState() {
-        when(getThing().getUID()).thenReturn(new ThingUID("boschshc", "abcdef"));
-
         JsonElement jsonObject = JsonParser.parseString("{\n" + "  \"@type\": \"powerMeterState\",\n"
                 + "  \"powerConsumption\": \"23\",\n" + "  \"energyConsumption\": 42\n" + "}");
         getFixture().processUpdate("PowerMeter", jsonObject);
index 872ca5336b8e45623ac8bdb9aeaae7e26199359b..71558c76db7e4bfc6d7b29419fcdee40cdb590d5 100644 (file)
@@ -27,6 +27,7 @@ import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingStatus;
 import org.openhab.core.thing.ThingStatusDetail;
 import org.openhab.core.thing.ThingStatusInfo;
+import org.openhab.core.thing.ThingTypeUID;
 import org.openhab.core.thing.ThingUID;
 import org.openhab.core.thing.binding.ThingHandlerCallback;
 
@@ -57,6 +58,7 @@ public abstract class AbstractSHCHandlerTest<T extends BoschSHCHandler> {
     @BeforeEach
     public void beforeEach() {
         fixture = createFixture();
+        lenient().when(thing.getUID()).thenReturn(getThingUID());
         when(thing.getBridgeUID()).thenReturn(new ThingUID("boschshc", "shc", "myBridgeUID"));
         when(callback.getBridge(any())).thenReturn(bridge);
         fixture.setCallback(callback);
@@ -72,6 +74,12 @@ public abstract class AbstractSHCHandlerTest<T extends BoschSHCHandler> {
         return fixture;
     }
 
+    protected ThingUID getThingUID() {
+        return new ThingUID(getThingTypeUID(), "abcdef");
+    }
+
+    protected abstract ThingTypeUID getThingTypeUID();
+
     protected Configuration getConfiguration() {
         return new Configuration();
     }
index 9877caeb65813bca81a6cc95ab02fc6dd0d049c3..48ab25ddaa1b0d5d75198834f9d5337210d00084 100644 (file)
@@ -71,10 +71,16 @@ class BoschHttpClientTest {
 
     @Test
     void getServiceUrl() {
-        assertEquals("https://127.0.0.1:8444/smarthome/devices/testDevice/services/testService/state",
+        assertEquals("https://127.0.0.1:8444/smarthome/devices/testDevice/services/testService",
                 httpClient.getServiceUrl("testService", "testDevice"));
     }
 
+    @Test
+    void getServiceStateUrl() {
+        assertEquals("https://127.0.0.1:8444/smarthome/devices/testDevice/services/testService/state",
+                httpClient.getServiceStateUrl("testService", "testDevice"));
+    }
+
     @Test
     void isAccessPossible() throws InterruptedException {
         assertFalse(httpClient.isAccessPossible());
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/motiondetector/MotionDetectorHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/motiondetector/MotionDetectorHandlerTest.java
new file mode 100644 (file)
index 0000000..e35d145
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * 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.boschshc.internal.devices.motiondetector;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandlerTest;
+import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * Unit Tests for {@link MotionDetectorHandler}.
+ *
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class MotionDetectorHandlerTest extends AbstractBatteryPoweredDeviceHandlerTest<MotionDetectorHandler> {
+
+    @Override
+    protected MotionDetectorHandler createFixture() {
+        return new MotionDetectorHandler(getThing());
+    }
+
+    @Override
+    protected String getDeviceID() {
+        return "hdm:ZigBee:000d6f0012fd2571";
+    }
+
+    @Override
+    protected ThingTypeUID getThingTypeUID() {
+        return BoschSHCBindingConstants.THING_TYPE_MOTION_DETECTOR;
+    }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandlerTest.java
new file mode 100644 (file)
index 0000000..73b9b20
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * 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.boschshc.internal.devices.thermostat;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandlerTest;
+import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * Unit Tests for {@link ThermostatHandler}.
+ *
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class ThermostatHandlerTest extends AbstractBatteryPoweredDeviceHandlerTest<ThermostatHandler> {
+
+    @Override
+    protected ThermostatHandler createFixture() {
+        return new ThermostatHandler(getThing());
+    }
+
+    @Override
+    protected String getDeviceID() {
+        return "hdm:ZigBee:000d6f0017f1ace2";
+    }
+
+    @Override
+    protected ThingTypeUID getThingTypeUID() {
+        return BoschSHCBindingConstants.THING_TYPE_THERMOSTAT;
+    }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/twinguard/TwinguardHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/twinguard/TwinguardHandlerTest.java
new file mode 100644 (file)
index 0000000..0b22f87
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * 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.boschshc.internal.devices.twinguard;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandlerTest;
+import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * Unit Tests for {@link TwinguardHandler}.
+ *
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class TwinguardHandlerTest extends AbstractBatteryPoweredDeviceHandlerTest<TwinguardHandler> {
+
+    @Override
+    protected TwinguardHandler createFixture() {
+        return new TwinguardHandler(getThing());
+    }
+
+    @Override
+    protected String getDeviceID() {
+        return "hdm:ZigBee:000d6f0016d1a193";
+    }
+
+    @Override
+    protected ThingTypeUID getThingTypeUID() {
+        return BoschSHCBindingConstants.THING_TYPE_TWINGUARD;
+    }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/wallthermostat/WallThermostatHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/wallthermostat/WallThermostatHandlerTest.java
new file mode 100644 (file)
index 0000000..8f1c2a6
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * 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.boschshc.internal.devices.wallthermostat;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandlerTest;
+import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * Unit Tests for {@link WallThermostatHandler}.
+ *
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class WallThermostatHandlerTest extends AbstractBatteryPoweredDeviceHandlerTest<WallThermostatHandler> {
+
+    @Override
+    protected WallThermostatHandler createFixture() {
+        return new WallThermostatHandler(getThing());
+    }
+
+    @Override
+    protected String getDeviceID() {
+        return "hdm:ZigBee:000d6f0016d1a193";
+    }
+
+    @Override
+    protected ThingTypeUID getThingTypeUID() {
+        return BoschSHCBindingConstants.THING_TYPE_WALL_THERMOSTAT;
+    }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/windowcontact/WindowContactHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/windowcontact/WindowContactHandlerTest.java
new file mode 100644 (file)
index 0000000..9acd2cc
--- /dev/null
@@ -0,0 +1,43 @@
+/**
+ * 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.boschshc.internal.devices.windowcontact;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandlerTest;
+import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * Unit Tests for {@link WindowContactHandler}.
+ *
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class WindowContactHandlerTest extends AbstractBatteryPoweredDeviceHandlerTest<WindowContactHandler> {
+
+    @Override
+    protected WindowContactHandler createFixture() {
+        return new WindowContactHandler(getThing());
+    }
+
+    @Override
+    protected String getDeviceID() {
+        return "hdm:HomeMaticIP:3014D711A000009D545DEB39D";
+    }
+
+    @Override
+    protected ThingTypeUID getThingTypeUID() {
+        return BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT;
+    }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/batterylevel/BatteryLevelTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/batterylevel/BatteryLevelTest.java
new file mode 100644 (file)
index 0000000..b788074
--- /dev/null
@@ -0,0 +1,98 @@
+/**
+ * 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.boschshc.internal.services.batterylevel;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.ArrayList;
+
+import org.junit.jupiter.api.Test;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Fault;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Faults;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * Unit tests for {@link BatteryLevel}.
+ *
+ * @author David Pace - Initial contribution
+ *
+ */
+class BatteryLevelTest {
+
+    @Test
+    void testGet() {
+        assertSame(BatteryLevel.OK, BatteryLevel.get("OK"));
+        assertSame(BatteryLevel.OK, BatteryLevel.get("ok"));
+        assertSame(BatteryLevel.LOW_BATTERY, BatteryLevel.get("LOW_BATTERY"));
+        assertSame(BatteryLevel.LOW_BATTERY, BatteryLevel.get("low_battery"));
+        assertSame(BatteryLevel.CRITICAL_LOW, BatteryLevel.get("CRITICAL_LOW"));
+        assertSame(BatteryLevel.CRITICAL_LOW, BatteryLevel.get("critical_low"));
+        assertSame(BatteryLevel.CRITICALLY_LOW_BATTERY, BatteryLevel.get("CRITICALLY_LOW_BATTERY"));
+        assertSame(BatteryLevel.CRITICALLY_LOW_BATTERY, BatteryLevel.get("critically_low_battery"));
+        assertSame(BatteryLevel.NOT_AVAILABLE, BatteryLevel.get("NOT_AVAILABLE"));
+        assertSame(BatteryLevel.NOT_AVAILABLE, BatteryLevel.get("not_available"));
+        assertNull(BatteryLevel.get("foo"));
+    }
+
+    @Test
+    void testFromDeviceServiceData() {
+        DeviceServiceData deviceServiceData = new DeviceServiceData();
+        assertSame(BatteryLevel.OK, BatteryLevel.fromDeviceServiceData(deviceServiceData));
+
+        Faults faults = new Faults();
+        deviceServiceData.faults = faults;
+        assertSame(BatteryLevel.OK, BatteryLevel.fromDeviceServiceData(deviceServiceData));
+
+        ArrayList<Fault> entries = new ArrayList<>();
+        faults.entries = entries;
+        assertSame(BatteryLevel.OK, BatteryLevel.fromDeviceServiceData(deviceServiceData));
+
+        Fault fault = new Fault();
+        entries.add(fault);
+        assertSame(BatteryLevel.OK, BatteryLevel.fromDeviceServiceData(deviceServiceData));
+
+        fault.category = "WARNING";
+        fault.type = "LOW_BATTERY";
+        assertSame(BatteryLevel.LOW_BATTERY, BatteryLevel.fromDeviceServiceData(deviceServiceData));
+
+        fault.type = "CRITICAL_LOW";
+        assertSame(BatteryLevel.CRITICAL_LOW, BatteryLevel.fromDeviceServiceData(deviceServiceData));
+
+        fault.type = "CRITICALLY_LOW_BATTERY";
+        assertSame(BatteryLevel.CRITICALLY_LOW_BATTERY, BatteryLevel.fromDeviceServiceData(deviceServiceData));
+
+        fault.type = "FOO";
+        assertSame(BatteryLevel.OK, BatteryLevel.fromDeviceServiceData(deviceServiceData));
+    }
+
+    @Test
+    void testToState() {
+        assertEquals(new DecimalType(100), BatteryLevel.OK.toState());
+        assertEquals(new DecimalType(10), BatteryLevel.LOW_BATTERY.toState());
+        assertEquals(new DecimalType(1), BatteryLevel.CRITICAL_LOW.toState());
+        assertEquals(new DecimalType(1), BatteryLevel.CRITICALLY_LOW_BATTERY.toState());
+        assertEquals(UnDefType.UNDEF, BatteryLevel.NOT_AVAILABLE.toState());
+    }
+
+    @Test
+    void testToLowBatteryState() {
+        assertEquals(OnOffType.OFF, BatteryLevel.OK.toLowBatteryState());
+        assertEquals(OnOffType.ON, BatteryLevel.LOW_BATTERY.toLowBatteryState());
+        assertEquals(OnOffType.ON, BatteryLevel.CRITICAL_LOW.toLowBatteryState());
+        assertEquals(OnOffType.ON, BatteryLevel.CRITICALLY_LOW_BATTERY.toLowBatteryState());
+        assertEquals(OnOffType.OFF, BatteryLevel.NOT_AVAILABLE.toLowBatteryState());
+    }
+}