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 <dev@davidpace.de>
This commit is contained in:
David Pace 2023-04-11 22:09:56 +02:00 committed by GitHub
parent 1a7a1251d4
commit cc626de89a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 255 additions and 10 deletions

View File

@ -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 | &#9744; | Current measured temperature. |
| valve-tappet-position | Number:Dimensionless | &#9744; | Current open ratio of valve tappet (0 to 100). |
| child-lock | Switch | &#9745; | Indicates if child lock is active. |
| silent-mode | Switch | &#9745; | Enables or disables silent mode on thermostats. When enabled, the battery usage is higher. |
| battery-level | Number | &#9744; | 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 | &#9744; | 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 | &#9745; | Regulates the brightness on a percentage scale from 0 to 100%. |
| color | Color | &#9745; | The color of the emitted light. |
### Smoke detector
### Smoke Detector
The smoke detector warns you in case of fire.

View File

@ -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";

View File

@ -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());
}
}

View File

@ -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<SilentModeServiceState> {
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);
}
}

View File

@ -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;
}
}

View File

@ -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.
* <p>
* Example JSON for normal mode:
*
* <pre>
* {
* "@type": "silentModeState",
* "mode": "MODE_NORMAL"
* }
* </pre>
*
* Example JSON for silent mode:
*
* <pre>
* {
* "@type": "silentModeState",
* "mode": "MODE_SILENT"
* }
* </pre>
*
* @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;
}
}

View File

@ -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

View File

@ -141,6 +141,7 @@
<channel id="temperature" typeId="temperature"/>
<channel id="valve-tappet-position" typeId="valve-tappet-position"/>
<channel id="child-lock" typeId="child-lock"/>
<channel id="silent-mode" typeId="silent-mode"/>
<channel id="battery-level" typeId="system.battery-level"/>
<channel id="low-battery" typeId="system.low-battery"/>
</channels>
@ -504,7 +505,19 @@
<channel-type id="child-lock">
<item-type>Switch</item-type>
<label>Child Lock</label>
<description>Indicates if it is possible to set the desired temperature on the device.</description>
<description>Enables or disables the child lock on the device.</description>
</channel-type>
<channel-type id="silent-mode">
<item-type>Switch</item-type>
<label>Silent Mode</label>
<description>Enables or disables silent mode on thermostats. When enabled, the battery usage is higher.</description>
<state>
<options>
<option value="MODE_NORMAL">Silent mode disabled (lower battery usage)</option>
<option value="MODE_SILENT">Silent mode enabled (higher battery usage)</option>
</options>
</state>
</channel-type>
</thing:thing-descriptions>

View File

@ -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<ThermostatHandler> {
class ThermostatHandlerTest extends AbstractBatteryPoweredDeviceHandlerTest<ThermostatHandler> {
private @Captor @NonNullByDefault({}) ArgumentCaptor<ChildLockServiceState> childLockServiceStateCaptor;
private @Captor @NonNullByDefault({}) ArgumentCaptor<SilentModeServiceState> 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);
}
}

View File

@ -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));
}
}