2 * Copyright (c) 2010-2023 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.nikobus.internal.handler;
15 import static org.openhab.binding.nikobus.internal.NikobusBindingConstants.*;
16 import static org.openhab.binding.nikobus.internal.protocol.SwitchModuleGroup.*;
18 import java.util.List;
19 import java.util.concurrent.CopyOnWriteArrayList;
20 import java.util.concurrent.Future;
21 import java.util.concurrent.TimeUnit;
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;
46 * The {@link NikobusPushButtonHandler} is responsible for handling Nikobus push buttons.
48 * @author Boris Krivonog - Initial contribution
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;
58 public NikobusPushButtonHandler(Thing thing) {
63 public void initialize() {
66 if (thing.getStatus() == ThingStatus.OFFLINE) {
70 impactedModules.clear();
71 triggerProcessors.clear();
73 Object impactedModulesObject = getConfig().get(CONFIG_IMPACTED_MODULES);
74 if (impactedModulesObject != null) {
76 Bridge bridge = getBridge();
78 throw new IllegalArgumentException("Bridge does not exist!");
81 ThingUID bridgeUID = thing.getBridgeUID();
82 if (bridgeUID == null) {
83 throw new IllegalArgumentException("Unable to read BridgeUID!");
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());
93 if (!bridge.getThings().stream().anyMatch(thing -> thing.getUID().equals(thingUID))) {
94 throw new IllegalArgumentException(
95 "Impacted module " + thingUID + " not found for '" + impactedModuleString + "'");
98 impactedModules.add(new ImpactedModule(thingUID, impactedModuleUID.getGroup()));
100 } catch (RuntimeException e) {
101 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
105 logger.debug("Impacted modules for {} = {}", thing.getUID(), impactedModules);
108 for (Channel channel : thing.getChannels()) {
109 TriggerProcessor processor = createTriggerProcessor(channel);
110 if (processor != null) {
111 triggerProcessors.add(processor);
115 logger.debug("Trigger channels for {} = {}", thing.getUID(), triggerProcessors);
117 NikobusPcLinkHandler pcLink = getPcLink();
118 if (pcLink != null) {
119 pcLink.addListener(getAddress(), this::commandReceived);
124 public void dispose() {
127 Utils.cancel(requestUpdateFuture);
128 requestUpdateFuture = null;
130 NikobusPcLinkHandler pcLink = getPcLink();
131 if (pcLink != null) {
132 pcLink.removeListener(getAddress());
137 public void handleCommand(ChannelUID channelUID, Command command) {
138 logger.debug("handleCommand '{}' '{}'", channelUID, command);
140 if (!CHANNEL_BUTTON.equals(channelUID.getId())) {
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));
151 processImpactedModules();
155 private void commandReceived() {
156 if (thing.getStatus() != ThingStatus.ONLINE) {
157 updateStatus(ThingStatus.ONLINE);
160 updateState(CHANNEL_BUTTON, OnOffType.ON);
162 if (!triggerProcessors.isEmpty()) {
163 long currentTimeMillis = System.currentTimeMillis();
164 triggerProcessors.forEach(processor -> processor.process(currentTimeMillis));
167 processImpactedModules();
170 private void processImpactedModules() {
171 if (!impactedModules.isEmpty()) {
172 Utils.cancel(requestUpdateFuture);
173 requestUpdateFuture = scheduler.schedule(this::update, 400, TimeUnit.MILLISECONDS);
177 private void update() {
178 for (ImpactedModule module : impactedModules) {
179 NikobusModuleHandler switchModule = getModuleWithId(module.getThingUID());
180 if (switchModule != null) {
181 switchModule.requestStatus(module.getGroup());
186 private @Nullable NikobusModuleHandler getModuleWithId(ThingUID thingUID) {
187 Bridge bridge = getBridge();
188 if (bridge == null) {
192 Thing thing = bridge.getThing(thingUID);
197 ThingHandler thingHandler = thing.getHandler();
198 if (thingHandler instanceof NikobusModuleHandler) {
199 return (NikobusModuleHandler) thingHandler;
205 protected String getAddress() {
206 return "#N" + super.getAddress();
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);
222 private static class ImpactedModule {
223 private final ThingUID thingUID;
224 private final SwitchModuleGroup group;
226 ImpactedModule(ThingUID thingUID, SwitchModuleGroup group) {
227 this.thingUID = thingUID;
231 public ThingUID getThingUID() {
235 public SwitchModuleGroup getGroup() {
240 public String toString() {
241 return "'" + thingUID + "'-" + group;
245 private static class ImpactedModuleUID extends AbstractUID {
246 ImpactedModuleUID(String uid) {
250 String getThingTypeId() {
251 return getSegment(0);
254 String getThingId() {
255 return getSegment(1);
258 SwitchModuleGroup getGroup() {
259 if (getSegment(2).equals("1")) {
262 if (getSegment(2).equals("2")) {
265 throw new IllegalArgumentException("Unexpected group found " + getSegment(2));
269 protected int getMinimalNumberOfSegments() {
274 private interface TriggerProcessor {
275 void process(long currentTimeMillis);
278 private abstract class AbstractTriggerProcessor<Config> implements TriggerProcessor {
279 private long lastCommandReceivedTimestamp = 0;
280 protected final ChannelUID channelUID;
281 protected final Config config;
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;
288 protected AbstractTriggerProcessor(Class<Config> configType, Channel channel) {
289 this.channelUID = channel.getUID();
290 this.config = channel.getConfiguration().as(configType);
294 public void process(long currentTimeMillis) {
295 if (Math.abs(currentTimeMillis - lastCommandReceivedTimestamp) > BUTTON_RELEASED_MILIS) {
296 reset(currentTimeMillis);
298 lastCommandReceivedTimestamp = currentTimeMillis;
299 processNext(currentTimeMillis);
302 protected abstract void reset(long currentTimeMillis);
304 protected abstract void processNext(long currentTimeMillis);
307 public static class TriggerButtonConfig {
308 public int threshold = 1000;
311 private class TriggerButton extends AbstractTriggerProcessor<TriggerButtonConfig> {
312 private long nextLongPressTimestamp = 0;
313 private @Nullable Future<?> triggerShortPressFuture;
315 TriggerButton(Channel channel) {
316 super(TriggerButtonConfig.class, channel);
320 protected void reset(long currentTimeMillis) {
321 nextLongPressTimestamp = currentTimeMillis + config.threshold;
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);
339 public String toString() {
340 return "TriggerButton '" + channelUID + "', config: threshold = " + config.threshold;
344 public static class TriggerFilterConfig {
345 public @Nullable String command;
346 public int delay = 0;
347 public int period = -1;
350 private class TriggerFilter extends AbstractTriggerProcessor<TriggerFilterConfig> {
351 private long nextTriggerTimestamp = 0;
353 TriggerFilter(Channel channel) {
354 super(TriggerFilterConfig.class, channel);
358 protected void reset(long currentTimeMillis) {
359 nextTriggerTimestamp = currentTimeMillis + config.delay;
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);
370 triggerChannel(channelUID);
376 public String toString() {
377 return "TriggerFilter '" + channelUID + "', config: command = '" + config.command + "', delay = "
378 + config.delay + ", period = " + config.period;