]> git.basschouten.com Git - openhab-addons.git/blob
260693473ea4d44b8eabbf46e731efee4966ce58
[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.heliosventilation.internal;
14
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.io.OutputStream;
18 import java.util.HashMap;
19 import java.util.Map;
20 import java.util.TooManyListenersException;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.core.io.transport.serial.PortInUseException;
27 import org.openhab.core.io.transport.serial.SerialPort;
28 import org.openhab.core.io.transport.serial.SerialPortEvent;
29 import org.openhab.core.io.transport.serial.SerialPortEventListener;
30 import org.openhab.core.io.transport.serial.SerialPortIdentifier;
31 import org.openhab.core.io.transport.serial.SerialPortManager;
32 import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
33 import org.openhab.core.library.types.DecimalType;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.library.types.QuantityType;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.ThingStatusDetail;
40 import org.openhab.core.thing.binding.BaseThingHandler;
41 import org.openhab.core.types.Command;
42 import org.openhab.core.types.RefreshType;
43 import org.openhab.core.types.State;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47 /**
48  * The {@link HeliosVentilationHandler} is responsible for handling commands, which are
49  * sent to one of the channels.
50  *
51  * @author Raphael Mack - Initial contribution
52  */
53 @NonNullByDefault
54 public class HeliosVentilationHandler extends BaseThingHandler implements SerialPortEventListener {
55     private static final int BUSMEMBER_MAINBOARD = 0x11;
56     private static final int BUSMEMBER_SLAVEBOARDS = 0x10;
57     private static final byte BUSMEMBER_CONTROLBOARDS = (byte) 0x20;
58     private static final int BUSMEMBER_REC_MASK = 0xF0; // interpreting frames delivered to BUSMEMBER_ME &
59                                                         // BUSMEMBER_REC_MASK
60     private static final int BUSMEMBER_ME = 0x2F; // used as sender when communicating with the helios system
61     private static final int POLL_OFFLINE_THRESHOLD = 3;
62
63     /** Logger Instance */
64     private final Logger logger = LoggerFactory.getLogger(HeliosVentilationHandler.class);
65
66     /**
67      * store received data for read-modify-write operations on bitlevel
68      */
69     private final Map<Byte, Byte> memory = new HashMap<Byte, Byte>();
70
71     private final SerialPortManager serialPortManager;
72
73     /**
74      * init to default to avoid NPE in case handleCommand() is called before initialize()
75      */
76     private HeliosVentilationConfiguration config = new HeliosVentilationConfiguration();
77
78     private @Nullable SerialPort serialPort;
79     private @Nullable InputStream inputStream;
80     private @Nullable OutputStream outputStream;
81
82     private @Nullable ScheduledFuture<?> pollingTask;
83     private int pollCounter;
84
85     public HeliosVentilationHandler(Thing thing, final SerialPortManager serialPortManager) {
86         super(thing);
87         this.serialPortManager = serialPortManager;
88     }
89
90     @Override
91     public void initialize() {
92         config = getConfigAs(HeliosVentilationConfiguration.class);
93
94         logger.debug("Serial Port: {}, 9600 baud, PollPeriod: {}", config.serialPort, config.pollPeriod);
95
96         if (config.serialPort.length() < 1) {
97             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port must be set!");
98             return;
99         } else {
100             SerialPortIdentifier portId = serialPortManager.getIdentifier(config.serialPort);
101             if (portId == null) {
102                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
103                         "Port " + config.serialPort + " is not known!");
104                 serialPort = null;
105             } else {
106                 updateStatus(ThingStatus.UNKNOWN);
107                 if (this.config.pollPeriod > 0) {
108                     startPolling();
109                 }
110             }
111         }
112
113         scheduler.execute(this::connect);
114     }
115
116     private synchronized void connect() {
117         logger.debug("HeliosVentilation: connecting...");
118         // parse ports and if the port is found, initialize the reader
119         SerialPortIdentifier portId = serialPortManager.getIdentifier(config.serialPort);
120         if (portId == null) {
121             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
122                     "Port " + config.serialPort + " is not known!");
123             serialPort = null;
124
125             disconnect();
126         } else if (!isConnected()) {
127             // initialize serial port
128             try {
129                 SerialPort serial = portId.open(getThing().getUID().toString(), 2000);
130                 serial.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE);
131                 serial.addEventListener(this);
132
133                 try {
134                     if (inputStream != null) {
135                         inputStream.close();
136                     }
137                 } catch (IOException e) {
138                     // ignore the exception on close
139                     inputStream = null;
140                 }
141                 try {
142                     if (outputStream != null) {
143                         outputStream.close();
144                     }
145                 } catch (IOException e) {
146                     // ignore the exception on close
147                     outputStream = null;
148                 }
149
150                 inputStream = serial.getInputStream();
151                 outputStream = serial.getOutputStream();
152
153                 // activate the DATA_AVAILABLE notifier
154                 serial.notifyOnDataAvailable(true);
155                 serialPort = serial;
156                 updateStatus(ThingStatus.UNKNOWN);
157             } catch (final IOException ex) {
158                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "I/O error!");
159             } catch (PortInUseException e) {
160                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Port is in use!");
161             } catch (TooManyListenersException e) {
162                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
163                         "Cannot attach listener to port!");
164             } catch (UnsupportedCommOperationException e) {
165                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
166                         "Serial port does not support the RS485 parameters of the Helios remote protocol.");
167             }
168         }
169     }
170
171     @Override
172     public void dispose() {
173         stopPolling();
174         disconnect();
175         super.dispose();
176     }
177
178     /**
179      * Start the polling task.
180      */
181     public synchronized void startPolling() {
182         final ScheduledFuture<?> task = pollingTask;
183         if (task != null && task.isCancelled()) {
184             task.cancel(true);
185         }
186         if (config.pollPeriod > 0) {
187             pollingTask = scheduler.scheduleWithFixedDelay(this::polling, 10, config.pollPeriod, TimeUnit.SECONDS);
188         } else {
189             pollingTask = null;
190         }
191     }
192
193     /**
194      * Stop the polling task.
195      */
196     public synchronized void stopPolling() {
197         final ScheduledFuture<?> task = pollingTask;
198         if (task != null && !task.isCancelled()) {
199             task.cancel(true);
200             pollingTask = null;
201         }
202     }
203
204     /**
205      * Method for polling the RS485 Helios RemoteContol bus
206      */
207     public synchronized void polling() {
208         if (logger.isTraceEnabled()) {
209             logger.trace("HeliosVentilation Polling data for '{}'", getThing().getUID());
210         }
211         pollCounter++;
212         if (pollCounter > POLL_OFFLINE_THRESHOLD) {
213             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.GONE, "No data received!");
214             logger.info("No data received for '{}' disconnecting now...", getThing().getUID());
215             disconnect();
216         }
217
218         if (!isConnected()) {
219             connect(); // let's try to reconnect if the connection failed or was never established before
220         }
221
222         HeliosVentilationBindingConstants.DATAPOINTS.values().forEach((v) -> {
223             if (isLinked(v.getName())) {
224                 poll(v);
225             }
226         });
227     }
228
229     private void disconnect() {
230         if (thing.getStatus() != ThingStatus.REMOVING) {
231             updateStatus(ThingStatus.OFFLINE);
232         }
233         synchronized (this) {
234             try {
235                 if (inputStream != null) {
236                     inputStream.close();
237                 }
238             } catch (IOException e) {
239                 // ignore the exception on close
240                 inputStream = null;
241             }
242             try {
243                 if (outputStream != null) {
244                     outputStream.close();
245                 }
246             } catch (IOException e) {
247                 // ignore the exception on close
248                 outputStream = null;
249             }
250
251             SerialPort serial = serialPort;
252             if (serial != null) {
253                 serial.close();
254             }
255             serialPort = null;
256         }
257     }
258
259     private void poll(HeliosVentilationDataPoint v) {
260         byte[] txFrame = { 0x01, BUSMEMBER_ME, BUSMEMBER_MAINBOARD, 0x00, v.address(), 0x00 };
261         txFrame[5] = (byte) checksum(txFrame);
262
263         tx(txFrame);
264     }
265
266     /*
267      * transmit a frame
268      */
269     private void tx(byte[] txFrame) {
270         try {
271             OutputStream out = outputStream;
272             if (out != null) {
273                 if (logger.isTraceEnabled()) {
274                     logger.trace("HeliosVentilation: Write to serial port: {}",
275                             String.format("%02x %02x %02x %02x", txFrame[1], txFrame[2], txFrame[3], txFrame[4]));
276                 }
277
278                 out.write(txFrame);
279                 out.flush();
280                 // after each frame we have to wait.
281                 // 30 ms is taken from what we roughly see the original remote control is doing
282                 Thread.sleep(30);
283             }
284         } catch (IOException e) {
285             // in case we cannot write the connection is somehow broken, let's officially disconnect
286             disconnect();
287             connect();
288         } catch (InterruptedException e) {
289             // ignore if we got interrupted
290         }
291     }
292
293     /**
294      * Check connection status
295      *
296      * @return true if currently connected
297      */
298     private boolean isConnected() {
299         return serialPort != null && inputStream != null && outputStream != null;
300     }
301
302     @Override
303     public synchronized void serialEvent(SerialPortEvent event) {
304         switch (event.getEventType()) {
305             case SerialPortEvent.DATA_AVAILABLE:
306                 // we get here if data has been received
307
308                 try {
309                     // Wait roughly a frame length to ensure that the complete frame is already buffered. This improves
310                     // the robustness for RS485/USB converters which sometimes duplicate bytes otherwise.
311                     Thread.sleep(8);
312                 } catch (InterruptedException e) {
313                     // ignore interruption
314                 }
315
316                 byte[] frame = { 0, 0, 0, 0, 0, 0 };
317                 InputStream in = inputStream;
318                 if (in != null) {
319                     try {
320                         do {
321                             int cnt = 0;
322                             // read data from serial device
323                             while (cnt < 6 && in.available() > 0) {
324                                 final int bytes = in.read(frame, cnt, 1);
325                                 if (cnt > 0 || frame[0] == 0x01) {
326                                     // only proceed if the first byte was 0x01
327                                     cnt += bytes;
328                                 }
329                             }
330                             int sum = checksum(frame);
331                             if (sum == (frame[5] & 0xff)) {
332                                 if (logger.isTraceEnabled()) {
333                                     logger.trace("HeliosVentilation: Read from serial port: {}", String
334                                             .format("%02x %02x %02x %02x", frame[1], frame[2], frame[3], frame[4]));
335                                 }
336                                 interpretFrame(frame);
337
338                             } else {
339                                 if (logger.isTraceEnabled()) {
340                                     logger.trace(
341                                             "HeliosVentilation: Read frame with not matching checksum from serial port: {}",
342                                             String.format("%02x %02x %02x %02x %02x %02x (expected %02x)", frame[0],
343                                                     frame[1], frame[2], frame[3], frame[4], frame[5], sum));
344                                 }
345
346                             }
347
348                         } while (in.available() > 0);
349
350                     } catch (IOException e1) {
351                         logger.debug("Error reading from serial port: {}", e1.getMessage(), e1);
352                     }
353                 }
354                 break;
355             default:
356                 break;
357         }
358     }
359
360     @Override
361     public void handleCommand(ChannelUID channelUID, Command command) {
362         if (command instanceof RefreshType) {
363             scheduler.execute(this::polling);
364         } else if (command instanceof DecimalType || command instanceof QuantityType || command instanceof OnOffType) {
365             scheduler.execute(() -> update(channelUID, command));
366         }
367     }
368
369     /**
370      * Update the variable corresponding to given channel/command
371      *
372      * @param channelUID UID of the channel to update
373      * @param command data element to write
374      *
375      */
376     public void update(ChannelUID channelUID, Command command) {
377         HeliosVentilationBindingConstants.DATAPOINTS.values().forEach((outer) -> {
378             HeliosVentilationDataPoint v = outer;
379             do {
380                 if (channelUID.getThingUID().equals(thing.getUID()) && v.getName().equals(channelUID.getId())) {
381                     if (v.isWritable()) {
382                         byte[] txFrame = { 0x01, BUSMEMBER_ME, BUSMEMBER_CONTROLBOARDS, v.address(), 0x00, 0x00 };
383                         txFrame[4] = v.getTransmitDataFor((State) command);
384                         if (v.requiresReadModifyWrite()) {
385                             txFrame[4] |= memory.get(v.address()) & ~v.bitMask();
386                             memory.put(v.address(), txFrame[4]);
387                         }
388                         txFrame[5] = (byte) checksum(txFrame);
389                         tx(txFrame);
390
391                         txFrame[2] = BUSMEMBER_SLAVEBOARDS;
392                         txFrame[5] = (byte) checksum(txFrame);
393                         tx(txFrame);
394
395                         txFrame[2] = BUSMEMBER_MAINBOARD;
396                         txFrame[5] = (byte) checksum(txFrame);
397                         tx(txFrame);
398                     }
399                 }
400                 v = v.next();
401             } while (v != null);
402         });
403     }
404
405     /**
406      * calculate checksum of a frame
407      *
408      * @param frame filled with 5 bytes
409      * @return checksum of the first 5 bytes of frame
410      */
411     private int checksum(byte[] frame) {
412         int sum = 0;
413         for (int a = 0; a < 5; a++) {
414             sum += frame[a] & 0xff;
415         }
416         sum %= 256;
417         return sum;
418     }
419
420     /**
421      * interpret a frame, which is already validated to be in correct format with valid checksum
422      *
423      * @param frame 6 bytes long data with 0x01, sender, receiver, address, value, checksum
424      */
425     private void interpretFrame(byte[] frame) {
426         if ((frame[2] & BUSMEMBER_REC_MASK) == (BUSMEMBER_ME & BUSMEMBER_REC_MASK)) {
427             // something to read for us
428             byte var = frame[3];
429             byte val = frame[4];
430             if (HeliosVentilationBindingConstants.DATAPOINTS.containsKey(var)) {
431                 HeliosVentilationDataPoint datapoint = HeliosVentilationBindingConstants.DATAPOINTS.get(var);
432                 if (datapoint.requiresReadModifyWrite()) {
433                     memory.put(var, val);
434                 }
435                 do {
436                     if (logger.isTraceEnabled()) {
437                         String t = datapoint.asString(val);
438                         logger.trace("Received {} = {}", datapoint, t);
439                     }
440                     updateStatus(ThingStatus.ONLINE);
441                     pollCounter = 0;
442
443                     updateState(datapoint.getName(), datapoint.asState(val));
444                     datapoint = datapoint.next();
445                 } while (datapoint != null);
446
447             } else {
448                 if (logger.isTraceEnabled()) {
449                     logger.trace("Received unkown data @{} = {}", String.format("%02X ", var),
450                             String.format("%02X ", val));
451                 }
452             }
453         }
454     }
455 }