]> git.basschouten.com Git - openhab-addons.git/commitdiff
[boschshc] Add support for Light/Shutter Control II (#16400)
authorDavid Pace <dev@davidpace.de>
Sun, 31 Mar 2024 08:36:43 +0000 (10:36 +0200)
committerGitHub <noreply@github.com>
Sun, 31 Mar 2024 08:36:43 +0000 (10:36 +0200)
* [boschshc] Add support for Shutter Control II (#14562)
* add new channel type for child protection

Signed-off-by: David Pace <dev@davidpace.de>
36 files changed:
bundles/org.openhab.binding.boschshc/README.md
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtension.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandler.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerWithPowerMeter.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschDeviceIdUtils.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/BoschSHCDeviceHandler.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/BoschSHCHandlerFactory.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/Device.java
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/lightcontrol/LightControl2Handler.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/LightControlHandler.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/plug/PlugHandler.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/shuttercontrol/ShutterControl2Handler.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryService.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/childprotection/ChildProtectionService.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/childprotection/dto/ChildProtectionServiceState.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/PowerSwitchService.java
bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/config/configs.xml
bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties
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/AbstractBoschSHCDeviceHandlerTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCHandlerTest.java
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/AbstractPowerSwitchHandlerWithPowerMeterTest.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/BoschDeviceIdUtilsTest.java [new file with mode: 0644]
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/bridge/BridgeHandlerTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/LightControl2HandlerTest.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/LightControlHandlerTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/plug/PlugHandlerTest.java
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/shuttercontrol/ShutterControl2HandlerTest.java [new file with mode: 0644]
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryServiceTest.java

index 60b0e8e6db664e90b19eb09388ee322b55e69533..68a0ff5a2464952467f65c023314ead220453eb3 100644 (file)
@@ -1,6 +1,6 @@
 # Bosch Smart Home Binding
 
-Binding for the Bosch Smart Home.
+Binding for Bosch Smart Home devices.
 
 - [Bosch Smart Home Binding](#bosch-smart-home-binding)
   - [Supported Things](#supported-things)
@@ -10,8 +10,10 @@ Binding for the Bosch Smart Home.
     - [Twinguard Smoke Detector](#twinguard-smoke-detector)
     - [Door/Window Contact](#door-window-contact)
     - [Door/Window Contact II](#door-window-contact-ii)
+    - [Light Control II](#light-control-ii)
     - [Motion Detector](#motion-detector)
     - [Shutter Control](#shutter-control)
+    - [Shutter Control II](#shutter-control-ii)
     - [Thermostat](#thermostat)
     - [Climate Control](#climate-control)
     - [Wall Thermostat](#wall-thermostat)
@@ -114,6 +116,22 @@ Detects open windows and doors and features an additional button.
 | bypass          | Switch    | &#9744;  | Indicates whether the device is currently bypassed. Possible values are `ON`,`OFF` and `UNDEF` if the bypass state cannot be determined. |
 | signal-strength | Number    | &#9744;  | Communication quality between the device and the Smart Home Controller. Possible values range between 0 (unknown) and 4 (best signal strength). |
 
+### Light Control II
+
+This thing type is used if Light/Shutter Control II devices are configured as light controls.
+
+**Thing Type ID**: `light-control-2`
+
+| Channel Type ID    | Item Type     | Writable | Description                                                   |
+| ------------------ | ------------- | :------: | ------------------------------------------------------------- |
+| signal-strength    | Number        | &#9744;  | Communication quality between the device and the Smart Home Controller. Possible values range between 0 (unknown) and 4 (best signal strength). |
+| power-consumption  | Number:Power  | &#9744;  | Current power consumption (W) of the device.                  |
+| energy-consumption | Number:Energy | &#9744;  | Cumulated energy consumption (Wh) of the device.              |
+| power-switch-1     | Switch        | &#9745;  | Switches the light on or off (circuit 1).                     |
+| child-protection-1 | Switch        | &#9745;  | Indicates whether the child protection is active (circuit 1). |
+| power-switch-2     | Switch        | &#9745;  | Switches the light on or off (circuit 2).                     |
+| child-protection-2 | Switch        | &#9745;  | Indicates whether the child protection is active (circuit 2). |
+
 ### Motion Detector
 
 Detects every movement through an intelligent combination of passive infra-red technology and an additional temperature sensor.
@@ -137,6 +155,20 @@ Control of your shutter to take any position you desire.
 | --------------- | ------------- | :------: | ---------------------------------------- |
 | level           | Rollershutter | &#9745;  | Current open ratio (0 to 100, Step 0.5). |
 
+### Shutter Control II
+
+This thing type is used if Light/Shutter Control II devices are configured as shutter controls.
+
+**Thing Type ID**: `shutter-control-2`
+
+| Channel Type ID    | Item Type     | Writable | Description                                       |
+| ------------------ | ------------- | :------: | ------------------------------------------------- |
+| level              | Rollershutter | &#9745;  | Current open ratio (0 to 100, Step 0.5).          |
+| signal-strength    | Number        | &#9744;  | Communication quality between the device and the Smart Home Controller. Possible values range between 0 (unknown) and 4 (best signal strength). |
+| child-protection   | Switch        | &#9745;  | Indicates whether the child protection is active. |
+| power-consumption  | Number:Power  | &#9744;  | Current power consumption (W) of the device.      |
+| energy-consumption | Number:Energy | &#9744;  | Cumulated energy consumption (Wh) of the device.  |
+
 ### Thermostat
 
 Radiator thermostat
index 822146bff43c94d21c0c4702c0cb345e93e1ea4b..36354778c4f5b92fcba25773854c03db10a62cee 100644 (file)
@@ -79,10 +79,10 @@ public class BoschShcCommandExtension extends AbstractConsoleCommandExtension im
      */
     List<String> getAllBoschShcServices() {
         return List.of("airqualitylevel", "batterylevel", "binaryswitch", "bypass", "cameranotification", "childlock",
-                "communicationquality", "hsbcoloractuator", "humiditylevel", "illuminance", "intrusion", "keypad",
-                "latestmotion", "multilevelswitch", "powermeter", "powerswitch", "privacymode", "roomclimatecontrol",
-                "shuttercontact", "shuttercontrol", "silentmode", "smokedetectorcheck", "temperaturelevel", "userstate",
-                "valvetappet");
+                "childprotection", "communicationquality", "hsbcoloractuator", "humiditylevel", "illuminance",
+                "intrusion", "keypad", "latestmotion", "multilevelswitch", "powermeter", "powerswitch", "privacymode",
+                "roomclimatecontrol", "shuttercontact", "shuttercontrol", "silentmode", "smokedetectorcheck",
+                "temperaturelevel", "userstate", "valvetappet");
     }
 
     @Override
index d02a8d42fbe10d4969f2cc2b76e3fb4a38bb2a5f..81ecfbf27f772b712a6d0a65a73f6686a179e3fa 100644 (file)
  */
 package org.openhab.binding.boschshc.internal.devices;
 
-import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_POWER_SWITCH;
 
 import java.util.List;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
-import org.openhab.binding.boschshc.internal.services.powermeter.PowerMeterService;
-import org.openhab.binding.boschshc.internal.services.powermeter.dto.PowerMeterServiceState;
 import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchService;
 import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState;
 import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState;
 import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.QuantityType;
-import org.openhab.core.library.unit.Units;
 import org.openhab.core.thing.ChannelUID;
 import org.openhab.core.thing.Thing;
 import org.openhab.core.types.Command;
@@ -61,8 +57,6 @@ public abstract class AbstractPowerSwitchHandler extends BoschSHCDeviceHandler {
         super.initializeServices();
 
         this.registerService(this.powerSwitchService, this::updateChannels, List.of(CHANNEL_POWER_SWITCH), true);
-        this.createService(PowerMeterService::new, this::updateChannels,
-                List.of(CHANNEL_POWER_CONSUMPTION, CHANNEL_ENERGY_CONSUMPTION), true);
     }
 
     @Override
@@ -79,19 +73,9 @@ public abstract class AbstractPowerSwitchHandler extends BoschSHCDeviceHandler {
     }
 
     /**
-     * Updates the channels which are linked to the {@link PowerMeterService} of the device.
+     * Updates the power switch channel when a new state is received.
      *
-     * @param state Current state of {@link PowerMeterService}.
-     */
-    private void updateChannels(PowerMeterServiceState state) {
-        super.updateState(CHANNEL_POWER_CONSUMPTION, new QuantityType<>(state.powerConsumption, Units.WATT));
-        super.updateState(CHANNEL_ENERGY_CONSUMPTION, new QuantityType<>(state.energyConsumption, Units.WATT_HOUR));
-    }
-
-    /**
-     * Updates the channels which are linked to the {@link PowerSwitchService} of the device.
-     *
-     * @param state Current state of {@link PowerSwitchService}.
+     * @param state the new {@link PowerSwitchService} state.
      */
     private void updateChannels(PowerSwitchServiceState state) {
         State powerState = OnOffType.from(state.switchState.toString());
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerWithPowerMeter.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerWithPowerMeter.java
new file mode 100644 (file)
index 0000000..12407e9
--- /dev/null
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2010-2024 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.CHANNEL_ENERGY_CONSUMPTION;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.openhab.binding.boschshc.internal.services.powermeter.PowerMeterService;
+import org.openhab.binding.boschshc.internal.services.powermeter.dto.PowerMeterServiceState;
+import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchService;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.Thing;
+
+/**
+ * Abstract handler implementation for devices providing a {@link PowerSwitchService} and a {@link PowerMeterService}.
+ * <p>
+ * Examples for such devices are smart plugs and in-wall switches.
+ * 
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+public abstract class AbstractPowerSwitchHandlerWithPowerMeter extends AbstractPowerSwitchHandler {
+
+    protected AbstractPowerSwitchHandlerWithPowerMeter(Thing thing) {
+        super(thing);
+    }
+
+    @Override
+    protected void initializeServices() throws BoschSHCException {
+        super.initializeServices();
+
+        this.createService(PowerMeterService::new, this::updateChannels,
+                List.of(CHANNEL_POWER_CONSUMPTION, CHANNEL_ENERGY_CONSUMPTION), true);
+    }
+
+    /**
+     * Updates the channels which are linked to the {@link PowerMeterService} of the device.
+     *
+     * @param state Current state of {@link PowerMeterService}.
+     */
+    private void updateChannels(PowerMeterServiceState state) {
+        super.updateState(CHANNEL_POWER_CONSUMPTION, new QuantityType<>(state.powerConsumption, Units.WATT));
+        super.updateState(CHANNEL_ENERGY_CONSUMPTION, new QuantityType<>(state.energyConsumption, Units.WATT_HOUR));
+    }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschDeviceIdUtils.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschDeviceIdUtils.java
new file mode 100644 (file)
index 0000000..b489cc5
--- /dev/null
@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2010-2024 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 org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Utilities for handling parent/child relations in Bosch device IDs.
+ * 
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+public final class BoschDeviceIdUtils {
+
+    private static final String CHILD_ID_SEPARATOR = "#";
+
+    private BoschDeviceIdUtils() {
+        // Utility Class
+    }
+
+    /**
+     * Returns whether the given device ID is a child device ID.
+     * <p>
+     * Example for a parent device ID:
+     * 
+     * <pre>
+     * hdm:ZigBee:70ac08fffefead2d
+     * </pre>
+     * 
+     * Example for a child device ID:
+     * 
+     * <pre>
+     * hdm:ZigBee:70ac08fffefead2d#2
+     * </pre>
+     * 
+     * @param deviceId the Bosch device ID to check
+     * @return <code>true</code> if the device ID contains a hash character, <code>false</code> otherwise
+     */
+    public static boolean isChildDeviceId(String deviceId) {
+        return deviceId.contains(CHILD_ID_SEPARATOR);
+    }
+
+    /**
+     * If the given device ID is a child device ID, the parent device ID is derived by cutting off the part starting
+     * from the hash character.
+     * 
+     * @param deviceId a device ID
+     * @return the parent device ID, if derivable. Otherwise the given ID is returned.
+     */
+    public static String getParentDeviceId(String deviceId) {
+        int hashIndex = deviceId.indexOf(CHILD_ID_SEPARATOR);
+        if (hashIndex < 0) {
+            return deviceId;
+        }
+
+        return deviceId.substring(0, hashIndex);
+    }
+}
index 4d6676c76754a05aaa043cca5e3f8072904ba7b2..ed849aff4e8251da13caf64815b8f3f665da0081 100644 (file)
@@ -39,6 +39,7 @@ public class BoschSHCBindingConstants {
     public static final ThingTypeUID THING_TYPE_WINDOW_CONTACT_2 = new ThingTypeUID(BINDING_ID, "window-contact-2");
     public static final ThingTypeUID THING_TYPE_MOTION_DETECTOR = new ThingTypeUID(BINDING_ID, "motion-detector");
     public static final ThingTypeUID THING_TYPE_SHUTTER_CONTROL = new ThingTypeUID(BINDING_ID, "shutter-control");
+    public static final ThingTypeUID THING_TYPE_SHUTTER_CONTROL_2 = new ThingTypeUID(BINDING_ID, "shutter-control-2");
     public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat");
     public static final ThingTypeUID THING_TYPE_CLIMATE_CONTROL = new ThingTypeUID(BINDING_ID, "climate-control");
     public static final ThingTypeUID THING_TYPE_WALL_THERMOSTAT = new ThingTypeUID(BINDING_ID, "wall-thermostat");
@@ -51,9 +52,10 @@ public class BoschSHCBindingConstants {
     public static final ThingTypeUID THING_TYPE_SMOKE_DETECTOR = new ThingTypeUID(BINDING_ID, "smoke-detector");
     public static final ThingTypeUID THING_TYPE_UNIVERSAL_SWITCH = new ThingTypeUID(BINDING_ID, "universal-switch");
     public static final ThingTypeUID THING_TYPE_UNIVERSAL_SWITCH_2 = new ThingTypeUID(BINDING_ID, "universal-switch-2");
+    public static final ThingTypeUID THING_TYPE_SMOKE_DETECTOR_2 = new ThingTypeUID(BINDING_ID, "smoke-detector-2");
+    public static final ThingTypeUID THING_TYPE_LIGHT_CONTROL_2 = new ThingTypeUID(BINDING_ID, "light-control-2");
 
     public static final ThingTypeUID THING_TYPE_USER_DEFINED_STATE = new ThingTypeUID(BINDING_ID, "user-defined-state");
-    public static final ThingTypeUID THING_TYPE_SMOKE_DETECTOR_2 = new ThingTypeUID(BINDING_ID, "smoke-detector-2");
 
     // List of all Channel IDs
     // Auto-generated from thing-types.xml via script, don't modify
@@ -76,6 +78,7 @@ public class BoschSHCBindingConstants {
     public static final String CHANNEL_VALVE_TAPPET_POSITION = "valve-tappet-position";
     public static final String CHANNEL_SETPOINT_TEMPERATURE = "setpoint-temperature";
     public static final String CHANNEL_CHILD_LOCK = "child-lock";
+    public static final String CHANNEL_CHILD_PROTECTION = "child-protection";
     public static final String CHANNEL_PRIVACY_MODE = "privacy-mode";
     public static final String CHANNEL_CAMERA_NOTIFICATION = "camera-notification";
     public static final String CHANNEL_SYSTEM_AVAILABILITY = "system-availability";
@@ -99,6 +102,14 @@ public class BoschSHCBindingConstants {
     public static final String CHANNEL_KEY_EVENT_TYPE = "key-event-type";
     public static final String CHANNEL_KEY_EVENT_TIMESTAMP = "key-event-timestamp";
 
+    // numbered channels
+    // the rationale for introducing numbered channels was discussed in
+    // https://github.com/openhab/openhab-addons/pull/16400
+    public static final String CHANNEL_POWER_SWITCH_1 = "power-switch-1";
+    public static final String CHANNEL_POWER_SWITCH_2 = "power-switch-2";
+    public static final String CHANNEL_CHILD_PROTECTION_1 = "child-protection-1";
+    public static final String CHANNEL_CHILD_PROTECTION_2 = "child-protection-2";
+
     public static final String CHANNEL_USER_DEFINED_STATE = "user-state";
 
     // static device/service names
index 99bf97414c9145a8172b855575cc7978a7005f68..6791b19c9273db436eed0839f5a2e2202c2b0393 100644 (file)
@@ -17,6 +17,7 @@ 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.dto.Device;
 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
 import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingStatus;
@@ -57,30 +58,68 @@ public abstract class BoschSHCDeviceHandler extends BoschSHCHandler {
 
     @Override
     public void initialize() {
-        var config = this.config = getConfigAs(BoschSHCConfiguration.class);
+        this.config = getConfigAs(BoschSHCConfiguration.class);
 
         String deviceId = config.id;
+        @Nullable
+        Device deviceInfo = validateDeviceId(deviceId);
+        if (deviceInfo == null) {
+            return;
+        }
+
+        if (!processDeviceInfo(deviceInfo)) {
+            return;
+        }
+
+        super.initialize();
+    }
+
+    /**
+     * Allows the handler to process the device info that was obtained from a REST
+     * call to the Smart Home Controller at <code>/devices/{deviceId}</code>.
+     * 
+     * @param deviceInfo the device info obtained from the controller, guaranteed to be non-null
+     * @return <code>true</code> if the device info is valid and the initialization should proceed, <code>false</code>
+     *         otherwise
+     */
+    protected boolean processDeviceInfo(Device deviceInfo) {
+        return true;
+    }
+
+    /**
+     * Attempts to obtain information about the device with the specified ID via a REST call.
+     * <p>
+     * If the REST call is successful, the device ID is considered to be valid and the resulting {@link Device} object
+     * is returned.
+     * <p>
+     * If the device ID is not configured/empty or the REST call is not successful, the device ID is considered invalid
+     * and <code>null</code> is returned.
+     * 
+     * @param deviceId the device ID to check
+     * @return the {@link Device} info object if the REST call was successful, <code>null</code> otherwise
+     */
+    @Nullable
+    protected Device validateDeviceId(@Nullable String deviceId) {
         if (deviceId == null || deviceId.isBlank()) {
             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
                     "@text/offline.conf-error.empty-device-id");
-            return;
+            return null;
         }
 
         // Try to get device info to make sure the device exists
         try {
             var bridgeHandler = this.getBridgeHandler();
-            var info = bridgeHandler.getDeviceInfo(deviceId);
-            logger.trace("Device initialized:\n{}", info);
+            var deviceInfo = bridgeHandler.getDeviceInfo(deviceId);
+            logger.trace("Device validated and initialized:\n{}", deviceInfo);
+            return deviceInfo;
         } catch (TimeoutException | ExecutionException | BoschSHCException e) {
             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
-            return;
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
-            return;
         }
 
-        super.initialize();
+        return null;
     }
 
     /**
index 6ccc016e1e3e85895b8d8154be0c43b619c56b39..76cdf6dd0a701c3769fe0da72b2eb6aa09947c8a 100644 (file)
@@ -49,6 +49,7 @@ import com.google.gson.JsonElement;
  * @author Stefan Kästle - Initial contribution
  * @author Christian Oeing - refactorings of e.g. server registration
  * @author David Pace - Handler abstraction
+ * @author David Pace - Support for child device updates
  */
 @NonNullByDefault
 public abstract class BoschSHCHandler extends BaseThingHandler {
@@ -154,7 +155,7 @@ public abstract class BoschSHCHandler extends BaseThingHandler {
      * @param stateData Current state of device service. Serialized as JSON.
      */
     public void processUpdate(String serviceName, @Nullable JsonElement stateData) {
-        // Check services of device to correctly
+        // Find service(s) with the specified name and propagate new state to them
         for (DeviceService<? extends BoschSHCServiceState> deviceService : this.services) {
             BoschSHCService<? extends BoschSHCServiceState> service = deviceService.service;
             if (serviceName.equals(service.getServiceName())) {
@@ -163,11 +164,23 @@ public abstract class BoschSHCHandler extends BaseThingHandler {
         }
     }
 
+    /**
+     * Processes an update for a logical child device.
+     * 
+     * @param childDeviceId the ID of the logical child device
+     * @param serviceName the name of the service this update is targeted at
+     * @param stateData the new service state serialized as JSON
+     */
+    public void processChildUpdate(String childDeviceId, String serviceName, @Nullable JsonElement stateData) {
+        // default implementation is empty, subclasses may override
+    }
+
     /**
      * Use this method to register all services of the device with
      * {@link #registerService(BoschSHCService, Consumer, Collection, boolean)}.
      */
     protected void initializeServices() throws BoschSHCException {
+        // default implementation is empty, subclasses may override
     }
 
     /**
index c512d1090556582288566136ac2817fb8de5e8cc..94c9bfad5301f4ee1f24b0ad24fe7079cb1ce225 100644 (file)
@@ -17,9 +17,11 @@ import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConst
 import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_CLIMATE_CONTROL;
 import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_INTRUSION_DETECTION_SYSTEM;
 import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_INWALL_SWITCH;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_LIGHT_CONTROL_2;
 import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_MOTION_DETECTOR;
 import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SHC;
 import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL_2;
 import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SMART_BULB;
 import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SMART_PLUG_COMPACT;
 import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_SMOKE_DETECTOR;
@@ -43,9 +45,11 @@ import org.openhab.binding.boschshc.internal.devices.bridge.BridgeHandler;
 import org.openhab.binding.boschshc.internal.devices.camera.CameraHandler;
 import org.openhab.binding.boschshc.internal.devices.climatecontrol.ClimateControlHandler;
 import org.openhab.binding.boschshc.internal.devices.intrusion.IntrusionDetectionHandler;
+import org.openhab.binding.boschshc.internal.devices.lightcontrol.LightControl2Handler;
 import org.openhab.binding.boschshc.internal.devices.lightcontrol.LightControlHandler;
 import org.openhab.binding.boschshc.internal.devices.motiondetector.MotionDetectorHandler;
 import org.openhab.binding.boschshc.internal.devices.plug.PlugHandler;
+import org.openhab.binding.boschshc.internal.devices.shuttercontrol.ShutterControl2Handler;
 import org.openhab.binding.boschshc.internal.devices.shuttercontrol.ShutterControlHandler;
 import org.openhab.binding.boschshc.internal.devices.smartbulb.SmartBulbHandler;
 import org.openhab.binding.boschshc.internal.devices.smokedetector.SmokeDetector2Handler;
@@ -109,6 +113,7 @@ public class BoschSHCHandlerFactory extends BaseThingHandlerFactory {
             new ThingTypeHandlerMapping(THING_TYPE_WINDOW_CONTACT_2, WindowContact2Handler::new),
             new ThingTypeHandlerMapping(THING_TYPE_MOTION_DETECTOR, MotionDetectorHandler::new),
             new ThingTypeHandlerMapping(THING_TYPE_SHUTTER_CONTROL, ShutterControlHandler::new),
+            new ThingTypeHandlerMapping(THING_TYPE_SHUTTER_CONTROL_2, ShutterControl2Handler::new),
             new ThingTypeHandlerMapping(THING_TYPE_THERMOSTAT, ThermostatHandler::new),
             new ThingTypeHandlerMapping(THING_TYPE_CLIMATE_CONTROL, ClimateControlHandler::new),
             new ThingTypeHandlerMapping(THING_TYPE_WALL_THERMOSTAT, WallThermostatHandler::new),
@@ -123,7 +128,8 @@ public class BoschSHCHandlerFactory extends BaseThingHandlerFactory {
                     thing -> new UniversalSwitchHandler(thing, timeZoneProvider)),
             new ThingTypeHandlerMapping(THING_TYPE_UNIVERSAL_SWITCH_2,
                     thing -> new UniversalSwitch2Handler(thing, timeZoneProvider)),
-            new ThingTypeHandlerMapping(THING_TYPE_SMOKE_DETECTOR_2, SmokeDetector2Handler::new));
+            new ThingTypeHandlerMapping(THING_TYPE_SMOKE_DETECTOR_2, SmokeDetector2Handler::new),
+            new ThingTypeHandlerMapping(THING_TYPE_LIGHT_CONTROL_2, LightControl2Handler::new));
 
     @Override
     public boolean supportsThingType(ThingTypeUID thingTypeUID) {
index 945b3889c12942f6c998b3fc9237485b904e3de7..c498c3b933355b10d356b0fc455180f847fe159a 100644 (file)
@@ -111,7 +111,32 @@ public class BoschHttpClient extends HttpClient {
      * @return Bosch SHC URL for passed endpoint
      */
     public String getBoschShcUrl(String endpoint) {
-        return String.format("https://%s:8444/%s", this.ipAddress, endpoint);
+        String url = String.format("https://%s:8444/%s", this.ipAddress, endpoint);
+        return escapeURL(url);
+    }
+
+    /**
+     * Performs specific URL escaping required for certain Bosch SHC URLs.
+     * <p>
+     * In particular, hash characters in child device IDs must be escaped with <code>%23</code>.
+     * <p>
+     * Invalid example:
+     * 
+     * <pre>
+     * https://host:port/devices/hdm:ZigBee:70ac08fffe5294ea#3/services/PowerSwitch/state
+     * </pre>
+     * 
+     * Valid example:
+     * 
+     * <pre>
+     * https://host:port/devices/hdm:ZigBee:70ac08fffe5294ea%233/services/PowerSwitch/state
+     * </pre>
+     * 
+     * @param url the URL to be escaped
+     * @return the escaped URL
+     */
+    private String escapeURL(String url) {
+        return url.replace("#", "%23");
     }
 
     /**
index bfcedd426e8c74742213d59340f27c950946e33b..1bae6c86d13e0cb878ae53cdce0ca19987f4961a 100644 (file)
@@ -34,6 +34,7 @@ import org.eclipse.jetty.client.api.Request;
 import org.eclipse.jetty.client.api.Response;
 import org.eclipse.jetty.http.HttpStatus;
 import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.openhab.binding.boschshc.internal.devices.BoschDeviceIdUtils;
 import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
 import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
@@ -476,7 +477,7 @@ public class BridgeHandler extends BaseBridgeHandler {
      *
      * @param result Results from Long Polling
      */
-    private void handleLongPollResult(LongPollResult result) {
+    void handleLongPollResult(LongPollResult result) {
         for (BoschSHCServiceState serviceState : result.result) {
             if (serviceState instanceof DeviceServiceData deviceServiceData) {
                 handleDeviceServiceData(deviceServiceData);
@@ -562,12 +563,7 @@ public class BridgeHandler extends BaseBridgeHandler {
      */
     private void forwardStateToHandlers(BoschSHCServiceState serviceData, JsonElement state, String updateDeviceId) {
         boolean handled = false;
-        final String serviceId;
-        if (serviceData instanceof UserDefinedState userState) {
-            serviceId = userState.getId();
-        } else {
-            serviceId = ((DeviceServiceData) serviceData).id;
-        }
+        final String serviceId = getServiceId(serviceData);
 
         Bridge bridge = this.getThing();
         for (Thing childThing : bridge.getThings()) {
@@ -578,13 +574,17 @@ public class BridgeHandler extends BaseBridgeHandler {
                 @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, serviceId, state);
-                    handler.processUpdate(serviceId, state);
+                if (deviceId == null) {
+                    continue;
                 }
+
+                logger.trace("Checking device {}, looking for {}", deviceId, updateDeviceId);
+
+                // handled is a boolean latch that stays true once it becomes true
+                // note that no short-circuiting operators are used, meaning that the method
+                // calls will always be evaluated, even if the latch is already true
+                handled |= notifyHandler(handler, deviceId, updateDeviceId, serviceId, state);
+                handled |= notifyParentHandler(handler, deviceId, updateDeviceId, serviceId, state);
             } else {
                 logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler);
             }
@@ -595,6 +595,61 @@ public class BridgeHandler extends BaseBridgeHandler {
         }
     }
 
+    /**
+     * Notifies the given handler if its device ID exactly matches the device ID for which the update was received.
+     * 
+     * @param handler the handler to be notified if applicable
+     * @param deviceId the device ID associated with the handler
+     * @param updateDeviceId the device ID for which the update was received
+     * @param serviceId the ID of the service for which the update was received
+     * @param state the received state object as JSON element
+     * 
+     * @return <code>true</code> if the handler matched and was notified, <code>false</code> otherwise
+     */
+    private boolean notifyHandler(BoschSHCHandler handler, String deviceId, String updateDeviceId, String serviceId,
+            JsonElement state) {
+        if (updateDeviceId.equals(deviceId)) {
+            logger.debug("Found handler {}, calling processUpdate() for service {} with state {}", handler, serviceId,
+                    state);
+            handler.processUpdate(serviceId, state);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * If an update is received for a logical child device and the given handler is the parent device handler, the
+     * parent handler is notified.
+     * 
+     * @param handler the handler to be notified if applicable
+     * @param deviceId the device ID associated with the handler
+     * @param updateDeviceId the device ID for which the update was received
+     * @param serviceId the ID of the service for which the update was received
+     * @param state the received state object as JSON element
+     * @return <code>true</code> if the given handler was the corresponding parent handler and was notified,
+     *         <code>false</code> otherwise
+     */
+    private boolean notifyParentHandler(BoschSHCHandler handler, String deviceId, String updateDeviceId,
+            String serviceId, JsonElement state) {
+        if (BoschDeviceIdUtils.isChildDeviceId(updateDeviceId)) {
+            String parentDeviceId = BoschDeviceIdUtils.getParentDeviceId(updateDeviceId);
+            if (parentDeviceId.equals(deviceId)) {
+                logger.debug("Notifying parent handler {} about update for child device for service {} with state {}",
+                        handler, serviceId, state);
+                handler.processChildUpdate(updateDeviceId, serviceId, state);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private String getServiceId(BoschSHCServiceState serviceData) {
+        if (serviceData instanceof UserDefinedState userState) {
+            return userState.getId();
+        }
+        return ((DeviceServiceData) serviceData).id;
+    }
+
     /**
      * Bridge callback handler for the failures during long polls.
      *
index 2dbb3128a89b067078ca81e557a10c6ab031b9eb..9fd7e1caecd9d40f874ebc14bd3941c3fc261e7b 100644 (file)
@@ -18,23 +18,25 @@ import com.google.gson.annotations.SerializedName;
 
 /**
  * Represents a single devices connected to the Bosch Smart Home Controller.
- *
- * Example from Json:
- *
+ * <p>
+ * Example JSON:
+ * 
+ * <pre>
  * {
- * "@type":"device",
- * "rootDeviceId":"64-da-a0-02-14-9b",
- * "id":"hdm:HomeMaticIP:3014F711A00004953859F31B",
- * "deviceServiceIds":["PowerMeter","PowerSwitch","PowerSwitchProgram","Routing"],
- * "manufacturer":"BOSCH",
- * "roomId":"hz_3",
- * "deviceModel":"PSM",
- * "serial":"3014F711A00004953859F31B",
- * "profile":"GENERIC",
- * "name":"Coffee Machine",
- * "status":"AVAILABLE",
- * "childDeviceIds":[]
+ *   "@type": "device",
+ *   "rootDeviceId": "64-da-a0-02-14-9b",
+ *   "id": "hdm:HomeMaticIP:3014F711A00004953859F31B",
+ *   "deviceServiceIds": ["PowerMeter","PowerSwitch","PowerSwitchProgram","Routing"],
+ *   "manufacturer": "BOSCH",
+ *   "roomId": "hz_3",
+ *   "deviceModel": "PSM",
+ *   "serial": "3014F711A00004953859F31B",
+ *   "profile": "GENERIC",
+ *   "name": "Coffee Machine",
+ *   "status": "AVAILABLE",
+ *   "childDeviceIds": []
  * }
+ * </pre>
  *
  * @author Stefan Kästle - Initial contribution
  */
index a2deba85e0d88ba133bccb139bb019ec950f242e..58bfd2f55e336e8a719f80d999731a708ebb7e99 100644 (file)
@@ -18,25 +18,30 @@ import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
 
 /**
  * Response of the Controller for a Long Poll API call.
+ * <p>
+ * Example JSON:
+ * 
+ * <pre>
+ * {
+ *   "result": [{
+ *     "path": "/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch",
+ *     "@type": "DeviceServiceData",
+ *     "id": "PowerSwitch",
+ *     "state": {
+ *        "@type": "powerSwitchState",
+ *        "switchState": "ON"
+ *     },
+ *     "deviceId": "hdm:HomeMaticIP:3014F711A0001916D859A8A9"
+ *   }],
+ *   "jsonrpc": "2.0"
+ * }
+ * </pre>
  *
  * @author Stefan Kästle - Initial contribution
  */
 public class LongPollResult {
 
-    /**
-     * {"result":[
-     * ..{
-     * ...."path":"/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch",
-     * ...."@type":"DeviceServiceData",
-     * ...."id":"PowerSwitch",
-     * ...."state":{
-     * ......"@type":"powerSwitchState",
-     * ......"switchState":"ON"
-     * ....},
-     * ...."deviceId":"hdm:HomeMaticIP:3014F711A0001916D859A8A9"}
-     * ],"jsonrpc":"2.0"}
-     */
-
     public ArrayList<BoschSHCServiceState> result;
+
     public String jsonrpc;
 }
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/LightControl2Handler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/LightControl2Handler.java
new file mode 100644 (file)
index 0000000..4ce129e
--- /dev/null
@@ -0,0 +1,240 @@
+/**
+ * Copyright (c) 2010-2024 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.lightcontrol;
+
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION_1;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION_2;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_POWER_SWITCH_1;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_POWER_SWITCH_2;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_SIGNAL_STRENGTH;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.boschshc.internal.devices.BoschSHCDeviceHandler;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.openhab.binding.boschshc.internal.services.childprotection.ChildProtectionService;
+import org.openhab.binding.boschshc.internal.services.childprotection.dto.ChildProtectionServiceState;
+import org.openhab.binding.boschshc.internal.services.communicationquality.CommunicationQualityService;
+import org.openhab.binding.boschshc.internal.services.communicationquality.dto.CommunicationQualityServiceState;
+import org.openhab.binding.boschshc.internal.services.powermeter.PowerMeterService;
+import org.openhab.binding.boschshc.internal.services.powermeter.dto.PowerMeterServiceState;
+import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchService;
+import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState;
+import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+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;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonElement;
+
+/**
+ * Handler for Light Control II devices.
+ * <p>
+ * This implementation handles both common channels and specific channels of the
+ * two logical child devices.
+ * 
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class LightControl2Handler extends BoschSHCDeviceHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(LightControl2Handler.class);
+
+    private @Nullable String childDeviceId1;
+    private @Nullable String childDeviceId2;
+
+    private PowerSwitchService lightSwitchCircuit1PowerSwitchService;
+    private PowerSwitchService lightSwitchCircuit2PowerSwitchService;
+
+    private ChildProtectionService lightSwitchCircuit1ChildProtectionService;
+    private ChildProtectionService lightSwitchCircuit2ChildProtectionService;
+
+    public LightControl2Handler(Thing thing) {
+        super(thing);
+
+        lightSwitchCircuit1PowerSwitchService = new PowerSwitchService();
+        lightSwitchCircuit2PowerSwitchService = new PowerSwitchService();
+
+        lightSwitchCircuit1ChildProtectionService = new ChildProtectionService();
+        lightSwitchCircuit2ChildProtectionService = new ChildProtectionService();
+    }
+
+    @Override
+    protected boolean processDeviceInfo(Device deviceInfo) {
+        super.processDeviceInfo(deviceInfo);
+
+        logger.debug("Initializing child devices of Light Control II, child device IDs from device info: {}",
+                deviceInfo.childDeviceIds);
+
+        if (deviceInfo.childDeviceIds == null || deviceInfo.childDeviceIds.size() != 2) {
+            updateStatusChildDeviceIDsNotObtainable();
+            return false;
+        }
+
+        List<String> childDeviceIds = new ArrayList<>(deviceInfo.childDeviceIds);
+        // since we were not sure whether the child device ID order is always the same,
+        // we ensure a deterministic order by sorting the child IDs
+        // see https://github.com/openhab/openhab-addons/pull/16400#discussion_r1497762612
+        Collections.sort(childDeviceIds);
+
+        logger.trace("Child device IDs for Light Control II after sorting: {}", childDeviceIds);
+
+        if (validateDeviceId(childDeviceIds.get(0)) == null || validateDeviceId(childDeviceIds.get(1)) == null) {
+            updateStatusChildDeviceIDsNotObtainable();
+            return false;
+        }
+
+        childDeviceId1 = childDeviceIds.get(0);
+        childDeviceId2 = childDeviceIds.get(1);
+
+        logger.debug("Child device IDs for Light Control II configured successfully.");
+        return true;
+    }
+
+    private void updateStatusChildDeviceIDsNotObtainable() {
+        super.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                "@text/offline.conf-error.child-device-ids-not-obtainable");
+    }
+
+    @Override
+    protected void initializeServices() throws BoschSHCException {
+        super.initializeServices();
+
+        createService(CommunicationQualityService::new, this::updateChannels, List.of(CHANNEL_SIGNAL_STRENGTH), true);
+        createService(PowerMeterService::new, this::updateChannels,
+                List.of(CHANNEL_POWER_CONSUMPTION, CHANNEL_ENERGY_CONSUMPTION), true);
+
+        // local variable required to ensure non-nullness, member can theoretically be modified
+        String lChildDeviceId1 = childDeviceId1;
+        if (lChildDeviceId1 == null) {
+            throw new BoschSHCException("Child device ID 1 is not set for thing " + getThing().getUID());
+        }
+
+        // local variable required to ensure non-nullness, member can theoretically be modified
+        String lChildDeviceId2 = childDeviceId2;
+        if (lChildDeviceId2 == null) {
+            throw new BoschSHCException("Child device ID 2 is not set for thing " + getThing().getUID());
+        }
+
+        lightSwitchCircuit1PowerSwitchService.initialize(getBridgeHandler(), lChildDeviceId1,
+                state -> updatePowerSwitchChannel(state, CHANNEL_POWER_SWITCH_1));
+        lightSwitchCircuit2PowerSwitchService.initialize(getBridgeHandler(), lChildDeviceId2,
+                state -> updatePowerSwitchChannel(state, CHANNEL_POWER_SWITCH_2));
+
+        lightSwitchCircuit1ChildProtectionService.initialize(getBridgeHandler(), lChildDeviceId1,
+                state -> updateChildProtectionChannel(state, CHANNEL_CHILD_PROTECTION_1));
+        lightSwitchCircuit2ChildProtectionService.initialize(getBridgeHandler(), lChildDeviceId2,
+                state -> updateChildProtectionChannel(state, CHANNEL_CHILD_PROTECTION_2));
+    }
+
+    private void updateChannels(CommunicationQualityServiceState communicationQualityServiceState) {
+        updateState(CHANNEL_SIGNAL_STRENGTH, communicationQualityServiceState.quality.toSystemSignalStrength());
+    }
+
+    /**
+     * Updates the channels which are linked to the {@link PowerMeterService} of the
+     * device.
+     *
+     * @param state Current state of {@link PowerMeterService}.
+     */
+    private void updateChannels(PowerMeterServiceState state) {
+        super.updateState(CHANNEL_POWER_CONSUMPTION, new QuantityType<>(state.powerConsumption, Units.WATT));
+        super.updateState(CHANNEL_ENERGY_CONSUMPTION, new QuantityType<>(state.energyConsumption, Units.WATT_HOUR));
+    }
+
+    @Override
+    public void processChildUpdate(String childDeviceId, String serviceName, @Nullable JsonElement stateData) {
+        super.processChildUpdate(childDeviceId, serviceName, stateData);
+
+        if (PowerSwitchService.POWER_SWITCH_SERVICE_NAME.equals(serviceName)) {
+            if (childDeviceId.equals(childDeviceId1)) {
+                lightSwitchCircuit1PowerSwitchService.onStateUpdate(stateData);
+            } else if (childDeviceId.equals(childDeviceId2)) {
+                lightSwitchCircuit2PowerSwitchService.onStateUpdate(stateData);
+            }
+        } else if (ChildProtectionService.CHILD_PROTECTION_SERVICE_NAME.equals(serviceName)) {
+            if (childDeviceId.equals(childDeviceId1)) {
+                lightSwitchCircuit1ChildProtectionService.onStateUpdate(stateData);
+            } else if (childDeviceId.equals(childDeviceId2)) {
+                lightSwitchCircuit2ChildProtectionService.onStateUpdate(stateData);
+            }
+        }
+    }
+
+    /**
+     * Updates the power switch channel for one of the child devices.
+     *
+     * @param state the new {@link PowerSwitchServiceState}
+     * @param channelId the power switch channel ID associated with the child device
+     */
+    private void updatePowerSwitchChannel(PowerSwitchServiceState state, String channelId) {
+        State powerState = OnOffType.from(state.switchState.toString());
+        super.updateState(channelId, powerState);
+    }
+
+    /**
+     * Updates the child protection channel for one of the child devices.
+     * 
+     * @param state the new {@link ChildProtectionServiceState}
+     * @param channelId the child protection channel ID associated with the child
+     *            device
+     */
+    private void updateChildProtectionChannel(ChildProtectionServiceState state, String channelId) {
+        super.updateState(channelId, OnOffType.from(state.childLockActive));
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        super.handleCommand(channelUID, command);
+
+        if (CHANNEL_POWER_SWITCH_1.equals(channelUID.getId()) && (command instanceof OnOffType onOffCommand)) {
+            updatePowerSwitchState(onOffCommand, lightSwitchCircuit1PowerSwitchService);
+        } else if (CHANNEL_POWER_SWITCH_2.equals(channelUID.getId()) && (command instanceof OnOffType onOffCommand)) {
+            updatePowerSwitchState(onOffCommand, lightSwitchCircuit2PowerSwitchService);
+        } else if (CHANNEL_CHILD_PROTECTION_1.equals(channelUID.getId())
+                && (command instanceof OnOffType onOffCommand)) {
+            updateChildProtectionState(onOffCommand, lightSwitchCircuit1ChildProtectionService);
+        } else if (CHANNEL_CHILD_PROTECTION_2.equals(channelUID.getId())
+                && (command instanceof OnOffType onOffCommand)) {
+            updateChildProtectionState(onOffCommand, lightSwitchCircuit2ChildProtectionService);
+        }
+    }
+
+    private void updatePowerSwitchState(OnOffType command, PowerSwitchService powerSwitchService) {
+        PowerSwitchServiceState state = new PowerSwitchServiceState();
+        state.switchState = PowerSwitchState.valueOf(command.toFullString());
+        this.updateServiceState(powerSwitchService, state);
+    }
+
+    private void updateChildProtectionState(OnOffType onOffCommand, ChildProtectionService childProtectionService) {
+        ChildProtectionServiceState childProtectionServiceState = new ChildProtectionServiceState();
+        childProtectionServiceState.childLockActive = onOffCommand == OnOffType.ON;
+        updateServiceState(childProtectionService, childProtectionServiceState);
+    }
+}
index 89bdcb01e755b07d83bac994bc006825e2b6be1f..c3bbdbe85bdc1efac6ee8ec28fb243d80174658b 100644 (file)
@@ -13,7 +13,7 @@
 package org.openhab.binding.boschshc.internal.devices.lightcontrol;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandler;
+import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandlerWithPowerMeter;
 import org.openhab.core.thing.Thing;
 
 /**
@@ -22,7 +22,7 @@ import org.openhab.core.thing.Thing;
  * @author Stefan Kästle - Initial contribution
  */
 @NonNullByDefault
-public class LightControlHandler extends AbstractPowerSwitchHandler {
+public class LightControlHandler extends AbstractPowerSwitchHandlerWithPowerMeter {
 
     public LightControlHandler(Thing thing) {
         super(thing);
index 008507d196e680326f3899015d7ca12f20d0e11d..5f11e8b17fb2314058c853cf2255b194dadcd1ec 100644 (file)
@@ -13,7 +13,7 @@
 package org.openhab.binding.boschshc.internal.devices.plug;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandler;
+import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandlerWithPowerMeter;
 import org.openhab.core.thing.Thing;
 
 /**
@@ -22,7 +22,7 @@ import org.openhab.core.thing.Thing;
  * @author David Pace - Initial contribution
  */
 @NonNullByDefault
-public class PlugHandler extends AbstractPowerSwitchHandler {
+public class PlugHandler extends AbstractPowerSwitchHandlerWithPowerMeter {
 
     public PlugHandler(Thing thing) {
         super(thing);
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/shuttercontrol/ShutterControl2Handler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/shuttercontrol/ShutterControl2Handler.java
new file mode 100644 (file)
index 0000000..4dea74e
--- /dev/null
@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2010-2024 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.shuttercontrol;
+
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION;
+import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_SIGNAL_STRENGTH;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.openhab.binding.boschshc.internal.services.childprotection.ChildProtectionService;
+import org.openhab.binding.boschshc.internal.services.childprotection.dto.ChildProtectionServiceState;
+import org.openhab.binding.boschshc.internal.services.communicationquality.CommunicationQualityService;
+import org.openhab.binding.boschshc.internal.services.communicationquality.dto.CommunicationQualityServiceState;
+import org.openhab.binding.boschshc.internal.services.powermeter.PowerMeterService;
+import org.openhab.binding.boschshc.internal.services.powermeter.dto.PowerMeterServiceState;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.types.Command;
+
+/**
+ * Handler for second generation shutter controls.
+ * <p>
+ * This handler is used if Shutter/Light Control II devices are configured as shutter controls.
+ * 
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class ShutterControl2Handler extends ShutterControlHandler {
+
+    private final ChildProtectionService childProtectionService;
+
+    public ShutterControl2Handler(Thing thing) {
+        super(thing);
+        this.childProtectionService = new ChildProtectionService();
+    }
+
+    @Override
+    protected void initializeServices() throws BoschSHCException {
+        super.initializeServices();
+
+        createService(CommunicationQualityService::new, this::updateChannels, List.of(CHANNEL_SIGNAL_STRENGTH), true);
+        registerService(childProtectionService, this::updateChannels, List.of(CHANNEL_CHILD_PROTECTION), true);
+        createService(PowerMeterService::new, this::updateChannels,
+                List.of(CHANNEL_POWER_CONSUMPTION, CHANNEL_ENERGY_CONSUMPTION), true);
+    }
+
+    private void updateChannels(CommunicationQualityServiceState communicationQualityServiceState) {
+        updateState(CHANNEL_SIGNAL_STRENGTH, communicationQualityServiceState.quality.toSystemSignalStrength());
+    }
+
+    private void updateChannels(ChildProtectionServiceState childProtectionServiceState) {
+        updateState(CHANNEL_CHILD_PROTECTION, OnOffType.from(childProtectionServiceState.childLockActive));
+    }
+
+    private void updateChannels(PowerMeterServiceState state) {
+        super.updateState(CHANNEL_POWER_CONSUMPTION, new QuantityType<>(state.powerConsumption, Units.WATT));
+        super.updateState(CHANNEL_ENERGY_CONSUMPTION, new QuantityType<>(state.energyConsumption, Units.WATT_HOUR));
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        super.handleCommand(channelUID, command);
+
+        if (BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION.equals(channelUID.getId())
+                && (command instanceof OnOffType onOffCommand)) {
+            updateChildProtectionState(onOffCommand);
+        }
+    }
+
+    private void updateChildProtectionState(OnOffType onOffCommand) {
+        ChildProtectionServiceState childProtectionServiceState = new ChildProtectionServiceState();
+        childProtectionServiceState.childLockActive = onOffCommand == OnOffType.ON;
+        updateServiceState(childProtectionService, childProtectionServiceState);
+    }
+}
index 4e0246e778f28032c0d5ddd370fc488d208346c5..e912cbd07da180f66e74cfeccc3317d9a274a5d7 100644 (file)
@@ -56,6 +56,11 @@ public class ThingDiscoveryService extends AbstractThingHandlerDiscoveryService<
 
     private final Logger logger = LoggerFactory.getLogger(ThingDiscoveryService.class);
 
+    /**
+     * Device model representing logical child devices of Light Control II
+     */
+    static final String DEVICE_MODEL_LIGHT_CONTROL_CHILD_DEVICE = "MICROMODULE_LIGHT_ATTACHED";
+
     protected static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(
             BoschSHCBindingConstants.THING_TYPE_INWALL_SWITCH, BoschSHCBindingConstants.THING_TYPE_TWINGUARD,
             BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT, BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT_2,
@@ -89,7 +94,10 @@ public class ThingDiscoveryService extends AbstractThingHandlerDiscoveryService<
             new AbstractMap.SimpleEntry<>("TRV", BoschSHCBindingConstants.THING_TYPE_THERMOSTAT),
             new AbstractMap.SimpleEntry<>("WRC2", BoschSHCBindingConstants.THING_TYPE_UNIVERSAL_SWITCH),
             new AbstractMap.SimpleEntry<>("SWITCH2", BoschSHCBindingConstants.THING_TYPE_UNIVERSAL_SWITCH_2),
-            new AbstractMap.SimpleEntry<>("SMOKE_DETECTOR2", BoschSHCBindingConstants.THING_TYPE_SMOKE_DETECTOR_2)
+            new AbstractMap.SimpleEntry<>("SMOKE_DETECTOR2", BoschSHCBindingConstants.THING_TYPE_SMOKE_DETECTOR_2),
+            new AbstractMap.SimpleEntry<>("MICROMODULE_SHUTTER", BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL_2),
+            new AbstractMap.SimpleEntry<>("MICROMODULE_AWNING", BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL_2),
+            new AbstractMap.SimpleEntry<>("MICROMODULE_LIGHT_CONTROL", BoschSHCBindingConstants.THING_TYPE_LIGHT_CONTROL_2)
 // Future Extension: map deviceModel names to BoschSHC Thing Types when they are supported
 //            new AbstractMap.SimpleEntry<>("SMOKE_DETECTION_SYSTEM", BoschSHCBindingConstants.),
 //            new AbstractMap.SimpleEntry<>("PRESENCE_SIMULATION_SERVICE", BoschSHCBindingConstants.),
@@ -219,13 +227,15 @@ public class ThingDiscoveryService extends AbstractThingHandlerDiscoveryService<
 
         logger.trace("- got thingTypeID '{}' for deviceModel '{}'", thingTypeUID.getId(), device.deviceModel);
 
-        ThingUID thingUID = new ThingUID(thingTypeUID, thingHandler.getThing().getUID(), device.id.replace(':', '_'));
+        ThingUID thingUID = new ThingUID(thingTypeUID, thingHandler.getThing().getUID(),
+                buildCompliantThingID(device.id));
 
         logger.trace("- got thingUID '{}' for device: '{}'", thingUID, device);
 
         DiscoveryResultBuilder discoveryResult = DiscoveryResultBuilder.create(thingUID).withThingType(thingTypeUID)
                 .withProperty("id", device.id).withLabel(getNiceName(device.name, roomName));
         discoveryResult.withBridge(thingHandler.getThing().getUID());
+
         if (!roomName.isEmpty()) {
             discoveryResult.withProperty("Location", roomName);
         }
@@ -235,6 +245,18 @@ public class ThingDiscoveryService extends AbstractThingHandlerDiscoveryService<
                 thingUID, thingTypeUID, device.id, device.deviceModel);
     }
 
+    /**
+     * Translates a Bosch device ID to an openHAB-compliant thing ID.
+     * <p>
+     * Characters that are not allowed in thing IDs are replaced by underscores.
+     * 
+     * @param deviceId the Bosch device ID
+     * @return the translated openHAB-compliant thing ID
+     */
+    private String buildCompliantThingID(String deviceId) {
+        return deviceId.replace(':', '_').replace('#', '_');
+    }
+
     private String getNiceName(String name, String roomName) {
         if (!name.startsWith("-")) {
             return name;
@@ -268,6 +290,15 @@ public class ThingDiscoveryService extends AbstractThingHandlerDiscoveryService<
         if (thingTypeId != null) {
             return new ThingTypeUID(BoschSHCBindingConstants.BINDING_ID, thingTypeId.getId());
         }
+
+        if (DEVICE_MODEL_LIGHT_CONTROL_CHILD_DEVICE.equals(device.deviceModel)) {
+            // Light Control II exposes a parent device and two child devices.
+            // We only add one thing for the parent device and the child devices are logically included.
+            // Therefore we do not need to add separate things for the child devices and need to suppress the
+            // log entry about the unknown device model.
+            return null;
+        }
+
         logger.debug("Unknown deviceModel '{}'! Please create a support request issue for this unknown device model.",
                 device.deviceModel);
         return null;
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/childprotection/ChildProtectionService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/childprotection/ChildProtectionService.java
new file mode 100644 (file)
index 0000000..17e4b31
--- /dev/null
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2010-2024 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.childprotection;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.services.BoschSHCService;
+import org.openhab.binding.boschshc.internal.services.childprotection.dto.ChildProtectionServiceState;
+
+/**
+ * Service to activate and deactivate child protection.
+ * 
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class ChildProtectionService extends BoschSHCService<ChildProtectionServiceState> {
+
+    public static final String CHILD_PROTECTION_SERVICE_NAME = "ChildProtection";
+
+    public ChildProtectionService() {
+        super(CHILD_PROTECTION_SERVICE_NAME, ChildProtectionServiceState.class);
+    }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/childprotection/dto/ChildProtectionServiceState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/childprotection/dto/ChildProtectionServiceState.java
new file mode 100644 (file)
index 0000000..0d4bbe6
--- /dev/null
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) 2010-2024 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.childprotection.dto;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
+
+/**
+ * State of the child protection service.
+ * 
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class ChildProtectionServiceState extends BoschSHCServiceState {
+
+    public ChildProtectionServiceState() {
+        super("ChildProtectionState");
+    }
+
+    public boolean childLockActive;
+}
index 95e4d220546e7daf055b490dcf6b63a171e58b86..7a3266d43829ffe56505ddc1330a1a53e55d153b 100644 (file)
@@ -24,7 +24,9 @@ import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitc
 @NonNullByDefault
 public class PowerSwitchService extends BoschSHCService<PowerSwitchServiceState> {
 
+    public static final String POWER_SWITCH_SERVICE_NAME = "PowerSwitch";
+
     public PowerSwitchService() {
-        super("PowerSwitch", PowerSwitchServiceState.class);
+        super(POWER_SWITCH_SERVICE_NAME, PowerSwitchServiceState.class);
     }
 }
index dbfa963a8fb8f639956a2de3be1b02ae0c322682..561bafa26931e9c42c57c6e4bd3ad7aac69a7236 100644 (file)
@@ -3,6 +3,7 @@
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
        xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
+
        <config-description uri="thing-type:boschshc:bridge">
                <parameter name="ipAddress" type="text" required="true">
                        <context>network-address</context>
                        <description>The system password of the Bosch Smart Home Controller necessary for pairing.</description>
                </parameter>
        </config-description>
+
        <config-description uri="thing-type:boschshc:device">
                <parameter name="id" type="text" required="true">
                        <label>Device ID</label>
                        <description>Unique ID of the device.</description>
                </parameter>
        </config-description>
+
        <config-description uri="thing-type:boschshc:user-defined-state">
                <parameter name="id" type="text" required="true">
                        <label>State ID</label>
                        <description>Unique ID of the state.</description>
                </parameter>
        </config-description>
+
 </config-description:config-descriptions>
index b2a7349c9e94c3eb5606a80fd659851fac55e247..8ce04446766ca959562a0cd0b094869351b56e41 100644 (file)
@@ -11,6 +11,8 @@ thing-type.boschshc.in-wall-switch.label = In-wall Switch
 thing-type.boschshc.in-wall-switch.description = A simple light control.
 thing-type.boschshc.intrusion-detection-system.label = Intrusion Detection System
 thing-type.boschshc.intrusion-detection-system.description = Allows to retrieve and control the state of the intrusion detection alarm system.
+thing-type.boschshc.light-control-2.label = Light Control II
+thing-type.boschshc.light-control-2.description = Advanced light control with two switch circuits.
 thing-type.boschshc.motion-detector.label = Motion Detector
 thing-type.boschshc.motion-detector.description = Detects every movement through an intelligent combination of passive infra-red technology and an additional temperature sensor.
 thing-type.boschshc.security-camera-360.label = Security Camera 360
@@ -19,6 +21,8 @@ thing-type.boschshc.security-camera-eyes.label = Security Camera Eyes
 thing-type.boschshc.security-camera-eyes.description = Outdoor security camera with motion detection and light.
 thing-type.boschshc.shc.label = Smart Home Controller
 thing-type.boschshc.shc.description = The Bosch Smart Home Bridge representing the Bosch Smart Home Controller.
+thing-type.boschshc.shutter-control-2.label = Shutter Control II
+thing-type.boschshc.shutter-control-2.description = Second generation shutter control.
 thing-type.boschshc.shutter-control.label = Shutter Control
 thing-type.boschshc.shutter-control.description = Control of your shutter to take any position you desire.
 thing-type.boschshc.smart-bulb.label = Smart Bulb
@@ -87,6 +91,8 @@ channel-type.boschshc.camera-notification.state.option.ON = Enabled
 channel-type.boschshc.camera-notification.state.option.OFF = Disabled
 channel-type.boschshc.child-lock.label = Child Lock
 channel-type.boschshc.child-lock.description = Enables or disables the child lock on the device.
+channel-type.boschshc.child-protection.label = Child Protection
+channel-type.boschshc.child-protection.description = Enables or disables the child protection on the device.
 channel-type.boschshc.combined-rating.label = Combined Rating
 channel-type.boschshc.combined-rating.description = Combined rating of the air quality.
 channel-type.boschshc.combined-rating.state.option.GOOD = Good Quality
@@ -185,3 +191,4 @@ offline.conf-error.empty-device-id = No device ID set.
 offline.conf-error.invalid-device-id = Device ID is invalid.
 offline.conf-error.empty-state-id = No ID set.
 offline.conf-error.invalid-state-id = ID is invalid.
+offline.conf-error.child-device-ids-not-obtainable = Could not obtain child device IDs.
index 4aea5e096a3977edd6860ebaa6f26f5337b88d54..05fb5e87f72a6473eddbdf959b1bc67c8320d340 100644 (file)
 
        </thing-type>
 
+       <thing-type id="shutter-control-2">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="shc"/>
+               </supported-bridge-type-refs>
+
+               <label>Shutter Control II</label>
+               <description>Second generation shutter control.</description>
+
+               <channels>
+                       <channel id="level" typeId="level"/>
+                       <channel id="signal-strength" typeId="system.signal-strength"/>
+                       <channel id="child-protection" typeId="child-protection"/>
+                       <channel id="power-consumption" typeId="power-consumption"/>
+                       <channel id="energy-consumption" typeId="energy-consumption"/>
+               </channels>
+
+               <config-description-ref uri="thing-type:boschshc:device"/>
+
+       </thing-type>
+
+       <thing-type id="light-control-2">
+               <supported-bridge-type-refs>
+                       <bridge-type-ref id="shc"/>
+               </supported-bridge-type-refs>
+
+               <label>Light Control II</label>
+               <description>Advanced light control with two switch circuits.</description>
+
+               <channels>
+                       <channel id="signal-strength" typeId="system.signal-strength"/>
+                       <channel id="power-consumption" typeId="power-consumption"/>
+                       <channel id="energy-consumption" typeId="energy-consumption"/>
+                       <channel id="power-switch-1" typeId="system.power"/>
+                       <channel id="child-protection-1" typeId="child-protection"/>
+                       <channel id="power-switch-2" typeId="system.power"/>
+                       <channel id="child-protection-2" typeId="child-protection"/>
+               </channels>
+
+               <config-description-ref uri="thing-type:boschshc:device"/>
+
+       </thing-type>
+
        <thing-type id="thermostat">
                <supported-bridge-type-refs>
                        <bridge-type-ref id="shc"/>
                <state readOnly="true"/>
        </channel-type>
 
+       <channel-type id="child-protection">
+               <item-type>Switch</item-type>
+               <label>Child Protection</label>
+               <description>Enables or disables the child protection on the device.</description>
+       </channel-type>
+
 </thing:thing-descriptions>
index 4eae021579a5f3e368b5e1fb0339af0e9039fa37..335af59e3da45788f020accb30691f22554adf59 100644 (file)
@@ -13,6 +13,7 @@
 package org.openhab.binding.boschshc.internal.devices;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
 import org.openhab.core.config.core.Configuration;
 
 /**
@@ -26,6 +27,13 @@ import org.openhab.core.config.core.Configuration;
 public abstract class AbstractBoschSHCDeviceHandlerTest<T extends BoschSHCDeviceHandler>
         extends AbstractBoschSHCHandlerTest<T> {
 
+    @Override
+    protected void configureDevice(Device device) {
+        super.configureDevice(device);
+
+        device.id = getDeviceID();
+    }
+
     @Override
     protected Configuration getConfiguration() {
         Configuration configuration = super.getConfiguration();
index 4c35a1c2e5f8fbb8a3280f6f20e559b33e12113f..c8efb9c9b31592af115a460340b0986914ab8a1f 100644 (file)
  */
 package org.openhab.binding.boschshc.internal.devices;
 
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeoutException;
@@ -25,6 +30,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.openhab.binding.boschshc.internal.devices.bridge.BridgeHandler;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
 import org.openhab.core.config.core.Configuration;
 import org.openhab.core.thing.Bridge;
@@ -58,6 +64,8 @@ public abstract class AbstractBoschSHCHandlerTest<T extends BoschSHCHandler> {
 
     private @Mock @NonNullByDefault({}) ThingHandlerCallback callback;
 
+    private @NonNullByDefault({}) Device device;
+
     protected AbstractBoschSHCHandlerTest() {
         this.fixture = createFixture();
     }
@@ -72,6 +80,10 @@ public abstract class AbstractBoschSHCHandlerTest<T extends BoschSHCHandler> {
         when(bridge.getHandler()).thenReturn(bridgeHandler);
         lenient().when(thing.getConfiguration()).thenReturn(getConfiguration());
 
+        device = new Device();
+        configureDevice(device);
+        lenient().when(bridgeHandler.getDeviceInfo(anyString())).thenReturn(device);
+
         fixture.initialize();
     }
 
@@ -107,6 +119,14 @@ public abstract class AbstractBoschSHCHandlerTest<T extends BoschSHCHandler> {
         return callback;
     }
 
+    protected Device getDevice() {
+        return device;
+    }
+
+    protected void configureDevice(Device device) {
+        // abstract implementation is empty, subclasses may override
+    }
+
     @Test
     public void testInitialize() {
         ThingStatusInfo expectedStatusInfo = new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
index 923776f8639f2c0ae9582e589fc2d67e3b7b91f9..49be02c8286b025615e5b93084343d8df5485c09 100644 (file)
  */
 package org.openhab.binding.boschshc.internal.devices;
 
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeoutException;
 
-import javax.measure.quantity.Energy;
-import javax.measure.quantity.Power;
-
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
-import org.openhab.binding.boschshc.internal.services.powermeter.dto.PowerMeterServiceState;
 import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState;
 import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState;
 import org.openhab.core.library.types.OnOffType;
-import org.openhab.core.library.types.QuantityType;
-import org.openhab.core.library.unit.Units;
 import org.openhab.core.types.RefreshType;
 
 import com.google.gson.JsonElement;
@@ -52,10 +50,6 @@ public abstract class AbstractPowerSwitchHandlerTest<T extends AbstractPowerSwit
 
     private @Captor @NonNullByDefault({}) ArgumentCaptor<PowerSwitchServiceState> serviceStateCaptor;
 
-    private @Captor @NonNullByDefault({}) ArgumentCaptor<QuantityType<Power>> powerCaptor;
-
-    private @Captor @NonNullByDefault({}) ArgumentCaptor<QuantityType<Energy>> energyCaptor;
-
     @BeforeEach
     @Override
     public void beforeEach() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
@@ -65,12 +59,6 @@ public abstract class AbstractPowerSwitchHandlerTest<T extends AbstractPowerSwit
         powerSwitchServiceState.switchState = PowerSwitchState.ON;
         lenient().when(bridgeHandler.getState(anyString(), eq("PowerSwitch"), same(PowerSwitchServiceState.class)))
                 .thenReturn(powerSwitchServiceState);
-
-        PowerMeterServiceState powerMeterServiceState = new PowerMeterServiceState();
-        powerMeterServiceState.powerConsumption = 12.34d;
-        powerMeterServiceState.energyConsumption = 56.78d;
-        lenient().when(bridgeHandler.getState(anyString(), eq("PowerMeter"), same(PowerMeterServiceState.class)))
-                .thenReturn(powerMeterServiceState);
     }
 
     @Test
@@ -101,47 +89,9 @@ public abstract class AbstractPowerSwitchHandlerTest<T extends AbstractPowerSwit
         verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.OFF);
     }
 
-    @Test
-    public void testUpdateChannelPowerMeterServiceState() {
-        JsonElement jsonObject = JsonParser.parseString("""
-                {
-                  "@type": "powerMeterState",
-                  "powerConsumption": "23",
-                  "energyConsumption": 42
-                }\
-                """);
-        getFixture().processUpdate("PowerMeter", jsonObject);
-
-        verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION)),
-                powerCaptor.capture());
-        QuantityType<Power> powerValue = powerCaptor.getValue();
-        assertEquals(23, powerValue.intValue());
-
-        verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION)),
-                energyCaptor.capture());
-        QuantityType<Energy> energyValue = energyCaptor.getValue();
-        assertEquals(42, energyValue.intValue());
-    }
-
     @Test
     public void testHandleCommandRefreshPowerSwitchChannel() {
         getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), RefreshType.REFRESH);
         verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.ON);
     }
-
-    @Test
-    public void testHandleCommandRefreshPowerConsumptionChannel() {
-        getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION),
-                RefreshType.REFRESH);
-        verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION),
-                new QuantityType<>(12.34d, Units.WATT));
-    }
-
-    @Test
-    public void testHandleCommandRefreshEnergyConsumptionChannel() {
-        getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION),
-                RefreshType.REFRESH);
-        verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION),
-                new QuantityType<>(56.78d, Units.WATT_HOUR));
-    }
 }
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerWithPowerMeterTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerWithPowerMeterTest.java
new file mode 100644 (file)
index 0000000..3ce2da1
--- /dev/null
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2010-2024 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.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.verify;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import javax.measure.quantity.Energy;
+import javax.measure.quantity.Power;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.openhab.binding.boschshc.internal.services.powermeter.dto.PowerMeterServiceState;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.types.RefreshType;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+
+/**
+ * Abstract unit test implementation for power switch handler with power meter support.
+ * 
+ * @author David Pace - Initial contribution
+ *
+ * @param <T> type of the handler to be tested
+ */
+@NonNullByDefault
+public abstract class AbstractPowerSwitchHandlerWithPowerMeterTest<T extends AbstractPowerSwitchHandlerWithPowerMeter>
+        extends AbstractPowerSwitchHandlerTest<T> {
+
+    private @Captor @NonNullByDefault({}) ArgumentCaptor<QuantityType<Power>> powerCaptor;
+
+    private @Captor @NonNullByDefault({}) ArgumentCaptor<QuantityType<Energy>> energyCaptor;
+
+    @BeforeEach
+    public void beforeEach() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+        super.beforeEach();
+
+        PowerMeterServiceState powerMeterServiceState = new PowerMeterServiceState();
+        powerMeterServiceState.powerConsumption = 12.34d;
+        powerMeterServiceState.energyConsumption = 56.78d;
+        lenient().when(bridgeHandler.getState(anyString(), eq("PowerMeter"), same(PowerMeterServiceState.class)))
+                .thenReturn(powerMeterServiceState);
+    }
+
+    @Test
+    public void testUpdateChannelPowerMeterServiceState() {
+        JsonElement jsonObject = JsonParser.parseString("""
+                {
+                  "@type": "powerMeterState",
+                  "powerConsumption": "23",
+                  "energyConsumption": 42
+                }\
+                """);
+        getFixture().processUpdate("PowerMeter", jsonObject);
+
+        verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION)),
+                powerCaptor.capture());
+        QuantityType<Power> powerValue = powerCaptor.getValue();
+        assertEquals(23, powerValue.intValue());
+
+        verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION)),
+                energyCaptor.capture());
+        QuantityType<Energy> energyValue = energyCaptor.getValue();
+        assertEquals(42, energyValue.intValue());
+    }
+
+    @Test
+    public void testHandleCommandRefreshPowerConsumptionChannel() {
+        getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION),
+                RefreshType.REFRESH);
+        verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION),
+                new QuantityType<>(12.34d, Units.WATT));
+    }
+
+    @Test
+    public void testHandleCommandRefreshEnergyConsumptionChannel() {
+        getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION),
+                RefreshType.REFRESH);
+        verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION),
+                new QuantityType<>(56.78d, Units.WATT_HOUR));
+    }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/BoschDeviceIdUtilsTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/BoschDeviceIdUtilsTest.java
new file mode 100644 (file)
index 0000000..086f10f
--- /dev/null
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2010-2024 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.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for {@link BoschDeviceIdUtils}.
+ * 
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+class BoschDeviceIdUtilsTest {
+
+    @Test
+    void testIsChildDeviceId() {
+        assertFalse(BoschDeviceIdUtils.isChildDeviceId("hdm:ZigBee:70ac08fffe5294ea"));
+        assertTrue(BoschDeviceIdUtils.isChildDeviceId("hdm:ZigBee:70ac08fffe5294ea#3"));
+    }
+
+    @Test
+    void testGetParentDeviceId() {
+        assertEquals("hdm:ZigBee:70ac08fffe5294ea",
+                BoschDeviceIdUtils.getParentDeviceId("hdm:ZigBee:70ac08fffe5294ea#3"));
+        assertEquals("hdm:ZigBee:70ac08fffe5294ea",
+                BoschDeviceIdUtils.getParentDeviceId("hdm:ZigBee:70ac08fffe5294ea"));
+    }
+}
index 9d994e1996ea55d23dd52ee158c77c3561ec1dd0..821bb419ddfcbc5a6e21b1979b1732c8d0883299 100644 (file)
@@ -101,6 +101,13 @@ class BoschHttpClientTest {
                 httpClient.getServiceStateUrl("testService", "testDevice", UserStateServiceState.class));
     }
 
+    @Test
+    void getServiceStateUrlForChildDevice() {
+        assertEquals(
+                "https://127.0.0.1:8444/smarthome/devices/hdm:ZigBee:70ac08fffe5294ea%233/services/PowerSwitch/state",
+                httpClient.getServiceStateUrl("PowerSwitch", "hdm:ZigBee:70ac08fffe5294ea#3"));
+    }
+
     @Test
     void isAccessPossible() throws InterruptedException {
         assertFalse(httpClient.isAccessPossible());
index 38b07c34d5284fe313ec25ac5ac907408d73da08..4bfde88740be8326f3e439d3d81a85d7104ec0f0 100644 (file)
  */
 package org.openhab.binding.boschshc.internal.devices.bridge;
 
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.*;
-import static org.mockito.Mockito.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.contains;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
 
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -38,10 +51,12 @@ import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentCaptor;
+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.DeviceServiceData;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceTest;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Faults;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.SubscribeResult;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState;
 import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedStateTest;
@@ -62,6 +77,9 @@ import org.openhab.core.thing.ThingStatusDetail;
 import org.openhab.core.thing.binding.ThingHandlerCallback;
 import org.openhab.core.thing.binding.builder.ThingStatusInfoBuilder;
 
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+
 /**
  * Unit tests for the {@link BridgeHandler}.
  *
@@ -77,6 +95,11 @@ class BridgeHandlerTest {
 
     private @NonNullByDefault({}) ThingHandlerCallback thingHandlerCallback;
 
+    /**
+     * A mocked bridge instance
+     */
+    private @NonNullByDefault({}) Bridge thing;
+
     @BeforeAll
     static void beforeAll() throws IOException {
         Path mavenTargetFolder = Paths.get("target");
@@ -102,7 +125,7 @@ class BridgeHandlerTest {
         properties.put("password", "test");
         bridgeConfiguration.setProperties(properties);
 
-        Thing thing = mock(Bridge.class);
+        thing = mock(Bridge.class);
         when(thing.getConfiguration()).thenReturn(bridgeConfiguration);
         // this calls initialize() as well
         fixture.thingUpdated(thing);
@@ -502,4 +525,129 @@ class BridgeHandlerTest {
     void afterEach() throws Exception {
         fixture.dispose();
     }
+
+    @Test
+    void handleLongPollResultNoDeviceId() {
+        List<Thing> things = new ArrayList<Thing>();
+        when(thing.getThings()).thenReturn(things);
+
+        Thing thing = mock(Thing.class);
+        things.add(thing);
+
+        BoschSHCHandler thingHandler = mock(BoschSHCHandler.class);
+        when(thing.getHandler()).thenReturn(thingHandler);
+
+        String json = """
+                {
+                  "result": [{
+                    "path": "/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch",
+                    "@type": "DeviceServiceData",
+                    "id": "PowerSwitch",
+                    "state": {
+                       "@type": "powerSwitchState",
+                       "switchState": "ON"
+                    },
+                    "deviceId": "hdm:HomeMaticIP:3014F711A0001916D859A8A9"
+                  }],
+                  "jsonrpc": "2.0"
+                }
+                """;
+        LongPollResult longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, LongPollResult.class);
+        assertNotNull(longPollResult);
+
+        fixture.handleLongPollResult(longPollResult);
+
+        verify(thingHandler).getBoschID();
+        verifyNoMoreInteractions(thingHandler);
+    }
+
+    @Test
+    void handleLongPollResult() {
+        List<Thing> things = new ArrayList<Thing>();
+        when(thing.getThings()).thenReturn(things);
+
+        Thing thing = mock(Thing.class);
+        things.add(thing);
+
+        BoschSHCHandler thingHandler = mock(BoschSHCHandler.class);
+        when(thing.getHandler()).thenReturn(thingHandler);
+
+        when(thingHandler.getBoschID()).thenReturn("hdm:HomeMaticIP:3014F711A0001916D859A8A9");
+
+        String json = """
+                {
+                  "result": [{
+                    "path": "/devices/hdm:HomeMaticIP:3014F711A0001916D859A8A9/services/PowerSwitch",
+                    "@type": "DeviceServiceData",
+                    "id": "PowerSwitch",
+                    "state": {
+                       "@type": "powerSwitchState",
+                       "switchState": "ON"
+                    },
+                    "deviceId": "hdm:HomeMaticIP:3014F711A0001916D859A8A9"
+                  }],
+                  "jsonrpc": "2.0"
+                }
+                """;
+        LongPollResult longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, LongPollResult.class);
+        assertNotNull(longPollResult);
+
+        fixture.handleLongPollResult(longPollResult);
+
+        verify(thingHandler).getBoschID();
+
+        JsonElement expectedState = JsonParser.parseString("""
+                {
+                    "@type": "powerSwitchState",
+                    "switchState": "ON"
+                }
+                """);
+
+        verify(thingHandler).processUpdate("PowerSwitch", expectedState);
+    }
+
+    @Test
+    void handleLongPollResultHandleChildUpdate() {
+        List<Thing> things = new ArrayList<Thing>();
+        when(thing.getThings()).thenReturn(things);
+
+        Thing thing = mock(Thing.class);
+        things.add(thing);
+
+        BoschSHCHandler thingHandler = mock(BoschSHCHandler.class);
+        when(thing.getHandler()).thenReturn(thingHandler);
+
+        when(thingHandler.getBoschID()).thenReturn("hdm:ZigBee:70ac08fffefead2d");
+
+        String json = """
+                {
+                  "result": [{
+                    "path": "/devices/hdm:ZigBee:70ac08fffefead2d#3/services/PowerSwitch",
+                    "@type": "DeviceServiceData",
+                    "id": "PowerSwitch",
+                    "state": {
+                       "@type": "powerSwitchState",
+                       "switchState": "ON"
+                    },
+                    "deviceId": "hdm:ZigBee:70ac08fffefead2d#3"
+                  }],
+                  "jsonrpc": "2.0"
+                }
+                """;
+        LongPollResult longPollResult = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(json, LongPollResult.class);
+        assertNotNull(longPollResult);
+
+        fixture.handleLongPollResult(longPollResult);
+
+        verify(thingHandler).getBoschID();
+
+        JsonElement expectedState = JsonParser.parseString("""
+                {
+                    "@type": "powerSwitchState",
+                    "switchState": "ON"
+                }
+                """);
+
+        verify(thingHandler).processChildUpdate("hdm:ZigBee:70ac08fffefead2d#3", "PowerSwitch", expectedState);
+    }
 }
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/LightControl2HandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/LightControl2HandlerTest.java
new file mode 100644 (file)
index 0000000..5b56130
--- /dev/null
@@ -0,0 +1,284 @@
+/**
+ * Copyright (c) 2010-2024 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.lightcontrol;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import javax.measure.quantity.Energy;
+import javax.measure.quantity.Power;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.openhab.binding.boschshc.internal.devices.AbstractBoschSHCDeviceHandlerTest;
+import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
+import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.openhab.binding.boschshc.internal.services.childprotection.dto.ChildProtectionServiceState;
+import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchService;
+import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState;
+import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState;
+import org.openhab.core.library.types.DecimalType;
+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.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.ThingTypeUID;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+
+/**
+ * Unit tests for {@link LightControl2Handler}.
+ * 
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+class LightControl2HandlerTest extends AbstractBoschSHCDeviceHandlerTest<LightControl2Handler> {
+
+    private static final String CHILD_DEVICE_ID_1 = "hdm:ZigBee:70ac08fffefead2d#2";
+    private static final String CHILD_DEVICE_ID_2 = "hdm:ZigBee:70ac08fffefead2d#3";
+
+    private @Captor @NonNullByDefault({}) ArgumentCaptor<QuantityType<Power>> powerCaptor;
+
+    private @Captor @NonNullByDefault({}) ArgumentCaptor<QuantityType<Energy>> energyCaptor;
+
+    private @Captor @NonNullByDefault({}) ArgumentCaptor<ChildProtectionServiceState> childProtectionServiceStateCaptor;
+
+    private @Captor @NonNullByDefault({}) ArgumentCaptor<PowerSwitchServiceState> powerSwitchStateCaptor;
+
+    @Override
+    protected LightControl2Handler createFixture() {
+        return new LightControl2Handler(getThing());
+    }
+
+    @Override
+    protected ThingTypeUID getThingTypeUID() {
+        return BoschSHCBindingConstants.THING_TYPE_LIGHT_CONTROL_2;
+    }
+
+    @Override
+    protected String getDeviceID() {
+        return "hdm:ZigBee:70ac08fcfefa5197";
+    }
+
+    @Override
+    protected void configureDevice(Device device) {
+        super.configureDevice(device);
+
+        // order is reversed to test child ID sorting during initialization
+        device.childDeviceIds = List.of(CHILD_DEVICE_ID_2, CHILD_DEVICE_ID_1);
+    }
+
+    @Test
+    void testUpdateChannelCommunicationQualityService() {
+        String json = """
+                {
+                    "@type": "communicationQualityState",
+                    "quality": "UNKNOWN"
+                }
+                """;
+        JsonElement jsonObject = JsonParser.parseString(json);
+
+        getFixture().processUpdate("CommunicationQuality", jsonObject);
+        verify(getCallback()).stateUpdated(
+                new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_SIGNAL_STRENGTH),
+                new DecimalType(0));
+
+        json = """
+                {
+                    "@type": "communicationQualityState",
+                    "quality": "GOOD"
+                }
+                """;
+        jsonObject = JsonParser.parseString(json);
+
+        getFixture().processUpdate("CommunicationQuality", jsonObject);
+        verify(getCallback()).stateUpdated(
+                new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_SIGNAL_STRENGTH),
+                new DecimalType(4));
+    }
+
+    @Test
+    void testUpdateChannelPowerMeterServiceState() {
+        JsonElement jsonObject = JsonParser.parseString("""
+                {
+                  "@type": "powerMeterState",
+                  "powerConsumption": "23",
+                  "energyConsumption": 42
+                }\
+                """);
+        getFixture().processUpdate("PowerMeter", jsonObject);
+
+        verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION)),
+                powerCaptor.capture());
+        QuantityType<Power> powerValue = powerCaptor.getValue();
+        assertEquals(23, powerValue.intValue());
+
+        verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION)),
+                energyCaptor.capture());
+        QuantityType<Energy> energyValue = energyCaptor.getValue();
+        assertEquals(42, energyValue.intValue());
+    }
+
+    @Test
+    void testHandleCommandPowerSwitchChannelChildDevice1()
+            throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+        getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH_1), OnOffType.ON);
+        verify(getBridgeHandler()).putState(eq(CHILD_DEVICE_ID_1), eq("PowerSwitch"), powerSwitchStateCaptor.capture());
+        PowerSwitchServiceState state = powerSwitchStateCaptor.getValue();
+        assertSame(PowerSwitchState.ON, state.switchState);
+
+        getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH_1), OnOffType.OFF);
+        verify(getBridgeHandler(), times(2)).putState(eq(CHILD_DEVICE_ID_1), eq("PowerSwitch"),
+                powerSwitchStateCaptor.capture());
+        state = powerSwitchStateCaptor.getValue();
+        assertSame(PowerSwitchState.OFF, state.switchState);
+    }
+
+    @Test
+    void testHandleCommandPowerSwitchChannelChildDevice2()
+            throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+        getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH_2), OnOffType.ON);
+        verify(getBridgeHandler()).putState(eq(CHILD_DEVICE_ID_2), eq("PowerSwitch"), powerSwitchStateCaptor.capture());
+        PowerSwitchServiceState state = powerSwitchStateCaptor.getValue();
+        assertSame(PowerSwitchState.ON, state.switchState);
+
+        getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH_2), OnOffType.OFF);
+        verify(getBridgeHandler(), times(2)).putState(eq(CHILD_DEVICE_ID_2), eq("PowerSwitch"),
+                powerSwitchStateCaptor.capture());
+        state = powerSwitchStateCaptor.getValue();
+        assertSame(PowerSwitchState.OFF, state.switchState);
+    }
+
+    @Test
+    void testUpdateChannelPowerSwitchStateChildDevice1() {
+        JsonElement jsonObject = JsonParser
+                .parseString("{\n" + "  \"@type\": \"powerSwitchState\",\n" + "  \"switchState\": \"ON\"\n" + "}");
+        getFixture().processChildUpdate(CHILD_DEVICE_ID_1, PowerSwitchService.POWER_SWITCH_SERVICE_NAME, jsonObject);
+        verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH_1),
+                OnOffType.ON);
+
+        jsonObject = JsonParser
+                .parseString("{\n" + "  \"@type\": \"powerSwitchState\",\n" + "  \"switchState\": \"OFF\"\n" + "}");
+        getFixture().processChildUpdate(CHILD_DEVICE_ID_1, PowerSwitchService.POWER_SWITCH_SERVICE_NAME, jsonObject);
+        verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH_1),
+                OnOffType.OFF);
+    }
+
+    @Test
+    void testUpdateChannelPowerSwitchStateChildDevice2() {
+        JsonElement jsonObject = JsonParser
+                .parseString("{\n" + "  \"@type\": \"powerSwitchState\",\n" + "  \"switchState\": \"ON\"\n" + "}");
+        getFixture().processChildUpdate(CHILD_DEVICE_ID_2, PowerSwitchService.POWER_SWITCH_SERVICE_NAME, jsonObject);
+        verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH_2),
+                OnOffType.ON);
+
+        jsonObject = JsonParser
+                .parseString("{\n" + "  \"@type\": \"powerSwitchState\",\n" + "  \"switchState\": \"OFF\"\n" + "}");
+        getFixture().processChildUpdate(CHILD_DEVICE_ID_2, PowerSwitchService.POWER_SWITCH_SERVICE_NAME, jsonObject);
+        verify(getCallback()).stateUpdated(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH_2),
+                OnOffType.OFF);
+    }
+
+    @Test
+    void testUpdateChannelsChildProtectionServiceChildDevice1() {
+        String json = """
+                {
+                    "@type": "ChildProtectionState",
+                    "childLockActive": true
+                }
+                """;
+        JsonElement jsonObject = JsonParser.parseString(json);
+
+        getFixture().processChildUpdate(CHILD_DEVICE_ID_1, "ChildProtection", jsonObject);
+        verify(getCallback()).stateUpdated(
+                new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION_1), OnOffType.ON);
+    }
+
+    @Test
+    void testUpdateChannelsChildProtectionServiceChildDevice2() {
+        String json = """
+                {
+                    "@type": "ChildProtectionState",
+                    "childLockActive": true
+                }
+                """;
+        JsonElement jsonObject = JsonParser.parseString(json);
+
+        getFixture().processChildUpdate(CHILD_DEVICE_ID_2, "ChildProtection", jsonObject);
+        verify(getCallback()).stateUpdated(
+                new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION_2), OnOffType.ON);
+    }
+
+    @Test
+    void testHandleCommandChildProtectionServiceChildDevice1()
+            throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+        getFixture().handleCommand(
+                new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION_1), OnOffType.ON);
+        verify(getBridgeHandler()).putState(eq(CHILD_DEVICE_ID_1), eq("ChildProtection"),
+                childProtectionServiceStateCaptor.capture());
+        ChildProtectionServiceState state = childProtectionServiceStateCaptor.getValue();
+        assertTrue(state.childLockActive);
+    }
+
+    @Test
+    void testHandleCommandChildProtectionServiceChildDevice2()
+            throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+        getFixture().handleCommand(
+                new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION_2), OnOffType.ON);
+        verify(getBridgeHandler()).putState(eq(CHILD_DEVICE_ID_2), eq("ChildProtection"),
+                childProtectionServiceStateCaptor.capture());
+        ChildProtectionServiceState state = childProtectionServiceStateCaptor.getValue();
+        assertTrue(state.childLockActive);
+    }
+
+    @Test
+    void testInitializeNoChildIDsInDeviceInfo() {
+        getDevice().childDeviceIds = null;
+
+        getFixture().initialize();
+
+        verify(getCallback()).statusUpdated(same(getThing()),
+                argThat(status -> status.getStatus().equals(ThingStatus.OFFLINE)
+                        && status.getStatusDetail().equals(ThingStatusDetail.CONFIGURATION_ERROR)));
+    }
+
+    @Test
+    void testInitializeServicesNoChildIDsInDeviceInfo() {
+        getDevice().childDeviceIds = null;
+
+        LightControl2Handler lFixture = new LightControl2Handler(getThing());
+        lFixture.setCallback(getCallback());
+
+        // this call will return before reaching initializeServices()
+        lFixture.initialize();
+
+        assertThrows(BoschSHCException.class, () -> lFixture.initializeServices());
+    }
+}
index 77e715419d3d59faa06ca5cf6a6ad23f50cfdd10..c1157311f6ecfc3ad9fc29678cc840ef8a65168a 100644 (file)
@@ -13,7 +13,7 @@
 package org.openhab.binding.boschshc.internal.devices.lightcontrol;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandlerTest;
+import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandlerWithPowerMeterTest;
 import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
 import org.openhab.core.thing.ThingTypeUID;
 
@@ -24,7 +24,7 @@ import org.openhab.core.thing.ThingTypeUID;
  *
  */
 @NonNullByDefault
-public class LightControlHandlerTest extends AbstractPowerSwitchHandlerTest<LightControlHandler> {
+class LightControlHandlerTest extends AbstractPowerSwitchHandlerWithPowerMeterTest<LightControlHandler> {
 
     @Override
     protected ThingTypeUID getThingTypeUID() {
index e2864e5e1a1cd42fd9837edab66281789b0dadc2..74fb3b2ead78f70f1fce48b63d076e5f8cc35ab4 100644 (file)
@@ -13,7 +13,7 @@
 package org.openhab.binding.boschshc.internal.devices.plug;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandlerTest;
+import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandlerWithPowerMeterTest;
 import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
 import org.openhab.core.thing.ThingTypeUID;
 
@@ -24,7 +24,7 @@ import org.openhab.core.thing.ThingTypeUID;
  *
  */
 @NonNullByDefault
-public class PlugHandlerTest extends AbstractPowerSwitchHandlerTest<PlugHandler> {
+class PlugHandlerTest extends AbstractPowerSwitchHandlerWithPowerMeterTest<PlugHandler> {
 
     @Override
     protected PlugHandler createFixture() {
diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/shuttercontrol/ShutterControl2HandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/shuttercontrol/ShutterControl2HandlerTest.java
new file mode 100644 (file)
index 0000000..b5a2192
--- /dev/null
@@ -0,0 +1,143 @@
+/**
+ * Copyright (c) 2010-2024 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.shuttercontrol;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import javax.measure.quantity.Energy;
+import javax.measure.quantity.Power;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
+import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
+import org.openhab.binding.boschshc.internal.services.childprotection.dto.ChildProtectionServiceState;
+import org.openhab.core.library.types.DecimalType;
+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 com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+
+/**
+ * Unit tests for {@link ShutterControl2Handler}
+ * 
+ * @author David Pace - Initial contribution
+ *
+ */
+@NonNullByDefault
+class ShutterControl2HandlerTest extends ShutterControlHandlerTest {
+
+    private @Captor @NonNullByDefault({}) ArgumentCaptor<QuantityType<Power>> powerCaptor;
+
+    private @Captor @NonNullByDefault({}) ArgumentCaptor<QuantityType<Energy>> energyCaptor;
+
+    private @Captor @NonNullByDefault({}) ArgumentCaptor<ChildProtectionServiceState> childProtectionServiceStateCaptor;
+
+    @Override
+    protected ShutterControlHandler createFixture() {
+        return new ShutterControl2Handler(getThing());
+    }
+
+    @Override
+    protected ThingTypeUID getThingTypeUID() {
+        return BoschSHCBindingConstants.THING_TYPE_SHUTTER_CONTROL_2;
+    }
+
+    @Test
+    void testUpdateChannelsCommunicationQualityService() {
+        String json = """
+                {
+                    "@type": "communicationQualityState",
+                    "quality": "UNKNOWN"
+                }
+                """;
+        JsonElement jsonObject = JsonParser.parseString(json);
+
+        getFixture().processUpdate("CommunicationQuality", jsonObject);
+        verify(getCallback()).stateUpdated(
+                new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_SIGNAL_STRENGTH),
+                new DecimalType(0));
+
+        json = """
+                {
+                    "@type": "communicationQualityState",
+                    "quality": "GOOD"
+                }
+                """;
+        jsonObject = JsonParser.parseString(json);
+
+        getFixture().processUpdate("CommunicationQuality", jsonObject);
+        verify(getCallback()).stateUpdated(
+                new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_SIGNAL_STRENGTH),
+                new DecimalType(4));
+    }
+
+    @Test
+    void testUpdateChannelsChildProtectionService() {
+        String json = """
+                {
+                    "@type": "ChildProtectionState",
+                    "childLockActive": true
+                }
+                """;
+        JsonElement jsonObject = JsonParser.parseString(json);
+
+        getFixture().processUpdate("ChildProtection", jsonObject);
+        verify(getCallback()).stateUpdated(
+                new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION), OnOffType.ON);
+    }
+
+    @Test
+    void testUpdateChannelPowerMeterServiceState() {
+        JsonElement jsonObject = JsonParser.parseString("""
+                {
+                  "@type": "powerMeterState",
+                  "powerConsumption": "23",
+                  "energyConsumption": 42
+                }\
+                """);
+        getFixture().processUpdate("PowerMeter", jsonObject);
+
+        verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION)),
+                powerCaptor.capture());
+        QuantityType<Power> powerValue = powerCaptor.getValue();
+        assertEquals(23, powerValue.intValue());
+
+        verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION)),
+                energyCaptor.capture());
+        QuantityType<Energy> energyValue = energyCaptor.getValue();
+        assertEquals(42, energyValue.intValue());
+    }
+
+    @Test
+    void testHandleCommandChildProtection()
+            throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+        getFixture().handleCommand(
+                new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION), OnOffType.ON);
+        verify(getBridgeHandler()).putState(eq(getDeviceID()), eq("ChildProtection"),
+                childProtectionServiceStateCaptor.capture());
+        ChildProtectionServiceState state = childProtectionServiceStateCaptor.getValue();
+        assertTrue(state.childLockActive);
+    }
+}
index 7521910d7b1a5a633974db1715d2e14cf87544f8..92ce5dd4a1310f1a4764e2ade67195cba4a51c2d 100644 (file)
 package org.openhab.binding.boschshc.internal.discovery;
 
 import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.jupiter.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import java.util.ArrayList;
 import java.util.UUID;
@@ -41,7 +47,7 @@ import org.openhab.core.thing.Bridge;
 import org.openhab.core.thing.ThingUID;
 
 /**
- * ThingDiscoveryService Tester.
+ * Unit tests for {@link ThingDiscoveryService}.
  *
  * @author Gerd Zanker - Initial contribution
  */
@@ -261,4 +267,12 @@ class ThingDiscoveryServiceTest {
         // two calls for the two devices expected
         verify(discoveryListener, times(2)).thingDiscovered(any(), any());
     }
+
+    @Test
+    void getThingTypeUIDLightControl2ChildDevice() {
+        Device device = new Device();
+        device.deviceModel = ThingDiscoveryService.DEVICE_MODEL_LIGHT_CONTROL_CHILD_DEVICE;
+
+        assertThat(fixture.getThingTypeUID(device), is(nullValue()));
+    }
 }