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.alarmdecoder.internal.handler;
15 import static org.openhab.binding.alarmdecoder.internal.AlarmDecoderBindingConstants.*;
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.Collections;
24 import java.util.Date;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
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;
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.
56 * @author Bernd Pfrommer - Initial contribution (OH1 version)
57 * @author Bob Adair - Re-factored into OH2 binding
60 public abstract class ADBridgeHandler extends BaseBridgeHandler {
61 protected static final Charset AD_CHARSET = StandardCharsets.UTF_8;
63 private final Logger logger = LoggerFactory.getLogger(ADBridgeHandler.class);
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;
75 protected @Nullable ScheduledFuture<?> connectionCheckJob;
76 protected @Nullable ScheduledFuture<?> connectRetryJob;
78 public ADBridgeHandler(Bridge bridge) {
83 public void dispose() {
84 logger.trace("dispose called");
89 public Collection<Class<? extends ThingHandlerService>> getServices() {
90 return Collections.singletonList(BridgeActions.class);
93 public void setDiscoveryService(AlarmDecoderDiscoveryService discoveryService) {
94 this.discoveryService = discoveryService;
98 public void handleCommand(ChannelUID channelUID, Command command) {
99 // Accepts no commands, so do nothing.
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.
106 * @param command Command string to send including terminator
108 public void sendADCommand(ADCommand command) {
109 logger.debug("Sending AD command: {}", command);
111 BufferedWriter bw = writer;
113 bw.write(command.toString());
116 } catch (IOException e) {
117 logger.info("Exception while sending command: {}", e.getMessage());
118 writeException = true;
122 protected abstract void connect();
124 protected abstract void disconnect();
126 protected void scheduleConnectRetry(long waitMinutes) {
127 logger.debug("Scheduling connection retry in {} minutes", waitMinutes);
128 connectRetryJob = scheduler.schedule(this::connect, waitMinutes, TimeUnit.MINUTES);
131 protected void startMsgReader() {
132 synchronized (msgReaderThreadLock) {
133 Thread mrt = new Thread(this::readerThread, "OH-binding-" + getThing().getUID() + "-ADReader");
136 msgReaderThread = mrt;
140 protected void stopMsgReader() {
141 synchronized (msgReaderThreadLock) {
142 Thread mrt = msgReaderThread;
144 logger.trace("Stopping reader thread.");
146 msgReaderThread = null;
152 * Method executed by message reader thread
154 private void readerThread() {
155 logger.debug("Message reader thread started");
156 String message = null;
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();
170 parseKeypadMessage(message);
174 parseRelayOrExpanderMessage(msgType, message);
177 parseRFMessage(message);
180 parseLRRMessage(message);
183 parseVersionMessage(message);
189 } catch (MessageParseException e) {
190 logger.warn("Error {} while parsing message {}. Please report bug.", e.getMessage(), message);
193 if (message == null) {
194 logger.info("End of input stream detected");
195 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connection lost");
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());
204 logger.debug("Message reader thread exiting");
209 * Parse and handle keypad messages
211 * @param msg string containing incoming message payload
212 * @throws MessageParseException
214 private void parseKeypadMessage(String msg) throws MessageParseException {
219 kpMsg = new KeypadMessage(msg);
220 } catch (IllegalArgumentException e) {
221 throw new MessageParseException(e.getMessage());
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();
230 notifyChildHandlers(kpMsg);
234 * Parse and handle relay and expander messages. The REL and EXP messages have identical format.
236 * @param mt message type of incoming message
237 * @param msg string containing incoming message payload
238 * @throws MessageParseException
240 private void parseRelayOrExpanderMessage(ADMsgType mt, String msg) throws MessageParseException {
241 // mt is unused at the moment
245 expMsg = new EXPMessage(msg);
246 } catch (IllegalArgumentException e) {
247 throw new MessageParseException(e.getMessage());
250 notifyChildHandlers(expMsg);
252 AlarmDecoderDiscoveryService ds = discoveryService;
253 if (discovery && ds != null) {
254 ds.processZone(expMsg.address, expMsg.channel);
259 * Parse and handle RFX messages.
261 * @param msg string containing incoming message payload
262 * @throws MessageParseException
264 private void parseRFMessage(String msg) throws MessageParseException {
268 rfxMsg = new RFXMessage(msg);
269 } catch (IllegalArgumentException e) {
270 throw new MessageParseException(e.getMessage());
273 notifyChildHandlers(rfxMsg);
275 AlarmDecoderDiscoveryService ds = discoveryService;
276 if (discovery && ds != null) {
277 ds.processRFZone(rfxMsg.serial);
282 * Parse and handle LRR messages.
284 * @param msg string containing incoming message payload
285 * @throws MessageParseException
287 private void parseLRRMessage(String msg) throws MessageParseException {
292 lrrMsg = new LRRMessage(msg);
293 } catch (IllegalArgumentException e) {
294 throw new MessageParseException(e.getMessage());
297 notifyChildHandlers(lrrMsg);
301 * Parse and handle version (VER) message. This just updates bridge properties.
303 * @param msg string containing incoming message payload
304 * @throws MessageParseException
306 private void parseVersionMessage(String msg) throws MessageParseException {
307 VersionMessage verMsg;
310 verMsg = new VersionMessage(msg);
311 } catch (IllegalArgumentException e) {
312 throw new MessageParseException(e.getMessage());
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);
325 * Notify appropriate child thing handlers of an AD message by calling their handleUpdate() methods.
327 * @param msg message to forward to child handler(s)
329 private void notifyChildHandlers(ADMessage msg) {
330 for (Thing thing : getThing().getThings()) {
331 ADThingHandler handler = (ADThingHandler) thing.getHandler();
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);
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
350 private void notifyChildHandlersPanelReady() {
351 if (!panelReadyReceived) {
352 panelReadyReceived = true;
353 logger.trace("Notifying child handlers that panel is in ready state");
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();
366 * Exception thrown by message parsing code when it encounters a malformed message
368 private static class MessageParseException extends Exception {
369 private static final long serialVersionUID = 1L;
371 public MessageParseException(@Nullable String msg) {