]> git.basschouten.com Git - openhab-addons.git/blob
ed851b0ce9bda859ea56336945b1392b12fe325e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.nikobus.internal.handler;
14
15 import static org.openhab.binding.nikobus.internal.NikobusBindingConstants.*;
16 import static org.openhab.binding.nikobus.internal.protocol.SwitchModuleGroup.*;
17
18 import java.util.List;
19 import java.util.concurrent.CopyOnWriteArrayList;
20 import java.util.concurrent.Future;
21 import java.util.concurrent.TimeUnit;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.nikobus.internal.protocol.NikobusCommand;
26 import org.openhab.binding.nikobus.internal.protocol.SwitchModuleGroup;
27 import org.openhab.binding.nikobus.internal.utils.Utils;
28 import org.openhab.core.common.AbstractUID;
29 import org.openhab.core.library.types.OnOffType;
30 import org.openhab.core.thing.Bridge;
31 import org.openhab.core.thing.Channel;
32 import org.openhab.core.thing.ChannelUID;
33 import org.openhab.core.thing.CommonTriggerEvents;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingStatus;
36 import org.openhab.core.thing.ThingStatusDetail;
37 import org.openhab.core.thing.ThingTypeUID;
38 import org.openhab.core.thing.ThingUID;
39 import org.openhab.core.thing.binding.ThingHandler;
40 import org.openhab.core.thing.type.ChannelTypeUID;
41 import org.openhab.core.types.Command;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 /**
46  * The {@link NikobusPushButtonHandler} is responsible for handling Nikobus push buttons.
47  *
48  * @author Boris Krivonog - Initial contribution
49  */
50 @NonNullByDefault
51 public class NikobusPushButtonHandler extends NikobusBaseThingHandler {
52     private static final String END_OF_TRANSMISSION = "\r#E1";
53     private final Logger logger = LoggerFactory.getLogger(NikobusPushButtonHandler.class);
54     private final List<ImpactedModule> impactedModules = new CopyOnWriteArrayList<>();
55     private final List<TriggerProcessor> triggerProcessors = new CopyOnWriteArrayList<>();
56     private @Nullable Future<?> requestUpdateFuture;
57
58     public NikobusPushButtonHandler(Thing thing) {
59         super(thing);
60     }
61
62     @Override
63     public void initialize() {
64         super.initialize();
65
66         if (thing.getStatus() == ThingStatus.OFFLINE) {
67             return;
68         }
69
70         impactedModules.clear();
71         triggerProcessors.clear();
72
73         Object impactedModulesObject = getConfig().get(CONFIG_IMPACTED_MODULES);
74         if (impactedModulesObject != null) {
75             try {
76                 Bridge bridge = getBridge();
77                 if (bridge == null) {
78                     throw new IllegalArgumentException("Bridge does not exist!");
79                 }
80
81                 ThingUID bridgeUID = thing.getBridgeUID();
82                 if (bridgeUID == null) {
83                     throw new IllegalArgumentException("Unable to read BridgeUID!");
84                 }
85
86                 String[] impactedModulesString = impactedModulesObject.toString().split(",");
87                 for (String impactedModuleString : impactedModulesString) {
88                     ImpactedModuleUID impactedModuleUID = new ImpactedModuleUID(impactedModuleString.trim());
89                     ThingTypeUID thingTypeUID = new ThingTypeUID(bridgeUID.getBindingId(),
90                             impactedModuleUID.getThingTypeId());
91                     ThingUID thingUID = new ThingUID(thingTypeUID, bridgeUID, impactedModuleUID.getThingId());
92
93                     if (!bridge.getThings().stream().anyMatch(thing -> thing.getUID().equals(thingUID))) {
94                         throw new IllegalArgumentException(
95                                 "Impacted module " + thingUID + " not found for '" + impactedModuleString + "'");
96                     }
97
98                     impactedModules.add(new ImpactedModule(thingUID, impactedModuleUID.getGroup()));
99                 }
100             } catch (RuntimeException e) {
101                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
102                 return;
103             }
104
105             logger.debug("Impacted modules for {} = {}", thing.getUID(), impactedModules);
106         }
107
108         for (Channel channel : thing.getChannels()) {
109             TriggerProcessor processor = createTriggerProcessor(channel);
110             if (processor != null) {
111                 triggerProcessors.add(processor);
112             }
113         }
114
115         logger.debug("Trigger channels for {} = {}", thing.getUID(), triggerProcessors);
116
117         NikobusPcLinkHandler pcLink = getPcLink();
118         if (pcLink != null) {
119             pcLink.addListener(getAddress(), this::commandReceived);
120         }
121     }
122
123     @Override
124     public void dispose() {
125         super.dispose();
126
127         Utils.cancel(requestUpdateFuture);
128         requestUpdateFuture = null;
129
130         NikobusPcLinkHandler pcLink = getPcLink();
131         if (pcLink != null) {
132             pcLink.removeListener(getAddress());
133         }
134     }
135
136     @Override
137     public void handleCommand(ChannelUID channelUID, Command command) {
138         logger.debug("handleCommand '{}' '{}'", channelUID, command);
139
140         if (!CHANNEL_BUTTON.equals(channelUID.getId())) {
141             return;
142         }
143
144         // Whenever the button receives an ON command,
145         // we send a simulated button press to the Nikobus.
146         if (command == OnOffType.ON) {
147             NikobusPcLinkHandler pcLink = getPcLink();
148             if (pcLink != null) {
149                 pcLink.sendCommand(new NikobusCommand(getAddress() + END_OF_TRANSMISSION));
150             }
151         }
152     }
153
154     private void commandReceived() {
155         if (thing.getStatus() != ThingStatus.ONLINE) {
156             updateStatus(ThingStatus.ONLINE);
157         }
158
159         updateState(CHANNEL_BUTTON, OnOffType.ON);
160
161         if (!triggerProcessors.isEmpty()) {
162             long currentTimeMillis = System.currentTimeMillis();
163             triggerProcessors.forEach(processor -> processor.process(currentTimeMillis));
164         }
165
166         if (!impactedModules.isEmpty()) {
167             Utils.cancel(requestUpdateFuture);
168             requestUpdateFuture = scheduler.schedule(this::update, 400, TimeUnit.MILLISECONDS);
169         }
170     }
171
172     private void update() {
173         for (ImpactedModule module : impactedModules) {
174             NikobusModuleHandler switchModule = getModuleWithId(module.getThingUID());
175             if (switchModule != null) {
176                 switchModule.requestStatus(module.getGroup());
177             }
178         }
179     }
180
181     private @Nullable NikobusModuleHandler getModuleWithId(ThingUID thingUID) {
182         Bridge bridge = getBridge();
183         if (bridge == null) {
184             return null;
185         }
186
187         Thing thing = bridge.getThing(thingUID);
188         if (thing == null) {
189             return null;
190         }
191
192         ThingHandler thingHandler = thing.getHandler();
193         if (thingHandler instanceof NikobusModuleHandler) {
194             return (NikobusModuleHandler) thingHandler;
195         }
196         return null;
197     }
198
199     @Override
200     protected String getAddress() {
201         return "#N" + super.getAddress();
202     }
203
204     private @Nullable TriggerProcessor createTriggerProcessor(Channel channel) {
205         ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
206         if (channelTypeUID != null) {
207             switch (channelTypeUID.getId()) {
208                 case CHANNEL_TRIGGER_FILTER:
209                     return new TriggerFilter(channel);
210                 case CHANNEL_TRIGGER_BUTTON:
211                     return new TriggerButton(channel);
212             }
213         }
214         return null;
215     }
216
217     private static class ImpactedModule {
218         private final ThingUID thingUID;
219         private final SwitchModuleGroup group;
220
221         ImpactedModule(ThingUID thingUID, SwitchModuleGroup group) {
222             this.thingUID = thingUID;
223             this.group = group;
224         }
225
226         public ThingUID getThingUID() {
227             return thingUID;
228         }
229
230         public SwitchModuleGroup getGroup() {
231             return group;
232         }
233
234         @Override
235         public String toString() {
236             return "'" + thingUID + "'-" + group;
237         }
238     }
239
240     private static class ImpactedModuleUID extends AbstractUID {
241         ImpactedModuleUID(String uid) {
242             super(uid);
243         }
244
245         String getThingTypeId() {
246             return getSegment(0);
247         }
248
249         String getThingId() {
250             return getSegment(1);
251         }
252
253         SwitchModuleGroup getGroup() {
254             if (getSegment(2).equals("1")) {
255                 return FIRST;
256             }
257             if (getSegment(2).equals("2")) {
258                 return SECOND;
259             }
260             throw new IllegalArgumentException("Unexpected group found " + getSegment(2));
261         }
262
263         @Override
264         protected int getMinimalNumberOfSegments() {
265             return 3;
266         }
267     }
268
269     private interface TriggerProcessor {
270         void process(long currentTimeMillis);
271     }
272
273     private abstract class AbstractTriggerProcessor<Config> implements TriggerProcessor {
274         private long lastCommandReceivedTimestamp = 0;
275         protected final ChannelUID channelUID;
276         protected final Config config;
277
278         // Nikobus push button will send a new message on bus every ~50ms so
279         // lets assume if we haven't received a new message in over 150ms that
280         // button was released and pressed again.
281         protected static final long BUTTON_RELEASED_MILIS = 150;
282
283         protected AbstractTriggerProcessor(Class<Config> configType, Channel channel) {
284             this.channelUID = channel.getUID();
285             this.config = channel.getConfiguration().as(configType);
286         }
287
288         @Override
289         public void process(long currentTimeMillis) {
290             if (Math.abs(currentTimeMillis - lastCommandReceivedTimestamp) > BUTTON_RELEASED_MILIS) {
291                 reset(currentTimeMillis);
292             }
293             lastCommandReceivedTimestamp = currentTimeMillis;
294             processNext(currentTimeMillis);
295         }
296
297         abstract protected void reset(long currentTimeMillis);
298
299         abstract protected void processNext(long currentTimeMillis);
300     }
301
302     public static class TriggerButtonConfig {
303         public int threshold = 1000;
304     }
305
306     private class TriggerButton extends AbstractTriggerProcessor<TriggerButtonConfig> {
307         private long nextLongPressTimestamp = 0;
308         private @Nullable Future<?> triggerShortPressFuture;
309
310         TriggerButton(Channel channel) {
311             super(TriggerButtonConfig.class, channel);
312         }
313
314         @Override
315         protected void reset(long currentTimeMillis) {
316             nextLongPressTimestamp = currentTimeMillis + config.threshold;
317         }
318
319         @Override
320         protected void processNext(long currentTimeMillis) {
321             if (currentTimeMillis < nextLongPressTimestamp) {
322                 Utils.cancel(triggerShortPressFuture);
323                 triggerShortPressFuture = scheduler.schedule(
324                         () -> triggerChannel(channelUID, CommonTriggerEvents.SHORT_PRESSED), BUTTON_RELEASED_MILIS,
325                         TimeUnit.MILLISECONDS);
326             } else if (nextLongPressTimestamp != 0) {
327                 Utils.cancel(triggerShortPressFuture);
328                 nextLongPressTimestamp = 0;
329                 triggerChannel(channelUID, CommonTriggerEvents.LONG_PRESSED);
330             }
331         }
332
333         @Override
334         public String toString() {
335             return "TriggerButton '" + channelUID + "', config: threshold = " + config.threshold;
336         }
337     }
338
339     public static class TriggerFilterConfig {
340         public @Nullable String command;
341         public int delay = 0;
342         public int period = -1;
343     }
344
345     private class TriggerFilter extends AbstractTriggerProcessor<TriggerFilterConfig> {
346         private long nextTriggerTimestamp = 0;
347
348         TriggerFilter(Channel channel) {
349             super(TriggerFilterConfig.class, channel);
350         }
351
352         @Override
353         protected void reset(long currentTimeMillis) {
354             nextTriggerTimestamp = currentTimeMillis + config.delay;
355         }
356
357         @Override
358         protected void processNext(long currentTimeMillis) {
359             if (currentTimeMillis >= nextTriggerTimestamp) {
360                 nextTriggerTimestamp = (config.period < 0) ? Long.MAX_VALUE : currentTimeMillis + config.period;
361                 String command = config.command;
362                 if (command != null) {
363                     triggerChannel(channelUID, command);
364                 } else {
365                     triggerChannel(channelUID);
366                 }
367             }
368         }
369
370         @Override
371         public String toString() {
372             return "TriggerFilter '" + channelUID + "', config: command = '" + config.command + "', delay = "
373                     + config.delay + ", period = " + config.period;
374         }
375     }
376 }