]> git.basschouten.com Git - openhab-addons.git/blob
e46c9927263d19381045068200109cac709b70fc
[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.plugwise.internal;
14
15 import static org.openhab.binding.plugwise.internal.PlugwiseCommunicationContext.*;
16
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.nio.ByteBuffer;
20 import java.util.Iterator;
21 import java.util.TooManyListenersException;
22 import java.util.regex.Matcher;
23 import java.util.regex.Pattern;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.plugwise.internal.protocol.AcknowledgementMessage;
28 import org.openhab.binding.plugwise.internal.protocol.Message;
29 import org.openhab.binding.plugwise.internal.protocol.MessageFactory;
30 import org.openhab.binding.plugwise.internal.protocol.field.MessageType;
31 import org.openhab.core.io.transport.serial.SerialPort;
32 import org.openhab.core.io.transport.serial.SerialPortEvent;
33 import org.openhab.core.io.transport.serial.SerialPortEventListener;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
36
37 /**
38  * Processes messages received from the Plugwise Stick using a serial connection.
39  *
40  * @author Wouter Born, Karel Goderis - Initial contribution
41  */
42 @NonNullByDefault
43 public class PlugwiseMessageProcessor implements SerialPortEventListener {
44
45     private class MessageProcessorThread extends Thread {
46
47         public MessageProcessorThread() {
48             super("OH-binding-" + context.getBridgeUID() + "-message-processor");
49             setDaemon(true);
50         }
51
52         @Override
53         public void run() {
54             while (!interrupted()) {
55                 try {
56                     Message message = context.getReceivedQueue().take();
57                     if (message != null) {
58                         logger.debug("Took message from receivedQueue (length={})", context.getReceivedQueue().size());
59                         processMessage(message);
60                     } else {
61                         logger.debug("Skipping null message from receivedQueue (length={})",
62                                 context.getReceivedQueue().size());
63                     }
64                 } catch (InterruptedException e) {
65                     // That's our signal to stop
66                     break;
67                 } catch (Exception e) {
68                     logger.warn("Error while taking message from receivedQueue", e);
69                 }
70             }
71         }
72     }
73
74     /** Matches Plugwise responses into the following groups: protocolHeader command sequence payload CRC */
75     private static final Pattern RESPONSE_PATTERN = Pattern.compile("(.{4})(\\w{4})(\\w{4})(\\w*?)(\\w{4})");
76
77     private final Logger logger = LoggerFactory.getLogger(PlugwiseMessageProcessor.class);
78     private final PlugwiseCommunicationContext context;
79     private final MessageFactory messageFactory = new MessageFactory();
80
81     private final ByteBuffer readBuffer = ByteBuffer.allocate(PlugwiseCommunicationContext.MAX_BUFFER_SIZE);
82     private int previousByte = -1;
83
84     private @Nullable MessageProcessorThread thread;
85
86     public PlugwiseMessageProcessor(PlugwiseCommunicationContext context) {
87         this.context = context;
88     }
89
90     /**
91      * Parse a buffer into a Message and put it in the appropriate queue for further processing
92      *
93      * @param readBuffer - the string to parse
94      */
95     private void parseAndQueue(ByteBuffer readBuffer) {
96         String response = new String(readBuffer.array(), 0, readBuffer.limit());
97         response = response.replaceAll("\r", "").replaceAll("\n", "");
98
99         Matcher matcher = RESPONSE_PATTERN.matcher(response);
100
101         if (matcher.matches()) {
102             String protocolHeader = matcher.group(1);
103             String messageTypeHex = matcher.group(2);
104             String sequence = matcher.group(3);
105             String payload = matcher.group(4);
106             String crc = matcher.group(5);
107
108             if (protocolHeader.equals(PROTOCOL_HEADER)) {
109                 String calculatedCRC = Message.getCRC(messageTypeHex + sequence + payload);
110                 if (calculatedCRC.equals(crc)) {
111                     MessageType messageType = MessageType.forValue(Integer.parseInt(messageTypeHex, 16));
112                     int sequenceNumber = Integer.parseInt(sequence, 16);
113
114                     if (messageType == null) {
115                         logger.debug("Received unrecognized message: messageTypeHex=0x{}, sequence={}, payload={}",
116                                 messageTypeHex, sequenceNumber, payload);
117                         return;
118                     }
119
120                     logger.debug("Received message: messageType={}, sequenceNumber={}, payload={}", messageType,
121                             sequenceNumber, payload);
122
123                     try {
124                         Message message = messageFactory.createMessage(messageType, sequenceNumber, payload);
125
126                         if (message instanceof AcknowledgementMessage
127                                 && !((AcknowledgementMessage) message).isExtended()) {
128                             logger.debug("Adding to acknowledgedQueue: {}", message);
129                             context.getAcknowledgedQueue().put((AcknowledgementMessage) message);
130                         } else {
131                             logger.debug("Adding to receivedQueue: {}", message);
132                             context.getReceivedQueue().put(message);
133                         }
134                     } catch (IllegalArgumentException e) {
135                         logger.warn("Failed to create message", e);
136                     } catch (InterruptedException e) {
137                         Thread.interrupted();
138                     }
139                 } else {
140                     logger.warn("Plugwise protocol CRC error: {} does not match {} in message", calculatedCRC, crc);
141                 }
142             } else {
143                 logger.debug("Plugwise protocol header error: {} in message {}", protocolHeader, response);
144             }
145         } else if (!response.contains("APSRequestNodeInfo") && !response.contains("APSSetSleepBehaviour")
146                 && !response.startsWith("# ")) {
147             logger.warn("Plugwise protocol message error: {}", response);
148         }
149     }
150
151     private void processMessage(Message message) {
152         context.getFilteredListeners().notifyListeners(message);
153
154         // After processing the response to a message, we remove any reference to the original request
155         // stored in the sentQueue
156         // WARNING: We assume that each request sent out can only be followed bye EXACTLY ONE response - so
157         // far it seems that the Plugwise protocol is operating in that way
158
159         try {
160             context.getSentQueueLock().lock();
161
162             Iterator<@Nullable PlugwiseQueuedMessage> messageIterator = context.getSentQueue().iterator();
163             while (messageIterator.hasNext()) {
164                 PlugwiseQueuedMessage queuedSentMessage = messageIterator.next();
165                 if (queuedSentMessage != null
166                         && queuedSentMessage.getMessage().getSequenceNumber() == message.getSequenceNumber()) {
167                     logger.debug("Removing from sentQueue: {}", queuedSentMessage.getMessage());
168                     context.getSentQueue().remove(queuedSentMessage);
169                     break;
170                 }
171             }
172         } finally {
173             context.getSentQueueLock().unlock();
174         }
175     }
176
177     @SuppressWarnings("resource")
178     @Override
179     public void serialEvent(@Nullable SerialPortEvent event) {
180         if (event != null && event.getEventType() == SerialPortEvent.DATA_AVAILABLE) {
181             // We get here if data has been received
182             SerialPort serialPort = context.getSerialPort();
183             if (serialPort == null) {
184                 logger.debug("Failed to read available data from null serialPort");
185                 return;
186             }
187
188             try {
189                 InputStream inputStream = serialPort.getInputStream();
190                 if (inputStream == null) {
191                     logger.debug("Failed to read available data from null inputStream");
192                     return;
193                 }
194
195                 // Read data from serial device
196                 while (inputStream.available() > 0) {
197                     int currentByte = inputStream.read();
198                     // Plugwise sends ASCII data, but for some unknown reason we sometimes get data with unsigned
199                     // byte value >127 which in itself is very strange. We filter these out for the time being
200                     if (currentByte < 128) {
201                         readBuffer.put((byte) currentByte);
202                         if (previousByte == CR && currentByte == LF) {
203                             readBuffer.flip();
204                             parseAndQueue(readBuffer);
205                             readBuffer.clear();
206                             previousByte = -1;
207                         } else {
208                             previousByte = currentByte;
209                         }
210                     }
211                 }
212             } catch (IOException e) {
213                 logger.debug("Error receiving data on serial port {}: {}", context.getConfiguration().getSerialPort(),
214                         e.getMessage());
215             }
216         }
217     }
218
219     @SuppressWarnings("resource")
220     public void start() throws PlugwiseInitializationException {
221         SerialPort serialPort = context.getSerialPort();
222         if (serialPort == null) {
223             throw new PlugwiseInitializationException("Failed to add serial port listener because port is null");
224         }
225
226         try {
227             serialPort.addEventListener(this);
228         } catch (TooManyListenersException e) {
229             throw new PlugwiseInitializationException("Failed to add serial port listener", e);
230         }
231
232         thread = new MessageProcessorThread();
233         thread.start();
234     }
235
236     @SuppressWarnings("resource")
237     public void stop() {
238         PlugwiseUtils.stopBackgroundThread(thread);
239
240         SerialPort serialPort = context.getSerialPort();
241         if (serialPort != null) {
242             serialPort.removeEventListener();
243         }
244     }
245 }