From: David Pace Date: Sun, 31 Mar 2024 08:36:43 +0000 (+0200) Subject: [boschshc] Add support for Light/Shutter Control II (#16400) X-Git-Url: https://git.basschouten.com/?a=commitdiff_plain;h=b77172c6bb984517c465417b5b09879e6d0e0a41;p=openhab-addons.git [boschshc] Add support for Light/Shutter Control II (#16400) * [boschshc] Add support for Shutter Control II (#14562) * add new channel type for child protection Signed-off-by: David Pace --- diff --git a/bundles/org.openhab.binding.boschshc/README.md b/bundles/org.openhab.binding.boschshc/README.md index 60b0e8e6db..68a0ff5a24 100644 --- a/bundles/org.openhab.binding.boschshc/README.md +++ b/bundles/org.openhab.binding.boschshc/README.md @@ -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 | ☐ | Indicates whether the device is currently bypassed. Possible values are `ON`,`OFF` and `UNDEF` if the bypass state cannot be determined. | | signal-strength | Number | ☐ | 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 | ☐ | 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 | ☐ | Current power consumption (W) of the device. | +| energy-consumption | Number:Energy | ☐ | Cumulated energy consumption (Wh) of the device. | +| power-switch-1 | Switch | ☑ | Switches the light on or off (circuit 1). | +| child-protection-1 | Switch | ☑ | Indicates whether the child protection is active (circuit 1). | +| power-switch-2 | Switch | ☑ | Switches the light on or off (circuit 2). | +| child-protection-2 | Switch | ☑ | 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 | ☑ | 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 | ☑ | Current open ratio (0 to 100, Step 0.5). | +| signal-strength | Number | ☐ | Communication quality between the device and the Smart Home Controller. Possible values range between 0 (unknown) and 4 (best signal strength). | +| child-protection | Switch | ☑ | Indicates whether the child protection is active. | +| power-consumption | Number:Power | ☐ | Current power consumption (W) of the device. | +| energy-consumption | Number:Energy | ☐ | Cumulated energy consumption (Wh) of the device. | + ### Thermostat Radiator thermostat diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtension.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtension.java index 822146bff4..36354778c4 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtension.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtension.java @@ -79,10 +79,10 @@ public class BoschShcCommandExtension extends AbstractConsoleCommandExtension im */ List 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 diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandler.java index d02a8d42fb..81ecfbf27f 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandler.java @@ -12,20 +12,16 @@ */ 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 index 0000000000..12407e9d46 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerWithPowerMeter.java @@ -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}. + *

+ * 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 index 0000000000..b489cc5fcc --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschDeviceIdUtils.java @@ -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. + *

+ * Example for a parent device ID: + * + *

+     * hdm:ZigBee:70ac08fffefead2d
+     * 
+ * + * Example for a child device ID: + * + *
+     * hdm:ZigBee:70ac08fffefead2d#2
+     * 
+ * + * @param deviceId the Bosch device ID to check + * @return true if the device ID contains a hash character, false 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); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java index 4d6676c767..ed849aff4e 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java @@ -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 diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCDeviceHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCDeviceHandler.java index 99bf97414c..6791b19c92 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCDeviceHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCDeviceHandler.java @@ -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 /devices/{deviceId}. + * + * @param deviceInfo the device info obtained from the controller, guaranteed to be non-null + * @return true if the device info is valid and the initialization should proceed, false + * otherwise + */ + protected boolean processDeviceInfo(Device deviceInfo) { + return true; + } + + /** + * Attempts to obtain information about the device with the specified ID via a REST call. + *

+ * If the REST call is successful, the device ID is considered to be valid and the resulting {@link Device} object + * is returned. + *

+ * If the device ID is not configured/empty or the REST call is not successful, the device ID is considered invalid + * and null is returned. + * + * @param deviceId the device ID to check + * @return the {@link Device} info object if the REST call was successful, null 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; } /** diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java index 6ccc016e1e..76cdf6dd0a 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java @@ -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 deviceService : this.services) { BoschSHCService 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 } /** diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java index c512d10905..94c9bfad53 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java @@ -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) { diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java index 945b3889c1..c498c3b933 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java @@ -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. + *

+ * In particular, hash characters in child device IDs must be escaped with %23. + *

+ * Invalid example: + * + *

+     * https://host:port/devices/hdm:ZigBee:70ac08fffe5294ea#3/services/PowerSwitch/state
+     * 
+ * + * Valid example: + * + *
+     * https://host:port/devices/hdm:ZigBee:70ac08fffe5294ea%233/services/PowerSwitch/state
+     * 
+ * + * @param url the URL to be escaped + * @return the escaped URL + */ + private String escapeURL(String url) { + return url.replace("#", "%23"); } /** diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java index bfcedd426e..1bae6c86d1 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java @@ -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 true if the handler matched and was notified, false 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 true if the given handler was the corresponding parent handler and was notified, + * false 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. * diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Device.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Device.java index 2dbb3128a8..9fd7e1caec 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Device.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Device.java @@ -18,23 +18,25 @@ import com.google.gson.annotations.SerializedName; /** * Represents a single devices connected to the Bosch Smart Home Controller. - * - * Example from Json: - * + *

+ * Example JSON: + * + *

  * {
- * "@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": []
  * }
+ * 
* * @author Stefan Kästle - Initial contribution */ diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResult.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResult.java index a2deba85e0..58bfd2f55e 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResult.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResult.java @@ -18,25 +18,30 @@ import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState; /** * Response of the Controller for a Long Poll API call. + *

+ * Example 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"
+ * }
+ * 
* * @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 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 index 0000000000..4ce129e9a7 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/LightControl2Handler.java @@ -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. + *

+ * 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 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); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/LightControlHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/LightControlHandler.java index 89bdcb01e7..c3bbdbe85b 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/LightControlHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/LightControlHandler.java @@ -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); diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/plug/PlugHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/plug/PlugHandler.java index 008507d196..5f11e8b17f 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/plug/PlugHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/plug/PlugHandler.java @@ -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 index 0000000000..4dea74ef5f --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/shuttercontrol/ShutterControl2Handler.java @@ -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. + *

+ * 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); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryService.java index 4e0246e778..e912cbd07d 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryService.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryService.java @@ -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 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. + *

+ * 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 index 0000000000..17e4b31959 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/childprotection/ChildProtectionService.java @@ -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 { + + 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 index 0000000000..0d4bbe6349 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/childprotection/dto/ChildProtectionServiceState.java @@ -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; +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/PowerSwitchService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/PowerSwitchService.java index 95e4d22054..7a3266d438 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/PowerSwitchService.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/powerswitch/PowerSwitchService.java @@ -24,7 +24,9 @@ import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitc @NonNullByDefault public class PowerSwitchService extends BoschSHCService { + public static final String POWER_SWITCH_SERVICE_NAME = "PowerSwitch"; + public PowerSwitchService() { - super("PowerSwitch", PowerSwitchServiceState.class); + super(POWER_SWITCH_SERVICE_NAME, PowerSwitchServiceState.class); } } diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/config/configs.xml b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/config/configs.xml index dbfa963a8f..561bafa269 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/config/configs.xml +++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/config/configs.xml @@ -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"> + network-address @@ -15,16 +16,19 @@ The system password of the Bosch Smart Home Controller necessary for pairing. + Unique ID of the device. + Unique ID of the state. + diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties index b2a7349c9e..8ce0444676 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties +++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties @@ -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. diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml index 4aea5e096a..05fb5e87f7 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml @@ -163,6 +163,48 @@ + + + + + + + Second generation shutter control. + + + + + + + + + + + + + + + + + + + + Advanced light control with two switch circuits. + + + + + + + + + + + + + + + @@ -703,4 +745,10 @@ + + Switch + + Enables or disables the child protection on the device. + + diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCDeviceHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCDeviceHandlerTest.java index 4eae021579..335af59e3d 100644 --- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCDeviceHandlerTest.java +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCDeviceHandlerTest.java @@ -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 extends AbstractBoschSHCHandlerTest { + @Override + protected void configureDevice(Device device) { + super.configureDevice(device); + + device.id = getDeviceID(); + } + @Override protected Configuration getConfiguration() { Configuration configuration = super.getConfiguration(); diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCHandlerTest.java index 4c35a1c2e5..c8efb9c9b3 100644 --- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCHandlerTest.java +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCHandlerTest.java @@ -12,8 +12,13 @@ */ 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 { private @Mock @NonNullByDefault({}) ThingHandlerCallback callback; + private @NonNullByDefault({}) Device device; + protected AbstractBoschSHCHandlerTest() { this.fixture = createFixture(); } @@ -72,6 +80,10 @@ public abstract class AbstractBoschSHCHandlerTest { 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 { 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); diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerTest.java index 923776f863..49be02c828 100644 --- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerTest.java +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerTest.java @@ -12,28 +12,26 @@ */ 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 serviceStateCaptor; - private @Captor @NonNullByDefault({}) ArgumentCaptor> powerCaptor; - - private @Captor @NonNullByDefault({}) ArgumentCaptor> energyCaptor; - @BeforeEach @Override public void beforeEach() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { @@ -65,12 +59,6 @@ public abstract class AbstractPowerSwitchHandlerTest powerValue = powerCaptor.getValue(); - assertEquals(23, powerValue.intValue()); - - verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION)), - energyCaptor.capture()); - QuantityType 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 index 0000000000..3ce2da1283 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerWithPowerMeterTest.java @@ -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 type of the handler to be tested + */ +@NonNullByDefault +public abstract class AbstractPowerSwitchHandlerWithPowerMeterTest + extends AbstractPowerSwitchHandlerTest { + + private @Captor @NonNullByDefault({}) ArgumentCaptor> powerCaptor; + + private @Captor @NonNullByDefault({}) ArgumentCaptor> 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 powerValue = powerCaptor.getValue(); + assertEquals(23, powerValue.intValue()); + + verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION)), + energyCaptor.capture()); + QuantityType 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 index 0000000000..086f10f7de --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/BoschDeviceIdUtilsTest.java @@ -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")); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java index 9d994e1996..821bb419dd 100644 --- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java @@ -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()); diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandlerTest.java index 38b07c34d5..4bfde88740 100644 --- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandlerTest.java +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandlerTest.java @@ -12,14 +12,27 @@ */ 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 things = new ArrayList(); + 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 things = new ArrayList(); + 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 things = new ArrayList(); + 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 index 0000000000..5b5613002e --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/LightControl2HandlerTest.java @@ -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 { + + 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> powerCaptor; + + private @Captor @NonNullByDefault({}) ArgumentCaptor> energyCaptor; + + private @Captor @NonNullByDefault({}) ArgumentCaptor childProtectionServiceStateCaptor; + + private @Captor @NonNullByDefault({}) ArgumentCaptor 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 powerValue = powerCaptor.getValue(); + assertEquals(23, powerValue.intValue()); + + verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION)), + energyCaptor.capture()); + QuantityType 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()); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/LightControlHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/LightControlHandlerTest.java index 77e715419d..c1157311f6 100644 --- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/LightControlHandlerTest.java +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/LightControlHandlerTest.java @@ -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 { +class LightControlHandlerTest extends AbstractPowerSwitchHandlerWithPowerMeterTest { @Override protected ThingTypeUID getThingTypeUID() { diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/plug/PlugHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/plug/PlugHandlerTest.java index e2864e5e1a..74fb3b2ead 100644 --- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/plug/PlugHandlerTest.java +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/plug/PlugHandlerTest.java @@ -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 { +class PlugHandlerTest extends AbstractPowerSwitchHandlerWithPowerMeterTest { @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 index 0000000000..b5a21927b9 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/shuttercontrol/ShutterControl2HandlerTest.java @@ -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> powerCaptor; + + private @Captor @NonNullByDefault({}) ArgumentCaptor> energyCaptor; + + private @Captor @NonNullByDefault({}) ArgumentCaptor 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 powerValue = powerCaptor.getValue(); + assertEquals(23, powerValue.intValue()); + + verify(getCallback()).stateUpdated(eq(getChannelUID(BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION)), + energyCaptor.capture()); + QuantityType 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); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryServiceTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryServiceTest.java index 7521910d7b..92ce5dd4a1 100644 --- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryServiceTest.java +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryServiceTest.java @@ -13,10 +13,16 @@ 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())); + } }