]> git.basschouten.com Git - openhab-addons.git/blob
453beb1f953c0b95ee11005d9a77c9e6fefdfe57
[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.alarmdecoder.internal.handler;
14
15 import static org.openhab.binding.alarmdecoder.internal.AlarmDecoderBindingConstants.*;
16
17 import java.io.BufferedReader;
18 import java.io.BufferedWriter;
19 import java.io.IOException;
20 import java.nio.charset.Charset;
21 import java.nio.charset.StandardCharsets;
22 import java.util.Collection;
23 import java.util.Date;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.alarmdecoder.internal.AlarmDecoderDiscoveryService;
32 import org.openhab.binding.alarmdecoder.internal.actions.BridgeActions;
33 import org.openhab.binding.alarmdecoder.internal.protocol.ADCommand;
34 import org.openhab.binding.alarmdecoder.internal.protocol.ADMessage;
35 import org.openhab.binding.alarmdecoder.internal.protocol.ADMsgType;
36 import org.openhab.binding.alarmdecoder.internal.protocol.EXPMessage;
37 import org.openhab.binding.alarmdecoder.internal.protocol.KeypadMessage;
38 import org.openhab.binding.alarmdecoder.internal.protocol.LRRMessage;
39 import org.openhab.binding.alarmdecoder.internal.protocol.RFXMessage;
40 import org.openhab.binding.alarmdecoder.internal.protocol.VersionMessage;
41 import org.openhab.core.thing.Bridge;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.thing.Thing;
44 import org.openhab.core.thing.ThingStatus;
45 import org.openhab.core.thing.ThingStatusDetail;
46 import org.openhab.core.thing.binding.BaseBridgeHandler;
47 import org.openhab.core.thing.binding.ThingHandlerService;
48 import org.openhab.core.types.Command;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51
52 /**
53  * Abstract base class for bridge handlers responsible for communicating with the Nu Tech Alarm Decoder devices.
54  * Based partly on and including code from the original OH1 alarmdecoder binding by Bernd Pfrommer.
55  *
56  * @author Bernd Pfrommer - Initial contribution (OH1 version)
57  * @author Bob Adair - Re-factored into OH2 binding
58  */
59 @NonNullByDefault
60 public abstract class ADBridgeHandler extends BaseBridgeHandler {
61     protected static final Charset AD_CHARSET = StandardCharsets.UTF_8;
62
63     private final Logger logger = LoggerFactory.getLogger(ADBridgeHandler.class);
64
65     protected @Nullable BufferedReader reader = null;
66     protected @Nullable BufferedWriter writer = null;
67     protected @Nullable Thread msgReaderThread = null;
68     private final Object msgReaderThreadLock = new Object();
69     protected @Nullable AlarmDecoderDiscoveryService discoveryService;
70     protected boolean discovery;
71     protected boolean panelReadyReceived = false;
72     protected volatile @Nullable Date lastReceivedTime;
73     protected volatile boolean writeException;
74
75     protected @Nullable ScheduledFuture<?> connectionCheckJob;
76     protected @Nullable ScheduledFuture<?> connectRetryJob;
77
78     public ADBridgeHandler(Bridge bridge) {
79         super(bridge);
80     }
81
82     @Override
83     public void dispose() {
84         logger.trace("dispose called");
85         disconnect();
86     }
87
88     @Override
89     public Collection<Class<? extends ThingHandlerService>> getServices() {
90         return List.of(BridgeActions.class);
91     }
92
93     public void setDiscoveryService(AlarmDecoderDiscoveryService discoveryService) {
94         this.discoveryService = discoveryService;
95     }
96
97     @Override
98     public void handleCommand(ChannelUID channelUID, Command command) {
99         // Accepts no commands, so do nothing.
100     }
101
102     /**
103      * Send a command to the alarm decoder using a buffered writer. This could block if the buffer is full, so it should
104      * eventually be replaced with a queuing mechanism and a separate writer thread.
105      *
106      * @param command Command string to send including terminator
107      */
108     public void sendADCommand(ADCommand command) {
109         logger.debug("Sending AD command: {}", command);
110         try {
111             BufferedWriter bw = writer;
112             if (bw != null) {
113                 bw.write(command.toString());
114                 bw.flush();
115             }
116         } catch (IOException e) {
117             logger.info("Exception while sending command: {}", e.getMessage());
118             writeException = true;
119         }
120     }
121
122     protected abstract void connect();
123
124     protected abstract void disconnect();
125
126     protected void scheduleConnectRetry(long waitMinutes) {
127         logger.debug("Scheduling connection retry in {} minutes", waitMinutes);
128         connectRetryJob = scheduler.schedule(this::connect, waitMinutes, TimeUnit.MINUTES);
129     }
130
131     protected void startMsgReader() {
132         synchronized (msgReaderThreadLock) {
133             Thread mrt = new Thread(this::readerThread, "OH-binding-" + getThing().getUID() + "-ADReader");
134             mrt.setDaemon(true);
135             mrt.start();
136             msgReaderThread = mrt;
137         }
138     }
139
140     protected void stopMsgReader() {
141         synchronized (msgReaderThreadLock) {
142             Thread mrt = msgReaderThread;
143             if (mrt != null) {
144                 logger.trace("Stopping reader thread.");
145                 mrt.interrupt();
146                 msgReaderThread = null;
147             }
148         }
149     }
150
151     /**
152      * Method executed by message reader thread
153      */
154     private void readerThread() {
155         logger.debug("Message reader thread started");
156         String message = null;
157         try {
158             // Send version command to get device to respond with VER message.
159             sendADCommand(ADCommand.getVersion());
160             BufferedReader reader = this.reader;
161             while (!Thread.interrupted() && reader != null && (message = reader.readLine()) != null) {
162                 logger.trace("Received msg: {}", message);
163                 ADMsgType msgType = ADMsgType.getMsgType(message);
164                 if (msgType != ADMsgType.INVALID) {
165                     lastReceivedTime = new Date();
166                 }
167                 try {
168                     switch (msgType) {
169                         case KPM:
170                             parseKeypadMessage(message);
171                             break;
172                         case REL:
173                         case EXP:
174                             parseRelayOrExpanderMessage(msgType, message);
175                             break;
176                         case RFX:
177                             parseRFMessage(message);
178                             break;
179                         case LRR:
180                             parseLRRMessage(message);
181                             break;
182                         case VER:
183                             parseVersionMessage(message);
184                             break;
185                         case INVALID:
186                         default:
187                             break;
188                     }
189                 } catch (MessageParseException e) {
190                     logger.warn("Error {} while parsing message {}. Please report bug.", e.getMessage(), message);
191                 }
192             }
193             if (message == null) {
194                 logger.info("End of input stream detected");
195                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connection lost");
196             }
197         } catch (IOException e) {
198             logger.debug("I/O error while reading from stream: {}", e.getMessage());
199             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
200         } catch (RuntimeException e) {
201             logger.warn("Runtime exception in reader thread", e);
202             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
203         } finally {
204             logger.debug("Message reader thread exiting");
205         }
206     }
207
208     /**
209      * Parse and handle keypad messages
210      *
211      * @param msg string containing incoming message payload
212      * @throws MessageParseException
213      */
214     private void parseKeypadMessage(String msg) throws MessageParseException {
215         KeypadMessage kpMsg;
216
217         // Parse the message
218         try {
219             kpMsg = new KeypadMessage(msg);
220         } catch (IllegalArgumentException e) {
221             throw new MessageParseException(e.getMessage());
222         }
223
224         if (kpMsg.panelClear()) {
225             // the panel is clear, so we can assume that all contacts that we
226             // have not heard from are open
227             notifyChildHandlersPanelReady();
228         }
229
230         notifyChildHandlers(kpMsg);
231     }
232
233     /**
234      * Parse and handle relay and expander messages. The REL and EXP messages have identical format.
235      *
236      * @param mt message type of incoming message
237      * @param msg string containing incoming message payload
238      * @throws MessageParseException
239      */
240     private void parseRelayOrExpanderMessage(ADMsgType mt, String msg) throws MessageParseException {
241         // mt is unused at the moment
242         EXPMessage expMsg;
243
244         try {
245             expMsg = new EXPMessage(msg);
246         } catch (IllegalArgumentException e) {
247             throw new MessageParseException(e.getMessage());
248         }
249
250         notifyChildHandlers(expMsg);
251
252         AlarmDecoderDiscoveryService ds = discoveryService;
253         if (discovery && ds != null) {
254             ds.processZone(expMsg.address, expMsg.channel);
255         }
256     }
257
258     /**
259      * Parse and handle RFX messages.
260      *
261      * @param msg string containing incoming message payload
262      * @throws MessageParseException
263      */
264     private void parseRFMessage(String msg) throws MessageParseException {
265         RFXMessage rfxMsg;
266
267         try {
268             rfxMsg = new RFXMessage(msg);
269         } catch (IllegalArgumentException e) {
270             throw new MessageParseException(e.getMessage());
271         }
272
273         notifyChildHandlers(rfxMsg);
274
275         AlarmDecoderDiscoveryService ds = discoveryService;
276         if (discovery && ds != null) {
277             ds.processRFZone(rfxMsg.serial);
278         }
279     }
280
281     /**
282      * Parse and handle LRR messages.
283      *
284      * @param msg string containing incoming message payload
285      * @throws MessageParseException
286      */
287     private void parseLRRMessage(String msg) throws MessageParseException {
288         LRRMessage lrrMsg;
289
290         // Parse the message
291         try {
292             lrrMsg = new LRRMessage(msg);
293         } catch (IllegalArgumentException e) {
294             throw new MessageParseException(e.getMessage());
295         }
296
297         notifyChildHandlers(lrrMsg);
298     }
299
300     /**
301      * Parse and handle version (VER) message. This just updates bridge properties.
302      *
303      * @param msg string containing incoming message payload
304      * @throws MessageParseException
305      */
306     private void parseVersionMessage(String msg) throws MessageParseException {
307         VersionMessage verMsg;
308
309         try {
310             verMsg = new VersionMessage(msg);
311         } catch (IllegalArgumentException e) {
312             throw new MessageParseException(e.getMessage());
313         }
314
315         logger.trace("Processing version message sn:{} ver:{} cap:{}", verMsg.serial, verMsg.version,
316                 verMsg.capabilities);
317         Map<String, String> properties = editProperties();
318         properties.put(PROPERTY_SERIALNUM, verMsg.serial);
319         properties.put(PROPERTY_VERSION, verMsg.version);
320         properties.put(PROPERTY_CAPABILITIES, verMsg.capabilities);
321         updateProperties(properties);
322     }
323
324     /**
325      * Notify appropriate child thing handlers of an AD message by calling their handleUpdate() methods.
326      *
327      * @param msg message to forward to child handler(s)
328      */
329     private void notifyChildHandlers(ADMessage msg) {
330         for (Thing thing : getThing().getThings()) {
331             ADThingHandler handler = (ADThingHandler) thing.getHandler();
332             //@formatter:off
333             if (handler != null && ((handler instanceof ZoneHandler && msg instanceof EXPMessage) ||
334                                     (handler instanceof RFZoneHandler && msg instanceof RFXMessage) ||
335                                     (handler instanceof KeypadHandler && msg instanceof KeypadMessage) ||
336                                     (handler instanceof LRRHandler && msg instanceof LRRMessage))) {
337                 handler.handleUpdate(msg);
338             }
339             //@formatter:on
340         }
341     }
342
343     /**
344      * Notify child thing handlers that the alarm panel is in the ready state. Since there is no way to poll, all
345      * contact channels are initialized into the UNDEF state. This method is called when there is reason to assume that
346      * there are no faulted zones, because the alarm panel is in state READY. Zone handlers that have not yet received
347      * updates can then set their contact states to CLOSED. Only executes the first time panel is ready after bridge
348      * connect/reconnect.
349      */
350     private void notifyChildHandlersPanelReady() {
351         if (!panelReadyReceived) {
352             panelReadyReceived = true;
353             logger.trace("Notifying child handlers that panel is in ready state");
354
355             // Notify child zone handlers by calling notifyPanelReady() for each
356             for (Thing thing : getThing().getThings()) {
357                 ADThingHandler handler = (ADThingHandler) thing.getHandler();
358                 if (handler != null) {
359                     handler.notifyPanelReady();
360                 }
361             }
362         }
363     }
364
365     /**
366      * Exception thrown by message parsing code when it encounters a malformed message
367      */
368     private static class MessageParseException extends Exception {
369         private static final long serialVersionUID = 1L;
370
371         public MessageParseException(@Nullable String msg) {
372             super(msg);
373         }
374     }
375 }