Adds support for Bosch Smart Home Dimmer devices.
Signed-off-by: David Pace <dev@davidpace.de>
| 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.
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");
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;
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;
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_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) {
--- /dev/null
+/**
+ * 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);
+ }
+}
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.),
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
<config-description-ref uri="thing-type:boschshc:device"/>
</thing-type>
+ <thing-type id="dimmer">
+ <supported-bridge-type-refs>
+ <bridge-type-ref id="shc"/>
+ </supported-bridge-type-refs>
+
+ <label>Dimmer</label>
+ <description>Smart dimmer capable of controlling any dimmable lamp.</description>
+
+ <channels>
+ <channel id="power-switch" typeId="system.power"/>
+ <channel id="brightness" typeId="system.brightness"/>
+ <channel id="signal-strength" typeId="system.signal-strength"/>
+ <channel id="child-protection" typeId="child-protection"/>
+ </channels>
+
+ <config-description-ref uri="thing-type:boschshc:device"/>
+ </thing-type>
+
<!-- Channels -->
<channel-type id="system-availability">
--- /dev/null
+/**
+ * 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<DimmerHandler> {
+
+ private @Captor @NonNullByDefault({}) ArgumentCaptor<MultiLevelSwitchServiceState> multiLevelSwitchServiceStateCaptor;
+
+ private @Captor @NonNullByDefault({}) ArgumentCaptor<ChildProtectionServiceState> 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());
+ }
+}