]> git.basschouten.com Git - openhab-addons.git/blob
8dd4784cd39412d4ca6610fe2ba4facc6ba00d8e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.boschshc.internal.devices.relay;
14
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;
22
23 import java.time.Instant;
24 import java.util.List;
25 import java.util.Objects;
26
27 import javax.inject.Provider;
28
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;
56
57 /**
58  * Handler for smart relays.
59  * <p>
60  * Relays are in one of two possible modes:
61  * <ul>
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>
65  * </ul>
66  * <p>
67  * Every time the thing is initialized, we detect dynamically which mode was
68  * configured for the relay and reconfigure the channels accordingly, if
69  * required.
70  * <p>
71  * In common usage scenarios, this will be the case upon the very first
72  * initialization only, or if the device is re-purposed.
73  * 
74  * @author David Pace - Initial contribution
75  *
76  */
77 @NonNullByDefault
78 public class RelayHandler extends AbstractPowerSwitchHandler {
79
80     private final Logger logger = LoggerFactory.getLogger(RelayHandler.class);
81
82     private ChildProtectionService childProtectionService;
83     private ImpulseSwitchService impulseSwitchService;
84
85     /**
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
88      */
89     private boolean isInImpulseSwitchMode;
90
91     /**
92      * A provider for the current date/time.
93      * <p>
94      * It is exchanged in unit tests in order to be able to assert that a certain
95      * date is contained in the result.
96      */
97     private Provider<Instant> currentDateTimeProvider = Instant::now;
98
99     @Nullable
100     private ImpulseSwitchServiceState currentImpulseSwitchServiceState;
101
102     public RelayHandler(Thing thing) {
103         super(thing);
104         this.childProtectionService = new ChildProtectionService();
105         this.impulseSwitchService = new ImpulseSwitchService();
106     }
107
108     @Override
109     protected boolean processDeviceInfo(Device deviceInfo) {
110         this.isInImpulseSwitchMode = isRelayInImpulseSwitchMode(deviceInfo);
111         configureChannels();
112         return super.processDeviceInfo(deviceInfo);
113     }
114
115     /**
116      * Dynamically configures the channels according to the device mode.
117      * <p>
118      * Two configurations are possible:
119      * 
120      * <ul>
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>
124      * </ul>
125      */
126     private void configureChannels() {
127         if (isInImpulseSwitchMode) {
128             configureImpulseSwitchModeChannels();
129         } else {
130             configurePowerSwitchModeChannels();
131         }
132     }
133
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);
139     }
140
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);
146     }
147
148     /**
149      * Re-configures the channels of the associated thing, if applicable.
150      * 
151      * @param channelsToBePresent channels to be added, if not present already
152      * @param channelsToBeAbsent channels to be removed, if present
153      */
154     private void configureChannels(List<String> channelsToBePresent, List<String> channelsToBeAbsent) {
155         List<String> channelsToAdd = channelsToBePresent.stream().filter(c -> getThing().getChannel(c) == null)
156                 .toList();
157         List<Channel> channelsToRemove = channelsToBeAbsent.stream().map(c -> getThing().getChannel(c))
158                 .filter(Objects::nonNull).map(Objects::requireNonNull).toList();
159
160         if (channelsToAdd.isEmpty() && channelsToRemove.isEmpty()) {
161             return;
162         }
163
164         ThingBuilder thingBuilder = editThing();
165         if (!channelsToAdd.isEmpty()) {
166             addChannels(channelsToAdd, thingBuilder);
167         }
168         if (!channelsToRemove.isEmpty()) {
169             thingBuilder.withoutChannels(channelsToRemove);
170         }
171
172         updateThing(thingBuilder.build());
173     }
174
175     private void addChannels(List<String> channelsToAdd, ThingBuilder thingBuilder) {
176         for (String channelToAdd : channelsToAdd) {
177             Channel channel = createChannel(channelToAdd);
178             thingBuilder.withChannel(channel);
179         }
180     }
181
182     private Channel createChannel(String channelId) {
183         ChannelUID channelUID = new ChannelUID(getThing().getUID(), channelId);
184         ChannelTypeUID channelTypeUID = getChannelTypeUID(channelId);
185         @Nullable
186         String itemType = getItemType(channelId);
187         return ChannelBuilder.create(channelUID, itemType).withType(channelTypeUID).build();
188     }
189
190     private ChannelTypeUID getChannelTypeUID(String channelId) {
191         switch (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;
196             default:
197                 throw new UnsupportedOperationException(
198                         "Cannot determine channel type UID to create channel " + channelId + " dynamically.");
199         }
200     }
201
202     private @Nullable String getItemType(String channelId) {
203         switch (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;
210             default:
211                 throw new UnsupportedOperationException(
212                         "Cannot determine item type to create channel " + channelId + " dynamically.");
213         }
214     }
215
216     private boolean isRelayInImpulseSwitchMode(Device deviceInfo) {
217         List<String> serviceIds = deviceInfo.deviceServiceIds;
218         return serviceIds != null && serviceIds.contains(ImpulseSwitchService.IMPULSE_SWITCH_SERVICE_NAME);
219     }
220
221     @Override
222     protected void initializeServices() throws BoschSHCException {
223         if (!isInImpulseSwitchMode) {
224             // initialize PowerSwitch service only if the relay is not configured as impulse
225             // switch
226             super.initializeServices();
227         } else {
228             // initialize impulse switch service only if the relay is configured as impulse
229             // switch
230             registerService(impulseSwitchService, this::updateChannels,
231                     List.of(CHANNEL_IMPULSE_SWITCH, CHANNEL_IMPULSE_LENGTH, CHANNEL_INSTANT_OF_LAST_IMPULSE), true);
232         }
233
234         createService(CommunicationQualityService::new, this::updateChannels, List.of(CHANNEL_SIGNAL_STRENGTH), true);
235         registerService(childProtectionService, this::updateChannels, List.of(CHANNEL_CHILD_PROTECTION), true);
236     }
237
238     private void updateChannels(CommunicationQualityServiceState communicationQualityServiceState) {
239         updateState(CHANNEL_SIGNAL_STRENGTH, communicationQualityServiceState.quality.toSystemSignalStrength());
240     }
241
242     private void updateChannels(ChildProtectionServiceState childProtectionServiceState) {
243         updateState(CHANNEL_CHILD_PROTECTION, OnOffType.from(childProtectionServiceState.childLockActive));
244     }
245
246     private void updateChannels(ImpulseSwitchServiceState impulseSwitchServiceState) {
247         this.currentImpulseSwitchServiceState = impulseSwitchServiceState;
248
249         updateState(CHANNEL_IMPULSE_SWITCH, OnOffType.from(impulseSwitchServiceState.impulseState));
250         updateState(CHANNEL_IMPULSE_LENGTH, new DecimalType(impulseSwitchServiceState.impulseLength));
251
252         State newInstantOfLastImpulse = impulseSwitchServiceState.instantOfLastImpulse != null
253                 ? new DateTimeType(impulseSwitchServiceState.instantOfLastImpulse)
254                 : UnDefType.NULL;
255         updateState(CHANNEL_INSTANT_OF_LAST_IMPULSE, newInstantOfLastImpulse);
256     }
257
258     @Override
259     public void handleCommand(ChannelUID channelUID, Command command) {
260         super.handleCommand(channelUID, command);
261
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);
268         }
269     }
270
271     private void updateChildProtectionState(OnOffType onOffCommand) {
272         ChildProtectionServiceState childProtectionServiceState = new ChildProtectionServiceState();
273         childProtectionServiceState.childLockActive = onOffCommand == OnOffType.ON;
274         updateServiceState(childProtectionService, childProtectionServiceState);
275     }
276
277     private void triggerImpulse(OnOffType onOffCommand) {
278         if (onOffCommand != OnOffType.ON) {
279             return;
280         }
281
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);
288         }
289     }
290
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);
297
298             updateServiceState(impulseSwitchService, newState);
299             logger.debug("Successfully sent state with new impulse length to controller.");
300         }
301     }
302
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;
309             return clonedState;
310         } else {
311             logger.warn("Could not obtain current impulse switch state, command will not be processed.");
312         }
313         return null;
314     }
315
316     void setCurrentDateTimeProvider(Provider<Instant> currentDateTimeProvider) {
317         this.currentDateTimeProvider = currentDateTimeProvider;
318     }
319 }