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