From df0bd2e291af7c5203577fb4d06bd21866cf6044 Mon Sep 17 00:00:00 2001 From: David Pace Date: Mon, 1 Apr 2024 12:26:03 +0200 Subject: [PATCH] [boschshc] Add support for Dimmer (#16501) Adds support for Bosch Smart Home Dimmer devices. Signed-off-by: David Pace Signed-off-by: Patrik Gfeller --- .../org.openhab.binding.boschshc/README.md | 13 ++ .../devices/BoschSHCBindingConstants.java | 1 + .../devices/BoschSHCHandlerFactory.java | 5 +- .../devices/lightcontrol/DimmerHandler.java | 98 +++++++++++ .../discovery/ThingDiscoveryService.java | 3 +- .../resources/OH-INF/i18n/boschshc.properties | 2 + .../resources/OH-INF/thing/thing-types.xml | 18 +++ .../lightcontrol/DimmerHandlerTest.java | 153 ++++++++++++++++++ 8 files changed, 291 insertions(+), 2 deletions(-) create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/DimmerHandler.java create mode 100644 bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/DimmerHandlerTest.java diff --git a/bundles/org.openhab.binding.boschshc/README.md b/bundles/org.openhab.binding.boschshc/README.md index 68a0ff5a24649..1cb21d4b48429 100644 --- a/bundles/org.openhab.binding.boschshc/README.md +++ b/bundles/org.openhab.binding.boschshc/README.md @@ -70,6 +70,19 @@ A compact smart plug with energy monitoring capabilities. | power-consumption | Number:Power | ☐ | Current power consumption (W) of the device. | | energy-consumption | Number:Energy | ☐ | Cumulated energy consumption (Wh) of the device. | +### Dimmer + +Smart dimmer capable of controlling any dimmable lamp. + +**Thing Type ID**: `dimmer` + +| Channel Type ID | Item Type | Writable | Description | +| ------------------ | ------------- | :------: | -------------------------------------------------------------- | +| power-switch | Switch | ☑ | Current state of the switch. | +| brightness | Dimmer | ☑ | Regulates the brightness on a percentage scale from 0 to 100%. | +| 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. | + ### Twinguard Smoke Detector The Twinguard smoke detector warns you in case of fire and constantly monitors the air. 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 ed849aff4e825..eb7f4dc685b14 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 @@ -54,6 +54,7 @@ public class BoschSHCBindingConstants { 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_DIMMER = new ThingTypeUID(BINDING_ID, "dimmer"); public static final ThingTypeUID THING_TYPE_USER_DEFINED_STATE = new ThingTypeUID(BINDING_ID, "user-defined-state"); 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 94c9bfad5301f..21e0b0c87f331 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 @@ -15,6 +15,7 @@ import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_CAMERA_360; import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_CAMERA_EYES; import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_CLIMATE_CONTROL; +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.THING_TYPE_DIMMER; 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; @@ -45,6 +46,7 @@ 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.DimmerHandler; 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; @@ -129,7 +131,8 @@ public ThingTypeHandlerMapping(ThingTypeUID thingTypeUID, Function new UniversalSwitch2Handler(thing, timeZoneProvider)), new ThingTypeHandlerMapping(THING_TYPE_SMOKE_DETECTOR_2, SmokeDetector2Handler::new), - new ThingTypeHandlerMapping(THING_TYPE_LIGHT_CONTROL_2, LightControl2Handler::new)); + new ThingTypeHandlerMapping(THING_TYPE_LIGHT_CONTROL_2, LightControl2Handler::new), + new ThingTypeHandlerMapping(THING_TYPE_DIMMER, DimmerHandler::new)); @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/DimmerHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/DimmerHandler.java new file mode 100644 index 0000000000000..13eeb30a267fb --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/DimmerHandler.java @@ -0,0 +1,98 @@ +/** + * 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_BRIGHTNESS; +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION; +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.AbstractPowerSwitchHandler; +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.multilevelswitch.MultiLevelSwitchService; +import org.openhab.binding.boschshc.internal.services.multilevelswitch.dto.MultiLevelSwitchServiceState; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; + +/** + * Handler for Bosch Smart Home dimmers. + * + * @author David Pace - Initial contribution + * + */ +@NonNullByDefault +public class DimmerHandler extends AbstractPowerSwitchHandler { + + private MultiLevelSwitchService multiLevelSwitchService; + private ChildProtectionService childProtectionService; + + public DimmerHandler(Thing thing) { + super(thing); + + this.multiLevelSwitchService = new MultiLevelSwitchService(); + this.childProtectionService = new ChildProtectionService(); + } + + @Override + protected void initializeServices() throws BoschSHCException { + super.initializeServices(); + + createService(CommunicationQualityService::new, this::updateChannels, List.of(CHANNEL_SIGNAL_STRENGTH), true); + registerService(multiLevelSwitchService, this::updateChannels, List.of(CHANNEL_BRIGHTNESS), true); + registerService(childProtectionService, this::updateChannels, List.of(CHANNEL_CHILD_PROTECTION), true); + } + + private void updateChannels(MultiLevelSwitchServiceState serviceState) { + super.updateState(CHANNEL_BRIGHTNESS, serviceState.toPercentType()); + } + + private void updateChannels(CommunicationQualityServiceState communicationQualityServiceState) { + updateState(CHANNEL_SIGNAL_STRENGTH, communicationQualityServiceState.quality.toSystemSignalStrength()); + } + + private void updateChannels(ChildProtectionServiceState childProtectionServiceState) { + super.updateState(CHANNEL_CHILD_PROTECTION, OnOffType.from(childProtectionServiceState.childLockActive)); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + + if (CHANNEL_CHILD_PROTECTION.equals(channelUID.getId()) && (command instanceof OnOffType onOffCommand)) { + updateChildProtectionState(onOffCommand); + } else if (CHANNEL_BRIGHTNESS.equals(channelUID.getId()) && command instanceof PercentType percentCommand) { + updateMultiLevelSwitchState(percentCommand); + } + } + + private void updateChildProtectionState(OnOffType onOffCommand) { + ChildProtectionServiceState childProtectionServiceState = new ChildProtectionServiceState(); + childProtectionServiceState.childLockActive = onOffCommand == OnOffType.ON; + updateServiceState(childProtectionService, childProtectionServiceState); + } + + private void updateMultiLevelSwitchState(PercentType percentCommand) { + MultiLevelSwitchServiceState serviceState = new MultiLevelSwitchServiceState(); + serviceState.level = percentCommand.intValue(); + this.updateServiceState(multiLevelSwitchService, serviceState); + } +} 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 e912cbd07da18..423e3acee752b 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 @@ -97,7 +97,8 @@ public class ThingDiscoveryService extends AbstractThingHandlerDiscoveryService< 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) + new AbstractMap.SimpleEntry<>("MICROMODULE_LIGHT_CONTROL", BoschSHCBindingConstants.THING_TYPE_LIGHT_CONTROL_2), + new AbstractMap.SimpleEntry<>("MICROMODULE_DIMMER", BoschSHCBindingConstants.THING_TYPE_DIMMER) // 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.), 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 8ce04446766ca..ba8473f1f8525 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 @@ -7,6 +7,8 @@ addon.boschshc.description = This is the binding for Bosch Smart Home. thing-type.boschshc.climate-control.label = Climate Control thing-type.boschshc.climate-control.description = This is a virtual device which is automatically created for all rooms that have thermostats in it. +thing-type.boschshc.dimmer.label = Dimmer +thing-type.boschshc.dimmer.description = Smart dimmer capable of controlling any dimmable lamp. 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 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 05fb5e87f72a6..ce106bdbba320 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 @@ -421,6 +421,24 @@ + + + + + + + Smart dimmer capable of controlling any dimmable lamp. + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/DimmerHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/DimmerHandlerTest.java new file mode 100644 index 0000000000000..96b8249b7fa0a --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/DimmerHandlerTest.java @@ -0,0 +1,153 @@ +/** + * 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.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +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.AbstractPowerSwitchHandlerTest; +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.binding.boschshc.internal.services.multilevelswitch.dto.MultiLevelSwitchServiceState; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingTypeUID; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; + +/** + * Unit tests for {@link DimmerHandler}. + * + * @author David Pace - Initial contribution + * + */ +@NonNullByDefault +class DimmerHandlerTest extends AbstractPowerSwitchHandlerTest { + + private @Captor @NonNullByDefault({}) ArgumentCaptor multiLevelSwitchServiceStateCaptor; + + private @Captor @NonNullByDefault({}) ArgumentCaptor childProtectionServiceStateCaptor; + + @Override + protected DimmerHandler createFixture() { + return new DimmerHandler(getThing()); + } + + @Override + protected ThingTypeUID getThingTypeUID() { + return BoschSHCBindingConstants.THING_TYPE_DIMMER; + } + + @Override + protected String getDeviceID() { + return "hdm:ZigBee:60b647fffec5a9d8"; + } + + @Test + void testUpdateChannelMultiLevelSwitchState() { + JsonElement jsonObject = JsonParser.parseString("{\"@type\":\"multiLevelSwitchState\",\"level\":16}"); + getFixture().processUpdate("MultiLevelSwitch", jsonObject); + verify(getCallback()).stateUpdated( + new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BRIGHTNESS), new PercentType(16)); + } + + @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 testHandleCommandMultiLevelSwitch() + throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { + getFixture().handleCommand(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BRIGHTNESS), + new PercentType(42)); + verify(getBridgeHandler()).putState(eq(getDeviceID()), eq("MultiLevelSwitch"), + multiLevelSwitchServiceStateCaptor.capture()); + MultiLevelSwitchServiceState state = multiLevelSwitchServiceStateCaptor.getValue(); + assertEquals(42, state.level); + } + + @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 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); + } + + @Test + void testHandleCommandChildProtectionInvalidCommand() + throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { + getFixture().handleCommand( + new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION), + DecimalType.ZERO); + verify(getBridgeHandler(), times(0)).putState(eq(getDeviceID()), eq("ChildProtection"), + childProtectionServiceStateCaptor.capture()); + } +}