diff --git a/bundles/org.openhab.binding.boschshc/README.md b/bundles/org.openhab.binding.boschshc/README.md
index 0d8dd4486a7..7e159a06d66 100644
--- a/bundles/org.openhab.binding.boschshc/README.md
+++ b/bundles/org.openhab.binding.boschshc/README.md
@@ -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 | ☐ | 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`). |
+### 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 | ☐ | 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. |
+| power-switch | Switch | ☑ | Switches the relay on or off. Only available if the relay is in power switch mode. |
+| impulse-switch | Switch | ☑ | 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 | ☑ | 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 | ☐ | 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.
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtension.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtension.java
index 0b912136214..189bcb5fdc0 100644
--- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtension.java
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/console/BoschShcCommandExtension.java
@@ -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.
+ *
+ * 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".
+ *
+ * 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.
*/
List 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
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 4dba916088f..f48a18bb906 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
@@ -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
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java
index a3b07f33030..3f00e0010cd 100644
--- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java
@@ -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 , 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()));
}
}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java
index 9fabcdff4f5..c16495c5a9b 100644
--- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java
@@ -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) {
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java
index 8deb0578061..9575de3c955 100644
--- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java
@@ -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 PUT 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 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 @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 POST request.
+ *
+ * @param 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 @Nullable Response postState(String deviceId, String serviceName, T state)
+ throws InterruptedException, TimeoutException, ExecutionException {
+ return sendState(deviceId, serviceName, state, POST);
+ }
+
+ private @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();
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/relay/RelayHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/relay/RelayHandler.java
new file mode 100644
index 00000000000..8dd4784cd39
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/relay/RelayHandler.java
@@ -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.
+ *
+ * Relays are in one of two possible modes:
+ *
+ *
Power switch mode: a switch is used to toggle the relay on / off
+ *
Impulse switch: the relay is triggered by an impulse and automatically
+ * switches off after a configured period of time
+ *
+ *
+ * Every time the thing is initialized, we detect dynamically which mode was
+ * configured for the relay and reconfigure the channels accordingly, if
+ * required.
+ *
+ * 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
+ * false, the relay is in the default power switch (toggle) mode
+ */
+ private boolean isInImpulseSwitchMode;
+
+ /**
+ * A provider for the current date/time.
+ *
+ * It is exchanged in unit tests in order to be able to assert that a certain
+ * date is contained in the result.
+ */
+ private Provider 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.
+ *
+ * Two configurations are possible:
+ *
+ *
+ *
Power Switch Mode (relay stays on indefinitely when switched on)
+ *
Impulse Switch Mode (relay stays on for a configured amount of time and
+ * then switches off automatically)
+ *
+ */
+ private void configureChannels() {
+ if (isInImpulseSwitchMode) {
+ configureImpulseSwitchModeChannels();
+ } else {
+ configurePowerSwitchModeChannels();
+ }
+ }
+
+ private void configureImpulseSwitchModeChannels() {
+ List channelsToBePresent = List.of(CHANNEL_IMPULSE_SWITCH, CHANNEL_IMPULSE_LENGTH,
+ CHANNEL_INSTANT_OF_LAST_IMPULSE);
+ List channelsToBeAbsent = List.of(CHANNEL_POWER_SWITCH);
+ configureChannels(channelsToBePresent, channelsToBeAbsent);
+ }
+
+ private void configurePowerSwitchModeChannels() {
+ List channelsToBePresent = List.of(CHANNEL_POWER_SWITCH);
+ List 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 channelsToBePresent, List channelsToBeAbsent) {
+ List channelsToAdd = channelsToBePresent.stream().filter(c -> getThing().getChannel(c) == null)
+ .toList();
+ List 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 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 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 currentDateTimeProvider) {
+ this.currentDateTimeProvider = currentDateTimeProvider;
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryService.java
index 50a9b893568..6f5bb912cca 100644
--- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryService.java
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/discovery/ThingDiscoveryService.java
@@ -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.),
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/AbstractStatelessBoschSHCDeviceService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/AbstractStatelessBoschSHCDeviceService.java
new file mode 100644
index 00000000000..70aa9db2ff6
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/AbstractStatelessBoschSHCDeviceService.java
@@ -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
+ 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);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/impulseswitch/ImpulseSwitchService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/impulseswitch/ImpulseSwitchService.java
new file mode 100644
index 00000000000..0e216b80334
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/impulseswitch/ImpulseSwitchService.java
@@ -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 {
+
+ public static final String IMPULSE_SWITCH_SERVICE_NAME = "ImpulseSwitch";
+
+ public ImpulseSwitchService() {
+ super(IMPULSE_SWITCH_SERVICE_NAME, ImpulseSwitchServiceState.class);
+ }
+}
diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/impulseswitch/dto/ImpulseSwitchServiceState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/impulseswitch/dto/ImpulseSwitchServiceState.java
new file mode 100644
index 00000000000..c6206d682a6
--- /dev/null
+++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/impulseswitch/dto/ImpulseSwitchServiceState.java
@@ -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.
+ *