2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.boschshc.internal.devices.relay;
15 import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.BINDING_ID;
16 import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_CHILD_PROTECTION;
17 import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_IMPULSE_LENGTH;
18 import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_IMPULSE_SWITCH;
19 import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_INSTANT_OF_LAST_IMPULSE;
20 import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_POWER_SWITCH;
21 import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_SIGNAL_STRENGTH;
23 import java.time.Instant;
24 import java.util.List;
25 import java.util.Objects;
27 import javax.inject.Provider;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandler;
32 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
33 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
34 import org.openhab.binding.boschshc.internal.services.childprotection.ChildProtectionService;
35 import org.openhab.binding.boschshc.internal.services.childprotection.dto.ChildProtectionServiceState;
36 import org.openhab.binding.boschshc.internal.services.communicationquality.CommunicationQualityService;
37 import org.openhab.binding.boschshc.internal.services.communicationquality.dto.CommunicationQualityServiceState;
38 import org.openhab.binding.boschshc.internal.services.impulseswitch.ImpulseSwitchService;
39 import org.openhab.binding.boschshc.internal.services.impulseswitch.dto.ImpulseSwitchServiceState;
40 import org.openhab.core.library.CoreItemFactory;
41 import org.openhab.core.library.types.DateTimeType;
42 import org.openhab.core.library.types.DecimalType;
43 import org.openhab.core.library.types.OnOffType;
44 import org.openhab.core.thing.Channel;
45 import org.openhab.core.thing.ChannelUID;
46 import org.openhab.core.thing.DefaultSystemChannelTypeProvider;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.thing.binding.builder.ChannelBuilder;
49 import org.openhab.core.thing.binding.builder.ThingBuilder;
50 import org.openhab.core.thing.type.ChannelTypeUID;
51 import org.openhab.core.types.Command;
52 import org.openhab.core.types.State;
53 import org.openhab.core.types.UnDefType;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
58 * Handler for smart relays.
60 * Relays are in one of two possible modes:
62 * <li>Power switch mode: a switch is used to toggle the relay on / off</li>
63 * <li>Impulse switch: the relay is triggered by an impulse and automatically
64 * switches off after a configured period of time</li>
67 * Every time the thing is initialized, we detect dynamically which mode was
68 * configured for the relay and reconfigure the channels accordingly, if
71 * In common usage scenarios, this will be the case upon the very first
72 * initialization only, or if the device is re-purposed.
74 * @author David Pace - Initial contribution
78 public class RelayHandler extends AbstractPowerSwitchHandler {
80 private final Logger logger = LoggerFactory.getLogger(RelayHandler.class);
82 private ChildProtectionService childProtectionService;
83 private ImpulseSwitchService impulseSwitchService;
86 * Indicates whether the relay is configured in impulse switch mode. If this is
87 * <code>false</code>, the relay is in the default power switch (toggle) mode
89 private boolean isInImpulseSwitchMode;
92 * A provider for the current date/time.
94 * It is exchanged in unit tests in order to be able to assert that a certain
95 * date is contained in the result.
97 private Provider<Instant> currentDateTimeProvider = Instant::now;
100 private ImpulseSwitchServiceState currentImpulseSwitchServiceState;
102 public RelayHandler(Thing thing) {
104 this.childProtectionService = new ChildProtectionService();
105 this.impulseSwitchService = new ImpulseSwitchService();
109 protected boolean processDeviceInfo(Device deviceInfo) {
110 this.isInImpulseSwitchMode = isRelayInImpulseSwitchMode(deviceInfo);
112 return super.processDeviceInfo(deviceInfo);
116 * Dynamically configures the channels according to the device mode.
118 * Two configurations are possible:
121 * <li>Power Switch Mode (relay stays on indefinitely when switched on)</li>
122 * <li>Impulse Switch Mode (relay stays on for a configured amount of time and
123 * then switches off automatically)</li>
126 private void configureChannels() {
127 if (isInImpulseSwitchMode) {
128 configureImpulseSwitchModeChannels();
130 configurePowerSwitchModeChannels();
134 private void configureImpulseSwitchModeChannels() {
135 List<String> channelsToBePresent = List.of(CHANNEL_IMPULSE_SWITCH, CHANNEL_IMPULSE_LENGTH,
136 CHANNEL_INSTANT_OF_LAST_IMPULSE);
137 List<String> channelsToBeAbsent = List.of(CHANNEL_POWER_SWITCH);
138 configureChannels(channelsToBePresent, channelsToBeAbsent);
141 private void configurePowerSwitchModeChannels() {
142 List<String> channelsToBePresent = List.of(CHANNEL_POWER_SWITCH);
143 List<String> channelsToBeAbsent = List.of(CHANNEL_IMPULSE_SWITCH, CHANNEL_IMPULSE_LENGTH,
144 CHANNEL_INSTANT_OF_LAST_IMPULSE);
145 configureChannels(channelsToBePresent, channelsToBeAbsent);
149 * Re-configures the channels of the associated thing, if applicable.
151 * @param channelsToBePresent channels to be added, if not present already
152 * @param channelsToBeAbsent channels to be removed, if present
154 private void configureChannels(List<String> channelsToBePresent, List<String> channelsToBeAbsent) {
155 List<String> channelsToAdd = channelsToBePresent.stream().filter(c -> getThing().getChannel(c) == null)
157 List<Channel> channelsToRemove = channelsToBeAbsent.stream().map(c -> getThing().getChannel(c))
158 .filter(Objects::nonNull).map(Objects::requireNonNull).toList();
160 if (channelsToAdd.isEmpty() && channelsToRemove.isEmpty()) {
164 ThingBuilder thingBuilder = editThing();
165 if (!channelsToAdd.isEmpty()) {
166 addChannels(channelsToAdd, thingBuilder);
168 if (!channelsToRemove.isEmpty()) {
169 thingBuilder.withoutChannels(channelsToRemove);
172 updateThing(thingBuilder.build());
175 private void addChannels(List<String> channelsToAdd, ThingBuilder thingBuilder) {
176 for (String channelToAdd : channelsToAdd) {
177 Channel channel = createChannel(channelToAdd);
178 thingBuilder.withChannel(channel);
182 private Channel createChannel(String channelId) {
183 ChannelUID channelUID = new ChannelUID(getThing().getUID(), channelId);
184 ChannelTypeUID channelTypeUID = getChannelTypeUID(channelId);
186 String itemType = getItemType(channelId);
187 return ChannelBuilder.create(channelUID, itemType).withType(channelTypeUID).build();
190 private ChannelTypeUID getChannelTypeUID(String channelId) {
192 case CHANNEL_IMPULSE_SWITCH, CHANNEL_IMPULSE_LENGTH, CHANNEL_INSTANT_OF_LAST_IMPULSE:
193 return new ChannelTypeUID(BINDING_ID, channelId);
194 case CHANNEL_POWER_SWITCH:
195 return DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_POWER;
197 throw new UnsupportedOperationException(
198 "Cannot determine channel type UID to create channel " + channelId + " dynamically.");
202 private @Nullable String getItemType(String channelId) {
204 case CHANNEL_POWER_SWITCH, CHANNEL_IMPULSE_SWITCH:
205 return CoreItemFactory.SWITCH;
206 case CHANNEL_IMPULSE_LENGTH:
207 return CoreItemFactory.NUMBER + ":Time";
208 case CHANNEL_INSTANT_OF_LAST_IMPULSE:
209 return CoreItemFactory.DATETIME;
211 throw new UnsupportedOperationException(
212 "Cannot determine item type to create channel " + channelId + " dynamically.");
216 private boolean isRelayInImpulseSwitchMode(Device deviceInfo) {
217 List<String> serviceIds = deviceInfo.deviceServiceIds;
218 return serviceIds != null && serviceIds.contains(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME);
222 protected void initializeServices() throws BoschSHCException {
223 if (!isInImpulseSwitchMode) {
224 // initialize PowerSwitch service only if the relay is not configured as impulse
226 super.initializeServices();
228 // initialize impulse switch service only if the relay is configured as impulse
230 registerService(impulseSwitchService, this::updateChannels,
231 List.of(CHANNEL_IMPULSE_SWITCH, CHANNEL_IMPULSE_LENGTH, CHANNEL_INSTANT_OF_LAST_IMPULSE), true);
234 createService(CommunicationQualityService::new, this::updateChannels, List.of(CHANNEL_SIGNAL_STRENGTH), true);
235 registerService(childProtectionService, this::updateChannels, List.of(CHANNEL_CHILD_PROTECTION), true);
238 private void updateChannels(CommunicationQualityServiceState communicationQualityServiceState) {
239 updateState(CHANNEL_SIGNAL_STRENGTH, communicationQualityServiceState.quality.toSystemSignalStrength());
242 private void updateChannels(ChildProtectionServiceState childProtectionServiceState) {
243 updateState(CHANNEL_CHILD_PROTECTION, OnOffType.from(childProtectionServiceState.childLockActive));
246 private void updateChannels(ImpulseSwitchServiceState impulseSwitchServiceState) {
247 this.currentImpulseSwitchServiceState = impulseSwitchServiceState;
249 updateState(CHANNEL_IMPULSE_SWITCH, OnOffType.from(impulseSwitchServiceState.impulseState));
250 updateState(CHANNEL_IMPULSE_LENGTH, new DecimalType(impulseSwitchServiceState.impulseLength));
252 State newInstantOfLastImpulse = impulseSwitchServiceState.instantOfLastImpulse != null
253 ? new DateTimeType(impulseSwitchServiceState.instantOfLastImpulse)
255 updateState(CHANNEL_INSTANT_OF_LAST_IMPULSE, newInstantOfLastImpulse);
259 public void handleCommand(ChannelUID channelUID, Command command) {
260 super.handleCommand(channelUID, command);
262 if (CHANNEL_CHILD_PROTECTION.equals(channelUID.getId()) && (command instanceof OnOffType onOffCommand)) {
263 updateChildProtectionState(onOffCommand);
264 } else if (CHANNEL_IMPULSE_SWITCH.equals(channelUID.getId()) && command instanceof OnOffType onOffCommand) {
265 triggerImpulse(onOffCommand);
266 } else if (CHANNEL_IMPULSE_LENGTH.equals(channelUID.getId()) && command instanceof DecimalType number) {
267 updateImpulseLength(number);
271 private void updateChildProtectionState(OnOffType onOffCommand) {
272 ChildProtectionServiceState childProtectionServiceState = new ChildProtectionServiceState();
273 childProtectionServiceState.childLockActive = onOffCommand == OnOffType.ON;
274 updateServiceState(childProtectionService, childProtectionServiceState);
277 private void triggerImpulse(OnOffType onOffCommand) {
278 if (onOffCommand != OnOffType.ON) {
282 ImpulseSwitchServiceState newState = cloneCurrentImpulseSwitchServiceState();
283 if (newState != null) {
284 newState.impulseState = true;
285 newState.instantOfLastImpulse = currentDateTimeProvider.get().toString();
286 this.currentImpulseSwitchServiceState = newState;
287 updateServiceState(impulseSwitchService, newState);
291 private void updateImpulseLength(DecimalType number) {
292 ImpulseSwitchServiceState newState = cloneCurrentImpulseSwitchServiceState();
293 if (newState != null) {
294 newState.impulseLength = number.intValue();
295 this.currentImpulseSwitchServiceState = newState;
296 logger.debug("New impulse length setting for relay: {} deciseconds", newState.impulseLength);
298 updateServiceState(impulseSwitchService, newState);
299 logger.debug("Successfully sent state with new impulse length to controller.");
303 private @Nullable ImpulseSwitchServiceState cloneCurrentImpulseSwitchServiceState() {
304 if (currentImpulseSwitchServiceState != null) {
305 ImpulseSwitchServiceState clonedState = new ImpulseSwitchServiceState();
306 clonedState.impulseState = currentImpulseSwitchServiceState.impulseState;
307 clonedState.impulseLength = currentImpulseSwitchServiceState.impulseLength;
308 clonedState.instantOfLastImpulse = currentImpulseSwitchServiceState.instantOfLastImpulse;
311 logger.warn("Could not obtain current impulse switch state, command will not be processed.");
316 void setCurrentDateTimeProvider(Provider<Instant> currentDateTimeProvider) {
317 this.currentDateTimeProvider = currentDateTimeProvider;