]> git.basschouten.com Git - openhab-addons.git/blob
1b58951433386c6c58e1f776688809b8baae3c23
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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             processImpactedModules();
152         }
153     }
154
155     private void commandReceived() {
156         if (thing.getStatus() != ThingStatus.ONLINE) {
157             updateStatus(ThingStatus.ONLINE);
158         }
159
160         updateState(CHANNEL_BUTTON, OnOffType.ON);
161
162         if (!triggerProcessors.isEmpty()) {
163             long currentTimeMillis = System.currentTimeMillis();
164             triggerProcessors.forEach(processor -> processor.process(currentTimeMillis));
165         }
166
167         processImpactedModules();
168     }
169
170     private void processImpactedModules() {
171         if (!impactedModules.isEmpty()) {
172             Utils.cancel(requestUpdateFuture);
173             requestUpdateFuture = scheduler.schedule(this::update, 400, TimeUnit.MILLISECONDS);
174         }
175     }
176
177     private void update() {
178         for (ImpactedModule module : impactedModules) {
179             NikobusModuleHandler switchModule = getModuleWithId(module.getThingUID());
180             if (switchModule != null) {
181                 switchModule.requestStatus(module.getGroup());
182             }
183         }
184     }
185
186     private @Nullable NikobusModuleHandler getModuleWithId(ThingUID thingUID) {
187         Bridge bridge = getBridge();
188         if (bridge == null) {
189             return null;
190         }
191
192         Thing thing = bridge.getThing(thingUID);
193         if (thing == null) {
194             return null;
195         }
196
197         ThingHandler thingHandler = thing.getHandler();
198         if (thingHandler instanceof NikobusModuleHandler) {
199             return (NikobusModuleHandler) thingHandler;
200         }
201         return null;
202     }
203
204     @Override
205     protected String getAddress() {
206         return "#N" + super.getAddress();
207     }
208
209     private @Nullable TriggerProcessor createTriggerProcessor(Channel channel) {
210         ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
211         if (channelTypeUID != null) {
212             switch (channelTypeUID.getId()) {
213                 case CHANNEL_TRIGGER_FILTER:
214                     return new TriggerFilter(channel);
215                 case CHANNEL_TRIGGER_BUTTON:
216                     return new TriggerButton(channel);
217             }
218         }
219         return null;
220     }
221
222     private static class ImpactedModule {
223         private final ThingUID thingUID;
224         private final SwitchModuleGroup group;
225
226         ImpactedModule(ThingUID thingUID, SwitchModuleGroup group) {
227             this.thingUID = thingUID;
228             this.group = group;
229         }
230
231         public ThingUID getThingUID() {
232             return thingUID;
233         }
234
235         public SwitchModuleGroup getGroup() {
236             return group;
237         }
238
239         @Override
240         public String toString() {
241             return "'" + thingUID + "'-" + group;
242         }
243     }
244
245     private static class ImpactedModuleUID extends AbstractUID {
246         ImpactedModuleUID(String uid) {
247             super(uid);
248         }
249
250         String getThingTypeId() {
251             return getSegment(0);
252         }
253
254         String getThingId() {
255             return getSegment(1);
256         }
257
258         SwitchModuleGroup getGroup() {
259             if (getSegment(2).equals("1")) {
260                 return FIRST;
261             }
262             if (getSegment(2).equals("2")) {
263                 return SECOND;
264             }
265             throw new IllegalArgumentException("Unexpected group found " + getSegment(2));
266         }
267
268         @Override
269         protected int getMinimalNumberOfSegments() {
270             return 3;
271         }
272     }
273
274     private interface TriggerProcessor {
275         void process(long currentTimeMillis);
276     }
277
278     private abstract class AbstractTriggerProcessor<Config> implements TriggerProcessor {
279         private long lastCommandReceivedTimestamp = 0;
280         protected final ChannelUID channelUID;
281         protected final Config config;
282
283         // Nikobus push button will send a new message on bus every ~50ms so
284         // lets assume if we haven't received a new message in over 150ms that
285         // button was released and pressed again.
286         protected static final long BUTTON_RELEASED_MILIS = 150;
287
288         protected AbstractTriggerProcessor(Class<Config> configType, Channel channel) {
289             this.channelUID = channel.getUID();
290             this.config = channel.getConfiguration().as(configType);
291         }
292
293         @Override
294         public void process(long currentTimeMillis) {
295             if (Math.abs(currentTimeMillis - lastCommandReceivedTimestamp) > BUTTON_RELEASED_MILIS) {
296                 reset(currentTimeMillis);
297             }
298             lastCommandReceivedTimestamp = currentTimeMillis;
299             processNext(currentTimeMillis);
300         }
301
302         protected abstract void reset(long currentTimeMillis);
303
304         protected abstract void processNext(long currentTimeMillis);
305     }
306
307     public static class TriggerButtonConfig {
308         public int threshold = 1000;
309     }
310
311     private class TriggerButton extends AbstractTriggerProcessor<TriggerButtonConfig> {
312         private long nextLongPressTimestamp = 0;
313         private @Nullable Future<?> triggerShortPressFuture;
314
315         TriggerButton(Channel channel) {
316             super(TriggerButtonConfig.class, channel);
317         }
318
319         @Override
320         protected void reset(long currentTimeMillis) {
321             nextLongPressTimestamp = currentTimeMillis + config.threshold;
322         }
323
324         @Override
325         protected void processNext(long currentTimeMillis) {
326             if (currentTimeMillis < nextLongPressTimestamp) {
327                 Utils.cancel(triggerShortPressFuture);
328                 triggerShortPressFuture = scheduler.schedule(
329                         () -> triggerChannel(channelUID, CommonTriggerEvents.SHORT_PRESSED), BUTTON_RELEASED_MILIS,
330                         TimeUnit.MILLISECONDS);
331             } else if (nextLongPressTimestamp != 0) {
332                 Utils.cancel(triggerShortPressFuture);
333                 nextLongPressTimestamp = 0;
334                 triggerChannel(channelUID, CommonTriggerEvents.LONG_PRESSED);
335             }
336         }
337
338         @Override
339         public String toString() {
340             return "TriggerButton '" + channelUID + "', config: threshold = " + config.threshold;
341         }
342     }
343
344     public static class TriggerFilterConfig {
345         public @Nullable String command;
346         public int delay = 0;
347         public int period = -1;
348     }
349
350     private class TriggerFilter extends AbstractTriggerProcessor<TriggerFilterConfig> {
351         private long nextTriggerTimestamp = 0;
352
353         TriggerFilter(Channel channel) {
354             super(TriggerFilterConfig.class, channel);
355         }
356
357         @Override
358         protected void reset(long currentTimeMillis) {
359             nextTriggerTimestamp = currentTimeMillis + config.delay;
360         }
361
362         @Override
363         protected void processNext(long currentTimeMillis) {
364             if (currentTimeMillis >= nextTriggerTimestamp) {
365                 nextTriggerTimestamp = (config.period < 0) ? Long.MAX_VALUE : currentTimeMillis + config.period;
366                 String command = config.command;
367                 if (command != null) {
368                     triggerChannel(channelUID, command);
369                 } else {
370                     triggerChannel(channelUID);
371                 }
372             }
373         }
374
375         @Override
376         public String toString() {
377             return "TriggerFilter '" + channelUID + "', config: command = '" + config.command + "', delay = "
378                     + config.delay + ", period = " + config.period;
379         }
380     }
381 }