From cc626de89ad5aaee7ad9deae530e053da9ab8b88 Mon Sep 17 00:00:00 2001 From: David Pace Date: Tue, 11 Apr 2023 22:09:56 +0200 Subject: [PATCH] Support for Thermostat SilentMode (#14779) (#14781) - Add new channel definition for silent mode - Implement silent mode service - Add unit tests - Add documentation - Fix some minor documentation issues Closes #14779 Signed-off-by: David Pace --- .../org.openhab.binding.boschshc/README.md | 5 +- .../devices/BoschSHCBindingConstants.java | 1 + .../devices/thermostat/ThermostatHandler.java | 18 ++++++ .../silentmode/SilentModeService.java | 44 +++++++++++++++ .../services/silentmode/SilentModeState.java | 32 +++++++++++ .../dto/SilentModeServiceState.java | 54 ++++++++++++++++++ .../resources/OH-INF/i18n/boschshc.properties | 6 +- .../resources/OH-INF/thing/thing-types.xml | 15 ++++- .../thermostat/ThermostatHandlerTest.java | 55 +++++++++++++++++-- .../silentmode/SilentModeStateTest.java | 35 ++++++++++++ 10 files changed, 255 insertions(+), 10 deletions(-) create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/silentmode/SilentModeService.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/silentmode/SilentModeState.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/silentmode/dto/SilentModeServiceState.java create mode 100644 bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/silentmode/SilentModeStateTest.java diff --git a/bundles/org.openhab.binding.boschshc/README.md b/bundles/org.openhab.binding.boschshc/README.md index f7241a0c6..6be38759e 100644 --- a/bundles/org.openhab.binding.boschshc/README.md +++ b/bundles/org.openhab.binding.boschshc/README.md @@ -7,7 +7,7 @@ Binding for the Bosch Smart Home. - [In-Wall Switch](#in-wall-switch) - [Compact Smart Plug](#compact-smart-plug) - [Twinguard Smoke Detector](#twinguard-smoke-detector) - - [Door/Window Contact](#doorwindow-contact) + - [Door/Window Contact](#door-window-contact) - [Motion Detector](#motion-detector) - [Shutter Control](#shutter-control) - [Thermostat](#thermostat) @@ -116,6 +116,7 @@ Radiator thermostat | temperature | Number:Temperature | ☐ | Current measured temperature. | | valve-tappet-position | Number:Dimensionless | ☐ | Current open ratio of valve tappet (0 to 100). | | child-lock | Switch | ☑ | Indicates if child lock is active. | +| silent-mode | Switch | ☑ | Enables or disables silent mode on thermostats. When enabled, the battery usage is higher. | | battery-level | Number | ☐ | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. | | low-battery | Switch | ☐ | Indicates whether the battery is low (`ON`) or OK (`OFF`). | @@ -193,7 +194,7 @@ A smart bulb connected to the bridge via Zigbee such as a Ledvance Smart+ bulb. | brightness | Dimmer | ☑ | Regulates the brightness on a percentage scale from 0 to 100%. | | color | Color | ☑ | The color of the emitted light. | -### Smoke detector +### Smoke Detector The smoke detector warns you in case of fire. 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 f97100d3e..8b70ce0a4 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 @@ -82,6 +82,7 @@ public class BoschSHCBindingConstants { public static final String CHANNEL_COLOR = "color"; public static final String CHANNEL_BRIGHTNESS = "brightness"; public static final String CHANNEL_SMOKE_CHECK = "smoke-check"; + public static final String CHANNEL_SILENT_MODE = "silent-mode"; // static device/service names public static final String SERVICE_INTRUSION_DETECTION = "intrusionDetectionSystem"; diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandler.java index fe2468129..6a6e242a8 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandler.java @@ -21,6 +21,8 @@ import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDevic import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; import org.openhab.binding.boschshc.internal.services.childlock.ChildLockService; import org.openhab.binding.boschshc.internal.services.childlock.dto.ChildLockServiceState; +import org.openhab.binding.boschshc.internal.services.silentmode.SilentModeService; +import org.openhab.binding.boschshc.internal.services.silentmode.dto.SilentModeServiceState; import org.openhab.binding.boschshc.internal.services.temperaturelevel.TemperatureLevelService; import org.openhab.binding.boschshc.internal.services.temperaturelevel.dto.TemperatureLevelServiceState; import org.openhab.binding.boschshc.internal.services.valvetappet.ValveTappetService; @@ -33,15 +35,18 @@ import org.openhab.core.types.Command; * Handler for a thermostat device. * * @author Christian Oeing - Initial contribution + * @author David Pace - Added silent mode service */ @NonNullByDefault public final class ThermostatHandler extends AbstractBatteryPoweredDeviceHandler { private ChildLockService childLockService; + private SilentModeService silentModeService; public ThermostatHandler(Thing thing) { super(thing); this.childLockService = new ChildLockService(); + this.silentModeService = new SilentModeService(); } @Override @@ -51,6 +56,7 @@ public final class ThermostatHandler extends AbstractBatteryPoweredDeviceHandler this.createService(TemperatureLevelService::new, this::updateChannels, List.of(CHANNEL_TEMPERATURE)); this.createService(ValveTappetService::new, this::updateChannels, List.of(CHANNEL_VALVE_TAPPET_POSITION)); this.registerService(this.childLockService, this::updateChannels, List.of(CHANNEL_CHILD_LOCK)); + this.registerService(this.silentModeService, this::updateChannels, List.of(CHANNEL_SILENT_MODE)); } @Override @@ -61,6 +67,9 @@ public final class ThermostatHandler extends AbstractBatteryPoweredDeviceHandler case CHANNEL_CHILD_LOCK: this.handleServiceCommand(this.childLockService, command); break; + case CHANNEL_SILENT_MODE: + this.handleServiceCommand(this.silentModeService, command); + break; } } @@ -93,4 +102,13 @@ public final class ThermostatHandler extends AbstractBatteryPoweredDeviceHandler private void updateChannels(ChildLockServiceState state) { super.updateState(CHANNEL_CHILD_LOCK, state.getActiveState()); } + + /** + * Updates the channels which are linked to the {@link SilentModeService} of the device. + * + * @param state current state of {@link SilentModeService} + */ + private void updateChannels(SilentModeServiceState state) { + super.updateState(CHANNEL_SILENT_MODE, state.toOnOffType()); + } } diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/silentmode/SilentModeService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/silentmode/SilentModeService.java new file mode 100644 index 000000000..386e837bf --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/silentmode/SilentModeService.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2023 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.silentmode; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; +import org.openhab.binding.boschshc.internal.services.BoschSHCService; +import org.openhab.binding.boschshc.internal.services.silentmode.dto.SilentModeServiceState; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.types.Command; + +/** + * Service to get and set the silent mode of thermostats. + * + * @author David Pace - Initial contribution + * + */ +@NonNullByDefault +public class SilentModeService extends BoschSHCService { + + public SilentModeService() { + super("SilentMode", SilentModeServiceState.class); + } + + @Override + public SilentModeServiceState handleCommand(Command command) throws BoschSHCException { + if (command instanceof OnOffType onOffCommand) { + SilentModeServiceState serviceState = new SilentModeServiceState(); + serviceState.mode = SilentModeState.fromOnOffType(onOffCommand); + return serviceState; + } + return super.handleCommand(command); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/silentmode/SilentModeState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/silentmode/SilentModeState.java new file mode 100644 index 000000000..6a931efff --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/silentmode/SilentModeState.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2023 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.silentmode; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.types.OnOffType; + +/** + * Enum for possible silent mode states. + * + * @author David Pace - Initial contribution + * + */ +@NonNullByDefault +public enum SilentModeState { + MODE_NORMAL, + MODE_SILENT; + + public static SilentModeState fromOnOffType(OnOffType onOffType) { + return onOffType == OnOffType.ON ? SilentModeState.MODE_SILENT : SilentModeState.MODE_NORMAL; + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/silentmode/dto/SilentModeServiceState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/silentmode/dto/SilentModeServiceState.java new file mode 100644 index 000000000..fc50d008b --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/silentmode/dto/SilentModeServiceState.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2010-2023 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.silentmode.dto; + +import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState; +import org.openhab.binding.boschshc.internal.services.silentmode.SilentModeState; +import org.openhab.core.library.types.OnOffType; + +/** + * Represents the state of the silent mode for thermostats. + *

+ * Example JSON for normal mode: + * + *

+ * {
+ *   "@type": "silentModeState",
+ *   "mode": "MODE_NORMAL"
+ * }
+ * 
+ * + * Example JSON for silent mode: + * + *
+ * {
+ *   "@type": "silentModeState",
+ *   "mode": "MODE_SILENT"
+ * }
+ * 
+ * + * @author David Pace - Initial contribution + * + */ +public class SilentModeServiceState extends BoschSHCServiceState { + + public SilentModeServiceState() { + super("silentModeState"); + } + + public SilentModeState mode; + + public OnOffType toOnOffType() { + return mode == SilentModeState.MODE_SILENT ? OnOffType.ON : OnOffType.OFF; + } +} 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 8a564ea88..2371ad36a 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 @@ -70,7 +70,7 @@ channel-type.boschshc.camera-notification.description = Enables or disables noti channel-type.boschshc.camera-notification.state.option.ENABLED = Enable notifications channel-type.boschshc.camera-notification.state.option.DISABLED = Disable notifications channel-type.boschshc.child-lock.label = Child Lock -channel-type.boschshc.child-lock.description = Indicates if it is possible to set the desired temperature on the device. +channel-type.boschshc.child-lock.description = Enables or disables the child lock 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 @@ -107,6 +107,10 @@ channel-type.boschshc.purity.label = Purity channel-type.boschshc.purity.description = Purity of the air. A higher value indicates a higher pollution. channel-type.boschshc.setpoint-temperature.label = Setpoint Temperature channel-type.boschshc.setpoint-temperature.description = Desired temperature. +channel-type.boschshc.silent-mode.label = Silent Mode +channel-type.boschshc.silent-mode.description = Enables or disables silent mode on thermostats. When enabled, the battery usage is higher. +channel-type.boschshc.silent-mode.state.option.MODE_NORMAL = Silent mode disabled (lower battery usage) +channel-type.boschshc.silent-mode.state.option.MODE_SILENT = Silent mode enabled (higher battery usage) channel-type.boschshc.smoke-check.label = Smoke Check State channel-type.boschshc.smoke-check.description = State of last smoke detector check. channel-type.boschshc.smoke-check.state.option.NONE = None 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 fe22ddb9b..03354abe1 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 @@ -141,6 +141,7 @@ + @@ -504,7 +505,19 @@ Switch - Indicates if it is possible to set the desired temperature on the device. + Enables or disables the child lock on the device. + + + + Switch + + Enables or disables silent mode on thermostats. When enabled, the battery usage is higher. + + + + + + diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandlerTest.java index e493c57cc..2430b9ea1 100644 --- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandlerTest.java +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandlerTest.java @@ -30,6 +30,8 @@ import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants; import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; import org.openhab.binding.boschshc.internal.services.childlock.dto.ChildLockServiceState; import org.openhab.binding.boschshc.internal.services.childlock.dto.ChildLockState; +import org.openhab.binding.boschshc.internal.services.silentmode.SilentModeState; +import org.openhab.binding.boschshc.internal.services.silentmode.dto.SilentModeServiceState; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.QuantityType; @@ -51,10 +53,12 @@ import com.google.gson.JsonParser; * */ @NonNullByDefault -public class ThermostatHandlerTest extends AbstractBatteryPoweredDeviceHandlerTest { +class ThermostatHandlerTest extends AbstractBatteryPoweredDeviceHandlerTest { private @Captor @NonNullByDefault({}) ArgumentCaptor childLockServiceStateCaptor; + private @Captor @NonNullByDefault({}) ArgumentCaptor silentModeServiceStateCaptor; + @Override protected ThermostatHandler createFixture() { return new ThermostatHandler(getThing()); @@ -71,7 +75,7 @@ public class ThermostatHandlerTest extends AbstractBatteryPoweredDeviceHandlerTe } @Test - public void testHandleCommand() + void testHandleCommandChildLockService() throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { getFixture().handleCommand(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_CHILD_LOCK), OnOffType.ON); @@ -81,7 +85,18 @@ public class ThermostatHandlerTest extends AbstractBatteryPoweredDeviceHandlerTe } @Test - public void testHandleCommandUnknownCommand() { + void testHandleCommandSilentModeService() + throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { + getFixture().handleCommand(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_SILENT_MODE), + OnOffType.ON); + verify(getBridgeHandler()).putState(eq(getDeviceID()), eq("SilentMode"), + silentModeServiceStateCaptor.capture()); + SilentModeServiceState state = silentModeServiceStateCaptor.getValue(); + assertSame(SilentModeState.MODE_SILENT, state.mode); + } + + @Test + void testHandleCommandUnknownCommandChildLockService() { getFixture().handleCommand(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_CHILD_LOCK), new DecimalType(42)); ThingStatusInfo expectedThingStatusInfo = ThingStatusInfoBuilder @@ -93,7 +108,19 @@ public class ThermostatHandlerTest extends AbstractBatteryPoweredDeviceHandlerTe } @Test - public void testUpdateChannelsTemperatureLevelService() { + void testHandleCommandUnknownCommandSilentModeService() { + getFixture().handleCommand(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_SILENT_MODE), + new DecimalType(42)); + ThingStatusInfo expectedThingStatusInfo = ThingStatusInfoBuilder + .create(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR) + .withDescription( + "Error when service SilentMode should handle command org.openhab.core.library.types.DecimalType: SilentMode: Can not handle command org.openhab.core.library.types.DecimalType") + .build(); + verify(getCallback()).statusUpdated(getThing(), expectedThingStatusInfo); + } + + @Test + void testUpdateChannelsTemperatureLevelService() { JsonElement jsonObject = JsonParser.parseString( "{\n" + " \"@type\": \"temperatureLevelState\",\n" + " \"temperature\": 21.5\n" + " }"); getFixture().processUpdate("TemperatureLevel", jsonObject); @@ -103,7 +130,7 @@ public class ThermostatHandlerTest extends AbstractBatteryPoweredDeviceHandlerTe } @Test - public void testUpdateChannelsValveTappetService() { + void testUpdateChannelsValveTappetService() { JsonElement jsonObject = JsonParser .parseString("{\n" + " \"@type\": \"valveTappetState\",\n" + " \"position\": 42\n" + " }"); getFixture().processUpdate("ValveTappet", jsonObject); @@ -113,11 +140,27 @@ public class ThermostatHandlerTest extends AbstractBatteryPoweredDeviceHandlerTe } @Test - public void testUpdateChannelsChildLockService() { + void testUpdateChannelsChildLockService() { JsonElement jsonObject = JsonParser .parseString("{\n" + " \"@type\": \"childLockState\",\n" + " \"childLock\": \"ON\"\n" + " }"); getFixture().processUpdate("Thermostat", jsonObject); verify(getCallback()).stateUpdated( new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_CHILD_LOCK), OnOffType.ON); } + + @Test + void testUpdateChannelsSilentModeService() { + JsonElement jsonObject = JsonParser.parseString("{\"@type\": \"silentModeState\", \"mode\": \"MODE_SILENT\"}"); + getFixture().processUpdate("SilentMode", jsonObject); + verify(getCallback()).stateUpdated( + new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_SILENT_MODE), OnOffType.ON); + } + + @Test + void testUpdateChannelsSilentModeServiceNormal() { + JsonElement jsonObject = JsonParser.parseString("{\"@type\": \"silentModeState\", \"mode\": \"MODE_NORMAL\"}"); + getFixture().processUpdate("SilentMode", jsonObject); + verify(getCallback()).stateUpdated( + new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_SILENT_MODE), OnOffType.OFF); + } } diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/silentmode/SilentModeStateTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/silentmode/SilentModeStateTest.java new file mode 100644 index 000000000..38ec3ed25 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/silentmode/SilentModeStateTest.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2023 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.silentmode; + +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.core.library.types.OnOffType; + +/** + * Unit tests for {@link SilentModeState}. + * + * @author David Pace - Initial contribution + * + */ +@NonNullByDefault +class SilentModeStateTest { + + @Test + void fromOnOffType() { + assertSame(SilentModeState.MODE_NORMAL, SilentModeState.fromOnOffType(OnOffType.OFF)); + assertSame(SilentModeState.MODE_SILENT, SilentModeState.fromOnOffType(OnOffType.ON)); + } +}