[boschshc] Support for Smart Relay (#17026)

* [boschshc] Support for Smart Relay

Signed-off-by: David Pace <dev@davidpace.de>
This commit is contained in:
David Pace 2024-07-22 21:10:29 +02:00 committed by GitHub
parent 9a19eaa0fa
commit 64fc6bcd1f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 867 additions and 29 deletions

View File

@ -17,6 +17,7 @@ Binding for Bosch Smart Home devices.
- [Thermostat](#thermostat)
- [Climate Control](#climate-control)
- [Wall Thermostat](#wall-thermostat)
- [Relay](#relay)
- [Security Camera 360](#security-camera-360)
- [Security Camera Eyes](#security-camera-eyes)
- [Intrusion Detection System](#intrusion-detection-system)
@ -222,6 +223,21 @@ Display of the current room temperature as well as the relative humidity in the
| 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`). |
### Relay
The smart switching relay is your universal all-rounder for smart switching.
**Thing Type ID**: `relay`
| Channel Type ID | Item Type | Writable | Description |
| ----------------------- | ----------- | :------: | ---------------------------------------------------------------------------------- |
| signal-strength | Number | &#9744; | Communication quality between the device and the Smart Home Controller. Possible values range between 0 (unknown) and 4 (best signal strength). |
| child-protection | Switch | &#9745; | Indicates whether the child protection is active. |
| power-switch | Switch | &#9745; | Switches the relay on or off. Only available if the relay is in power switch mode. |
| impulse-switch | Switch | &#9745; | Channel to send impulses by means of `ON` events. After the time specified by `impulse-length`, the relay will switch off automatically and the state will be reset to `OFF`. Only available if the relay is in impulse switch mode. |
| impulse-length | Number:Time | &#9745; | Channel to configure how long the relay will stay on after receiving an impulse switch event. The time is specified in tenth seconds (deciseconds), e.g. 15 means 1.5 seconds. Only available if the relay is in impulse switch mode. |
| instant-of-last-impulse | DateTime | &#9744; | Timestamp indicating when the last impulse was triggered. Only available if the relay is in impulse switch mode. |
### Security Camera 360
Indoor security camera with 360° view and motion detection.

View File

@ -44,9 +44,10 @@ import org.osgi.service.component.annotations.Reference;
/**
* Console command to list Bosch SHC devices and openhab support.
* Use the SHC API to get all SHC devices and SHC services
* and tries to lookup openhab devices and implemented service classes.
* Prints each name and looked-up implementation on console.
* <p>
* Uses the SHC API to get all SHC devices and SHC services and tries to lookup
* openHAB devices and implemented service classes. Prints each name and
* looked-up implementation on console.
*
* @author Gerd Zanker - Initial contribution
*/
@ -73,17 +74,19 @@ public class BoschShcCommandExtension extends AbstractConsoleCommandExtension im
/**
* Returns all implemented services of this Bosch SHC binding.
* This list shall contain all available services and needs to be extended when a new service is added.
* A unit tests checks if this list matches with the existing subfolders in
* "src/main/java/org/openhab/binding/boschshc/internal/services".
* <p>
* This list shall contain all available services and needs to be extended when
* a new service is added. A unit tests checks if this list matches with the
* existing subfolders in
* <code>src/main/java/org/openhab/binding/boschshc/internal/services</code>.
*/
List<String> getAllBoschShcServices() {
return List.of("airqualitylevel", "batterylevel", "binaryswitch", "bypass", "cameranotification", "childlock",
"childprotection", "communicationquality", "hsbcoloractuator", "humiditylevel", "illuminance",
"intrusion", "keypad", "latestmotion", "multilevelswitch", "powermeter", "powerswitch", "privacymode",
"roomclimatecontrol", "shuttercontact", "shuttercontrol", "silentmode", "smokedetectorcheck",
"temperaturelevel", "userstate", "valvetappet", "waterleakagesensor", "waterleakagesensorcheck",
"waterleakagesensortilt");
"impulseswitch", "intrusion", "keypad", "latestmotion", "multilevelswitch", "powermeter", "powerswitch",
"privacymode", "roomclimatecontrol", "shuttercontact", "shuttercontrol", "silentmode",
"smokedetectorcheck", "temperaturelevel", "userstate", "valvetappet", "waterleakagesensor",
"waterleakagesensorcheck", "waterleakagesensortilt");
}
@Override

View File

@ -60,6 +60,7 @@ public class BoschSHCBindingConstants {
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_WATER_DETECTOR = new ThingTypeUID(BINDING_ID, "water-detector");
public static final ThingTypeUID THING_TYPE_RELAY = new ThingTypeUID(BINDING_ID, "relay");
public static final ThingTypeUID THING_TYPE_USER_DEFINED_STATE = new ThingTypeUID(BINDING_ID, "user-defined-state");
@ -112,7 +113,9 @@ public class BoschSHCBindingConstants {
public static final String CHANNEL_ACOUSTIC_SIGNALS_ON_MOVE = "acoustic-signals-on-move";
public static final String CHANNEL_WATER_LEAKAGE_SENSOR_CHECK = "water-leakage-sensor-check";
public static final String CHANNEL_SENSOR_MOVED = "sensor-moved";
public static final String CHANNEL_IMPULSE_SWITCH = "impulse-switch";
public static final String CHANNEL_IMPULSE_LENGTH = "impulse-length";
public static final String CHANNEL_INSTANT_OF_LAST_IMPULSE = "instant-of-last-impulse";
// numbered channels
// the rationale for introducing numbered channels was discussed in
// https://github.com/openhab/openhab-addons/pull/16400

View File

@ -26,6 +26,7 @@ import org.openhab.binding.boschshc.internal.devices.bridge.BridgeHandler;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Message;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.services.AbstractBoschSHCService;
import org.openhab.binding.boschshc.internal.services.AbstractStatelessBoschSHCDeviceService;
import org.openhab.binding.boschshc.internal.services.AbstractStatelessBoschSHCService;
import org.openhab.binding.boschshc.internal.services.AbstractStatelessBoschSHCServiceWithRequestBody;
import org.openhab.binding.boschshc.internal.services.BoschSHCService;
@ -402,11 +403,25 @@ public abstract class BoschSHCHandler extends BaseThingHandler {
service.setState(state);
} catch (TimeoutException | ExecutionException e) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String.format(
"Error when trying to update state for service %s: %s", service.getServiceName(), e.getMessage()));
"Error while trying to update state for service %s: %s", service.getServiceName(), e.getMessage()));
} catch (InterruptedException e) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String
.format("Interrupted update state for service %s: %s", service.getServiceName(), e.getMessage()));
Thread.currentThread().interrupt();
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String
.format("Interrupted state update for service %s: %s", service.getServiceName(), e.getMessage()));
}
}
protected <TService extends AbstractStatelessBoschSHCDeviceService<TState>, TState extends BoschSHCServiceState> void postState(
TService service, TState state) {
try {
service.setState(state);
} catch (TimeoutException | ExecutionException e) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String.format(
"Error while trying to post state for service %s: %s", service.getServiceName(), e.getMessage()));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, String
.format("Interrupted state update for service %s: %s", service.getServiceName(), e.getMessage()));
}
}

View File

@ -20,6 +20,7 @@ import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConst
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_RELAY;
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;
@ -52,6 +53,7 @@ import org.openhab.binding.boschshc.internal.devices.lightcontrol.LightControl2H
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.relay.RelayHandler;
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;
@ -135,7 +137,8 @@ public class BoschSHCHandlerFactory extends BaseThingHandlerFactory {
new ThingTypeHandlerMapping(THING_TYPE_SMOKE_DETECTOR_2, SmokeDetector2Handler::new),
new ThingTypeHandlerMapping(THING_TYPE_LIGHT_CONTROL_2, LightControl2Handler::new),
new ThingTypeHandlerMapping(THING_TYPE_DIMMER, DimmerHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_WATER_DETECTOR, WaterLeakageSensorHandler::new));
new ThingTypeHandlerMapping(THING_TYPE_WATER_DETECTOR, WaterLeakageSensorHandler::new),
new ThingTypeHandlerMapping(THING_TYPE_RELAY, RelayHandler::new));
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {

View File

@ -33,6 +33,7 @@ import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.boschshc.internal.devices.BoschDeviceIdUtils;
@ -872,19 +873,47 @@ public class BridgeHandler extends BaseBridgeHandler {
}
/**
* Sends a state change for a device to the controller
* Sends a state change for a device to the controller using a HTTP <code>PUT</code> request.
*
* @param deviceId Id of device to change state for
* @param serviceName Name of service of device to change state for
* @param state New state data to set for service
* @param <T> type of the state object
*
* @param deviceId the ID of the device for which the state should be updated
* @param serviceName the name of the service for which the state should be updated
* @param state object representing the new state data
*
* @return Response of request
* @return the HTTP response of the Bosch Smart Home Controller
*
* @throws InterruptedException
* @throws ExecutionException
* @throws TimeoutException
*/
public <T extends BoschSHCServiceState> @Nullable Response putState(String deviceId, String serviceName, T state)
throws InterruptedException, TimeoutException, ExecutionException {
return sendState(deviceId, serviceName, state, PUT);
}
/**
* Sends a state change for a device to the controller using a HTTP <code>POST</code> request.
*
* @param <T> type of the state object
*
* @param deviceId the ID of the device for which the state should be updated
* @param serviceName the name of the service for which the state should be updated
* @param state object representing the new state data
*
* @return the HTTP response of the Bosch Smart Home Controller
*
* @throws InterruptedException
* @throws TimeoutException
* @throws ExecutionException
*/
public <T extends BoschSHCServiceState> @Nullable Response postState(String deviceId, String serviceName, T state)
throws InterruptedException, TimeoutException, ExecutionException {
return sendState(deviceId, serviceName, state, POST);
}
private <T extends BoschSHCServiceState> @Nullable Response sendState(String deviceId, String serviceName, T state,
HttpMethod method) throws InterruptedException, TimeoutException, ExecutionException {
@Nullable
BoschHttpClient localHttpClient = this.httpClient;
if (localHttpClient == null) {
@ -894,7 +923,7 @@ public class BridgeHandler extends BaseBridgeHandler {
// Create request
String url = localHttpClient.getServiceStateUrl(serviceName, deviceId, state.getClass());
Request request = localHttpClient.createRequest(url, PUT, state);
Request request = localHttpClient.createRequest(url, method, state);
// Send request
return request.send();

View File

@ -0,0 +1,319 @@
/**
* 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.relay;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.BINDING_ID;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_IMPULSE_LENGTH;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_IMPULSE_SWITCH;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_INSTANT_OF_LAST_IMPULSE;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_POWER_SWITCH;
import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_SIGNAL_STRENGTH;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import javax.inject.Provider;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandler;
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.impulseswitch.ImpulseSwitchService;
import org.openhab.binding.boschshc.internal.services.impulseswitch.dto.ImpulseSwitchServiceState;
import org.openhab.core.library.CoreItemFactory;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.DefaultSystemChannelTypeProvider;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Handler for smart relays.
* <p>
* Relays are in one of two possible modes:
* <ul>
* <li>Power switch mode: a switch is used to toggle the relay on / off</li>
* <li>Impulse switch: the relay is triggered by an impulse and automatically
* switches off after a configured period of time</li>
* </ul>
* <p>
* Every time the thing is initialized, we detect dynamically which mode was
* configured for the relay and reconfigure the channels accordingly, if
* required.
* <p>
* In common usage scenarios, this will be the case upon the very first
* initialization only, or if the device is re-purposed.
*
* @author David Pace - Initial contribution
*
*/
@NonNullByDefault
public class RelayHandler extends AbstractPowerSwitchHandler {
private final Logger logger = LoggerFactory.getLogger(RelayHandler.class);
private ChildProtectionService childProtectionService;
private ImpulseSwitchService impulseSwitchService;
/**
* Indicates whether the relay is configured in impulse switch mode. If this is
* <code>false</code>, the relay is in the default power switch (toggle) mode
*/
private boolean isInImpulseSwitchMode;
/**
* A provider for the current date/time.
* <p>
* It is exchanged in unit tests in order to be able to assert that a certain
* date is contained in the result.
*/
private Provider<Instant> currentDateTimeProvider = Instant::now;
@Nullable
private ImpulseSwitchServiceState currentImpulseSwitchServiceState;
public RelayHandler(Thing thing) {
super(thing);
this.childProtectionService = new ChildProtectionService();
this.impulseSwitchService = new ImpulseSwitchService();
}
@Override
protected boolean processDeviceInfo(Device deviceInfo) {
this.isInImpulseSwitchMode = isRelayInImpulseSwitchMode(deviceInfo);
configureChannels();
return super.processDeviceInfo(deviceInfo);
}
/**
* Dynamically configures the channels according to the device mode.
* <p>
* Two configurations are possible:
*
* <ul>
* <li>Power Switch Mode (relay stays on indefinitely when switched on)</li>
* <li>Impulse Switch Mode (relay stays on for a configured amount of time and
* then switches off automatically)</li>
* </ul>
*/
private void configureChannels() {
if (isInImpulseSwitchMode) {
configureImpulseSwitchModeChannels();
} else {
configurePowerSwitchModeChannels();
}
}
private void configureImpulseSwitchModeChannels() {
List<String> channelsToBePresent = List.of(CHANNEL_IMPULSE_SWITCH, CHANNEL_IMPULSE_LENGTH,
CHANNEL_INSTANT_OF_LAST_IMPULSE);
List<String> channelsToBeAbsent = List.of(CHANNEL_POWER_SWITCH);
configureChannels(channelsToBePresent, channelsToBeAbsent);
}
private void configurePowerSwitchModeChannels() {
List<String> channelsToBePresent = List.of(CHANNEL_POWER_SWITCH);
List<String> channelsToBeAbsent = List.of(CHANNEL_IMPULSE_SWITCH, CHANNEL_IMPULSE_LENGTH,
CHANNEL_INSTANT_OF_LAST_IMPULSE);
configureChannels(channelsToBePresent, channelsToBeAbsent);
}
/**
* Re-configures the channels of the associated thing, if applicable.
*
* @param channelsToBePresent channels to be added, if not present already
* @param channelsToBeAbsent channels to be removed, if present
*/
private void configureChannels(List<String> channelsToBePresent, List<String> channelsToBeAbsent) {
List<String> channelsToAdd = channelsToBePresent.stream().filter(c -> getThing().getChannel(c) == null)
.toList();
List<Channel> channelsToRemove = channelsToBeAbsent.stream().map(c -> getThing().getChannel(c))
.filter(Objects::nonNull).map(Objects::requireNonNull).toList();
if (channelsToAdd.isEmpty() && channelsToRemove.isEmpty()) {
return;
}
ThingBuilder thingBuilder = editThing();
if (!channelsToAdd.isEmpty()) {
addChannels(channelsToAdd, thingBuilder);
}
if (!channelsToRemove.isEmpty()) {
thingBuilder.withoutChannels(channelsToRemove);
}
updateThing(thingBuilder.build());
}
private void addChannels(List<String> channelsToAdd, ThingBuilder thingBuilder) {
for (String channelToAdd : channelsToAdd) {
Channel channel = createChannel(channelToAdd);
thingBuilder.withChannel(channel);
}
}
private Channel createChannel(String channelId) {
ChannelUID channelUID = new ChannelUID(getThing().getUID(), channelId);
ChannelTypeUID channelTypeUID = getChannelTypeUID(channelId);
@Nullable
String itemType = getItemType(channelId);
return ChannelBuilder.create(channelUID, itemType).withType(channelTypeUID).build();
}
private ChannelTypeUID getChannelTypeUID(String channelId) {
switch (channelId) {
case CHANNEL_IMPULSE_SWITCH, CHANNEL_IMPULSE_LENGTH, CHANNEL_INSTANT_OF_LAST_IMPULSE:
return new ChannelTypeUID(BINDING_ID, channelId);
case CHANNEL_POWER_SWITCH:
return DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_POWER;
default:
throw new UnsupportedOperationException(
"Cannot determine channel type UID to create channel " + channelId + " dynamically.");
}
}
private @Nullable String getItemType(String channelId) {
switch (channelId) {
case CHANNEL_POWER_SWITCH, CHANNEL_IMPULSE_SWITCH:
return CoreItemFactory.SWITCH;
case CHANNEL_IMPULSE_LENGTH:
return CoreItemFactory.NUMBER + ":Time";
case CHANNEL_INSTANT_OF_LAST_IMPULSE:
return CoreItemFactory.DATETIME;
default:
throw new UnsupportedOperationException(
"Cannot determine item type to create channel " + channelId + " dynamically.");
}
}
private boolean isRelayInImpulseSwitchMode(Device deviceInfo) {
List<String> serviceIds = deviceInfo.deviceServiceIds;
return serviceIds != null && serviceIds.contains(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME);
}
@Override
protected void initializeServices() throws BoschSHCException {
if (!isInImpulseSwitchMode) {
// initialize PowerSwitch service only if the relay is not configured as impulse
// switch
super.initializeServices();
} else {
// initialize impulse switch service only if the relay is configured as impulse
// switch
registerService(impulseSwitchService, this::updateChannels,
List.of(CHANNEL_IMPULSE_SWITCH, CHANNEL_IMPULSE_LENGTH, CHANNEL_INSTANT_OF_LAST_IMPULSE), true);
}
createService(CommunicationQualityService::new, this::updateChannels, List.of(CHANNEL_SIGNAL_STRENGTH), true);
registerService(childProtectionService, this::updateChannels, List.of(CHANNEL_CHILD_PROTECTION), 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(ImpulseSwitchServiceState impulseSwitchServiceState) {
this.currentImpulseSwitchServiceState = impulseSwitchServiceState;
updateState(CHANNEL_IMPULSE_SWITCH, OnOffType.from(impulseSwitchServiceState.impulseState));
updateState(CHANNEL_IMPULSE_LENGTH, new DecimalType(impulseSwitchServiceState.impulseLength));
State newInstantOfLastImpulse = impulseSwitchServiceState.instantOfLastImpulse != null
? new DateTimeType(impulseSwitchServiceState.instantOfLastImpulse)
: UnDefType.NULL;
updateState(CHANNEL_INSTANT_OF_LAST_IMPULSE, newInstantOfLastImpulse);
}
@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_IMPULSE_SWITCH.equals(channelUID.getId()) && command instanceof OnOffType onOffCommand) {
triggerImpulse(onOffCommand);
} else if (CHANNEL_IMPULSE_LENGTH.equals(channelUID.getId()) && command instanceof DecimalType number) {
updateImpulseLength(number);
}
}
private void updateChildProtectionState(OnOffType onOffCommand) {
ChildProtectionServiceState childProtectionServiceState = new ChildProtectionServiceState();
childProtectionServiceState.childLockActive = onOffCommand == OnOffType.ON;
updateServiceState(childProtectionService, childProtectionServiceState);
}
private void triggerImpulse(OnOffType onOffCommand) {
if (onOffCommand != OnOffType.ON) {
return;
}
ImpulseSwitchServiceState newState = cloneCurrentImpulseSwitchServiceState();
if (newState != null) {
newState.impulseState = true;
newState.instantOfLastImpulse = currentDateTimeProvider.get().toString();
this.currentImpulseSwitchServiceState = newState;
updateServiceState(impulseSwitchService, newState);
}
}
private void updateImpulseLength(DecimalType number) {
ImpulseSwitchServiceState newState = cloneCurrentImpulseSwitchServiceState();
if (newState != null) {
newState.impulseLength = number.intValue();
this.currentImpulseSwitchServiceState = newState;
logger.debug("New impulse length setting for relay: {} deciseconds", newState.impulseLength);
updateServiceState(impulseSwitchService, newState);
logger.debug("Successfully sent state with new impulse length to controller.");
}
}
private @Nullable ImpulseSwitchServiceState cloneCurrentImpulseSwitchServiceState() {
if (currentImpulseSwitchServiceState != null) {
ImpulseSwitchServiceState clonedState = new ImpulseSwitchServiceState();
clonedState.impulseState = currentImpulseSwitchServiceState.impulseState;
clonedState.impulseLength = currentImpulseSwitchServiceState.impulseLength;
clonedState.instantOfLastImpulse = currentImpulseSwitchServiceState.instantOfLastImpulse;
return clonedState;
} else {
logger.warn("Could not obtain current impulse switch state, command will not be processed.");
}
return null;
}
void setCurrentDateTimeProvider(Provider<Instant> currentDateTimeProvider) {
this.currentDateTimeProvider = currentDateTimeProvider;
}
}

View File

@ -100,7 +100,8 @@ public class ThingDiscoveryService extends AbstractThingHandlerDiscoveryService<
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_DIMMER", BoschSHCBindingConstants.THING_TYPE_DIMMER),
new AbstractMap.SimpleEntry<>("WLS", BoschSHCBindingConstants.THING_TYPE_WATER_DETECTOR)
new AbstractMap.SimpleEntry<>("WLS", BoschSHCBindingConstants.THING_TYPE_WATER_DETECTOR),
new AbstractMap.SimpleEntry<>("MICROMODULE_RELAY", BoschSHCBindingConstants.THING_TYPE_RELAY)
// 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.),

View File

@ -0,0 +1,60 @@
/**
* 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;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.devices.bridge.BridgeHandler;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
/**
* Abstract implementation for device services that only allow setting states
* via HTTP POST requests. State-less services can not receive any states from
* the bridge.
*
* @author David Pace - Initial contribution
*
*/
@NonNullByDefault
public class AbstractStatelessBoschSHCDeviceService<TState extends BoschSHCServiceState>
extends AbstractBoschSHCService {
protected AbstractStatelessBoschSHCDeviceService(String serviceName) {
super(serviceName);
}
/**
* Sends a HTTP POST request to the bridge with a state payload.
*
* @param state the state object to be sent to the bridge
*
* @throws InterruptedException
* @throws ExecutionException
* @throws TimeoutException
*/
public void setState(TState state) throws InterruptedException, TimeoutException, ExecutionException {
String deviceId = getDeviceId();
if (deviceId == null) {
return;
}
BridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler == null) {
return;
}
bridgeHandler.postState(deviceId, getServiceName(), state);
}
}

View File

@ -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.impulseswitch;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.boschshc.internal.services.BoschSHCService;
import org.openhab.binding.boschshc.internal.services.impulseswitch.dto.ImpulseSwitchServiceState;
/**
* Service to send impulses to relays.
*
* @author David Pace - Initial contribution
*
*/
@NonNullByDefault
public class ImpulseSwitchService extends BoschSHCService<ImpulseSwitchServiceState> {
public static final String IMPULSE_SWITCH_SERVICE_NAME = "ImpulseSwitch";
public ImpulseSwitchService() {
super(IMPULSE_SWITCH_SERVICE_NAME, ImpulseSwitchServiceState.class);
}
}

View File

@ -0,0 +1,47 @@
/**
* 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.impulseswitch.dto;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
/**
* Data transfer object for impulses to be sent to relays.
* <p>
* Example JSON:
*
* <pre>
* {
* "@type": "ImpulseSwitchState",
* "impulseState": true,
* "impulseLength": 100,
* "instantOfLastImpulse": "2024-04-14T15:52:31.677366Z"
* }
* </pre>
*
* The <code>impulseLength</code> specifies the time (in tenth seconds) until the relay switches off again.
*
* @author David Pace - Initial contribution
*
*/
public class ImpulseSwitchServiceState extends BoschSHCServiceState {
public ImpulseSwitchServiceState() {
super("ImpulseSwitchState");
}
public boolean impulseState;
public int impulseLength;
public String instantOfLastImpulse;
}

View File

@ -17,6 +17,8 @@ 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.relay.label = Relay
thing-type.boschshc.relay.description = The smart switching relay is your universal all-rounder for smart switching.
thing-type.boschshc.security-camera-360.label = Security Camera 360
thing-type.boschshc.security-camera-360.description = Indoor security camera with 360° view and motion detection.
thing-type.boschshc.security-camera-eyes.label = Security Camera Eyes
@ -121,6 +123,12 @@ channel-type.boschshc.humidity.label = Humidity
channel-type.boschshc.humidity.description = Current measured humidity.
channel-type.boschshc.illuminance.label = Illuminance
channel-type.boschshc.illuminance.description = The illuminance level measured by the sensor (0 to 1000).
channel-type.boschshc.impulse-length.label = Impulse Length
channel-type.boschshc.impulse-length.description = Channel to configure how long the relay will stay on after receiving an impulse switch event. The time is specified in tenth seconds (deciseconds), e.g. 15 means 1.5 seconds.
channel-type.boschshc.impulse-switch.label = Impulse Switch
channel-type.boschshc.impulse-switch.description = Channel to send impulses by means of `ON` events. After the time specified by impulse-length, the relay will switch off automatically and the state will be reset to `OFF`.
channel-type.boschshc.instant-of-last-impulse.label = Instant of Last Impulse
channel-type.boschshc.instant-of-last-impulse.description = Timestamp indicating when the last impulse was triggered.
channel-type.boschshc.key-code.label = Key Code
channel-type.boschshc.key-code.description = Integer code of the key that was pressed.
channel-type.boschshc.key-event-timestamp.label = Key Event Timestamp

View File

@ -461,6 +461,26 @@
<config-description-ref uri="thing-type:boschshc:device"/>
</thing-type>
<thing-type id="relay">
<supported-bridge-type-refs>
<bridge-type-ref id="shc"/>
</supported-bridge-type-refs>
<label>Relay</label>
<description>The smart switching relay is your universal all-rounder for smart switching.</description>
<channels>
<channel id="signal-strength" typeId="system.signal-strength"/>
<channel id="child-protection" typeId="child-protection"/>
<channel id="power-switch" typeId="system.power"/>
<channel id="impulse-switch" typeId="impulse-switch"/>
<channel id="impulse-length" typeId="impulse-length"/>
<channel id="instant-of-last-impulse" typeId="instant-of-last-impulse"/>
</channels>
<config-description-ref uri="thing-type:boschshc:device"/>
</thing-type>
<!-- Channels -->
<channel-type id="system-availability">
@ -845,4 +865,26 @@
</tags>
</channel-type>
<channel-type id="impulse-switch">
<item-type>Switch</item-type>
<label>Impulse Switch</label>
<description>Channel to send impulses by means of `ON` events. After the time specified by impulse-length, the relay
will switch off automatically and the state will be reset to `OFF`.</description>
</channel-type>
<channel-type id="impulse-length">
<item-type unitHint="ds">Number:Time</item-type>
<label>Impulse Length</label>
<description>Channel to configure how long the relay will stay on after receiving an impulse switch event. The time is
specified in tenth seconds (deciseconds), e.g. 15 means 1.5 seconds.</description>
</channel-type>
<channel-type id="instant-of-last-impulse">
<item-type>DateTime</item-type>
<label>Instant of Last Impulse</label>
<description>Timestamp indicating when the last impulse was triggered.</description>
<category>Time</category>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -12,8 +12,8 @@
*/
package org.openhab.binding.boschshc.internal.devices;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ -27,6 +27,7 @@ import org.junit.jupiter.params.provider.MethodSource;
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.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
@ -62,7 +63,7 @@ public abstract class AbstractBoschSHCDeviceHandlerTest<T extends BoschSHCDevice
getFixture().getThing().getConfiguration().remove("id");
getFixture().initialize();
verify(getCallback()).statusUpdated(eq(getThing()),
verify(getCallback()).statusUpdated(any(Thing.class),
argThat(status -> status.getStatus().equals(ThingStatus.OFFLINE)
&& status.getStatusDetail().equals(ThingStatusDetail.CONFIGURATION_ERROR)));
}
@ -75,7 +76,7 @@ public abstract class AbstractBoschSHCDeviceHandlerTest<T extends BoschSHCDevice
getFixture().initialize();
verify(getCallback()).statusUpdated(eq(getThing()),
verify(getCallback()).statusUpdated(any(Thing.class),
argThat(status -> status.getStatus().equals(ThingStatus.OFFLINE)
&& status.getStatusDetail().equals(ThingStatusDetail.CONFIGURATION_ERROR)));
}

View File

@ -18,7 +18,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
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;
@ -133,7 +132,7 @@ public abstract class AbstractBoschSHCHandlerTest<T extends BoschSHCHandler> {
@Test
void testInitialize() {
ThingStatusInfo expectedStatusInfo = new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
verify(callback).statusUpdated(same(thing), eq(expectedStatusInfo));
verify(callback).statusUpdated(any(Thing.class), eq(expectedStatusInfo));
}
@Test

View File

@ -36,6 +36,7 @@ import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
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.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.RefreshType;
@ -90,7 +91,7 @@ public abstract class AbstractPowerSwitchHandlerTest<T extends AbstractPowerSwit
getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.ON);
verify(getCallback()).statusUpdated(same(getThing()),
verify(getCallback()).statusUpdated(any(Thing.class),
argThat(status -> status.getStatus().equals(ThingStatus.OFFLINE)
&& status.getStatusDetail().equals(ThingStatusDetail.COMMUNICATION_ERROR)));
}
@ -132,7 +133,7 @@ public abstract class AbstractPowerSwitchHandlerTest<T extends AbstractPowerSwit
getFixture().handleCommand(getChannelUID(BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), RefreshType.REFRESH);
verify(getCallback()).statusUpdated(same(getThing()),
verify(getCallback()).statusUpdated(any(Thing.class),
argThat(status -> status.getStatus().equals(ThingStatus.OFFLINE)
&& status.getStatusDetail().equals(ThingStatusDetail.COMMUNICATION_ERROR)));
}

View File

@ -0,0 +1,258 @@
/**
* 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.relay;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
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.impulseswitch.ImpulseSwitchService;
import org.openhab.binding.boschshc.internal.services.impulseswitch.dto.ImpulseSwitchServiceState;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.types.UnDefType;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
/**
* Unit tests for {@link RelayHandler}.
*
* @author David Pace - Initial contributions
*
*/
@NonNullByDefault
class RelayHandlerTest extends AbstractPowerSwitchHandlerTest<RelayHandler> {
private @Captor @NonNullByDefault({}) ArgumentCaptor<ChildProtectionServiceState> childProtectionServiceStateCaptor;
private @Captor @NonNullByDefault({}) ArgumentCaptor<ImpulseSwitchServiceState> impulseSwitchServiceStateCaptor;
@Override
protected RelayHandler createFixture() {
return new RelayHandler(getThing());
}
@Override
protected ThingTypeUID getThingTypeUID() {
return BoschSHCBindingConstants.THING_TYPE_RELAY;
}
@Override
protected String getDeviceID() {
return "hdm:ZigBee:30XXXXXXXXXXXXXX";
}
@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 testUpdateChannelsImpulseSwitchService()
throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
configureImpulseSwitchMode();
String json = """
{
"@type": "ImpulseSwitchState",
"impulseState": true,
"impulseLength": 100,
"instantOfLastImpulse": "2024-04-14T15:52:31.677366Z"
}
""";
JsonElement jsonObject = JsonParser.parseString(json);
getFixture().processUpdate("ImpulseSwitch", jsonObject);
verify(getCallback()).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_IMPULSE_SWITCH), OnOffType.ON);
verify(getCallback(), times(2)).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_IMPULSE_LENGTH),
new DecimalType(100));
verify(getCallback(), times(2)).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_INSTANT_OF_LAST_IMPULSE),
new DateTimeType("2024-04-14T15:52:31.677366Z"));
}
@Test
void testUpdateChannelsImpulseSwitchServiceNoInstantOfLastImpulse()
throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
configureImpulseSwitchMode();
String json = """
{
"@type": "ImpulseSwitchState",
"impulseState": true,
"impulseLength": 100
}
""";
JsonElement jsonObject = JsonParser.parseString(json);
getFixture().processUpdate("ImpulseSwitch", jsonObject);
verify(getCallback()).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_IMPULSE_SWITCH), OnOffType.ON);
verify(getCallback(), times(2)).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_IMPULSE_LENGTH),
new DecimalType(100));
verify(getCallback()).stateUpdated(
new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_INSTANT_OF_LAST_IMPULSE),
UnDefType.NULL);
}
private void configureImpulseSwitchMode()
throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
getDevice().deviceServiceIds = List.of(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME);
getFixture().initialize();
assertThat(getFixture().getThing().getChannel(BoschSHCBindingConstants.CHANNEL_IMPULSE_SWITCH),
is(notNullValue()));
assertThat(getFixture().getThing().getChannel(BoschSHCBindingConstants.CHANNEL_IMPULSE_LENGTH),
is(notNullValue()));
assertThat(getFixture().getThing().getChannel(BoschSHCBindingConstants.CHANNEL_INSTANT_OF_LAST_IMPULSE),
is(notNullValue()));
@Nullable
JsonElement impulseSwitchServiceState = JsonParser.parseString("""
{
"@type": "ImpulseSwitchState",
"impulseState": false,
"impulseLength": 100,
"instantOfLastImpulse": "2024-04-14T15:52:31.677366Z"
}
""");
getFixture().processUpdate(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME, impulseSwitchServiceState);
}
@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();
assertThat(state.childLockActive, is(true));
}
@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"), any());
}
@Test
void testHandleCommandImpulseStateOn()
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
configureImpulseSwitchMode();
Instant testDate = Instant.now();
getFixture().setCurrentDateTimeProvider(() -> testDate);
getFixture().handleCommand(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_IMPULSE_SWITCH),
OnOffType.ON);
verify(getBridgeHandler()).putState(eq(getDeviceID()), eq(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME),
impulseSwitchServiceStateCaptor.capture());
ImpulseSwitchServiceState state = impulseSwitchServiceStateCaptor.getValue();
assertThat(state.impulseState, is(true));
assertThat(state.impulseLength, is(100));
assertThat(state.instantOfLastImpulse, is(testDate.toString()));
}
@Test
void testHandleCommandImpulseLength()
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
configureImpulseSwitchMode();
Instant testDate = Instant.now();
getFixture().setCurrentDateTimeProvider(() -> testDate);
getFixture().handleCommand(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_IMPULSE_LENGTH),
new DecimalType(15));
verify(getBridgeHandler()).putState(eq(getDeviceID()), eq(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME),
impulseSwitchServiceStateCaptor.capture());
ImpulseSwitchServiceState state = impulseSwitchServiceStateCaptor.getValue();
assertThat(state.impulseState, is(false));
assertThat(state.impulseLength, is(15));
assertThat(state.instantOfLastImpulse, is("2024-04-14T15:52:31.677366Z"));
}
@Test
void testHandleCommandImpulseStateOff()
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
getFixture().handleCommand(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_IMPULSE_SWITCH),
OnOffType.OFF);
verify(getBridgeHandler(), times(0)).postState(eq(getDeviceID()),
eq(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME), any());
}
}