]> git.basschouten.com Git - openhab-addons.git/blob
6477edad83878df2cb179d22fa7649cc7e0d6b80
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.comfoair.internal;
14
15 import java.io.BufferedInputStream;
16 import java.io.DataInputStream;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.InterruptedIOException;
20 import java.io.OutputStream;
21 import java.util.Arrays;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.core.io.transport.serial.PortInUseException;
26 import org.openhab.core.io.transport.serial.SerialPort;
27 import org.openhab.core.io.transport.serial.SerialPortIdentifier;
28 import org.openhab.core.io.transport.serial.SerialPortManager;
29 import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
30 import org.openhab.core.library.types.OnOffType;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33
34 /**
35  * Connector class for serial communication with ComfoAir device
36  *
37  * @author Hans Böhm - Initial contribution
38  *
39  */
40 @NonNullByDefault
41 public class ComfoAirSerialConnector {
42
43     private final Logger logger = LoggerFactory.getLogger(ComfoAirSerialConnector.class);
44
45     private static final byte CTRL = (byte) 0x07;
46     private static final byte[] START = { CTRL, (byte) 0xf0 };
47     private static final byte[] END = { CTRL, (byte) 0x0f };
48     private static final byte[] ACK = { CTRL, (byte) 0xf3 };
49
50     private static final int RS232_ENABLED_VALUE = 0x03;
51     private static final int RS232_DISABLED_VALUE = 0x00;
52
53     private boolean isSuspended = true;
54
55     private final String serialPortName;
56     private final int baudRate;
57     private final SerialPortManager serialPortManager;
58     private @Nullable SerialPort serialPort;
59     private @Nullable InputStream inputStream;
60     private @Nullable OutputStream outputStream;
61
62     public ComfoAirSerialConnector(final SerialPortManager serialPortManager, final String serialPortName,
63             final int baudRate) {
64         this.serialPortManager = serialPortManager;
65         this.serialPortName = serialPortName;
66         this.baudRate = baudRate;
67     }
68
69     /**
70      * Open serial port.
71      *
72      * @throws PortInUseException, UnsupportedCommOperationException, IOException
73      */
74     public void open() throws ComfoAirSerialException {
75         logger.debug("open(): Opening ComfoAir connection");
76
77         try {
78             SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(serialPortName);
79             if (portIdentifier != null) {
80                 SerialPort serialPort = portIdentifier.open(this.getClass().getName(), 3000);
81                 serialPort.setSerialPortParams(baudRate, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
82                         SerialPort.PARITY_NONE);
83                 serialPort.enableReceiveThreshold(1);
84                 serialPort.enableReceiveTimeout(1000);
85                 serialPort.notifyOnDataAvailable(true);
86                 this.serialPort = serialPort;
87
88                 inputStream = new DataInputStream(new BufferedInputStream(serialPort.getInputStream()));
89                 outputStream = serialPort.getOutputStream();
90
91                 ComfoAirCommand command = ComfoAirCommandType.getChangeCommand(ComfoAirCommandType.ACTIVATE.getKey(),
92                         OnOffType.ON);
93
94                 if (command != null) {
95                     sendCommand(command, ComfoAirCommandType.Constants.EMPTY_INT_ARRAY);
96                 } else {
97                     logger.debug("Failure while creating COMMAND: {}", command);
98                 }
99             } else {
100                 throw new ComfoAirSerialException("No such Port: " + serialPortName);
101             }
102         } catch (PortInUseException | UnsupportedCommOperationException | IOException e) {
103             throw new ComfoAirSerialException(e);
104         }
105     }
106
107     /**
108      * Close serial port.
109      */
110     public void close() {
111         logger.debug("close(): Close ComfoAir connection");
112         SerialPort serialPort = this.serialPort;
113
114         if (serialPort != null) {
115             ComfoAirCommand command = ComfoAirCommandType.getChangeCommand(ComfoAirCommandType.ACTIVATE.getKey(),
116                     OnOffType.OFF);
117
118             if (command != null) {
119                 sendCommand(command, ComfoAirCommandType.Constants.EMPTY_INT_ARRAY);
120             } else {
121                 logger.debug("Failure while creating COMMAND: {}", command);
122             }
123
124             if (inputStream != null) {
125                 try {
126                     inputStream.close();
127                 } catch (IOException e) {
128                     logger.debug("Error while closing input stream: {}", e.getMessage());
129                 }
130             }
131
132             if (outputStream != null) {
133                 try {
134                     outputStream.close();
135                 } catch (IOException e) {
136                     logger.debug("Error while closing output stream: {}", e.getMessage());
137                 }
138             }
139
140             serialPort.close();
141         }
142     }
143
144     /**
145      * Prepare a command for sending using the serial port.
146      *
147      * @param command
148      * @param preRequestData
149      * @return reply byte values
150      */
151     public synchronized int[] sendCommand(ComfoAirCommand command, int[] preRequestData) {
152         Integer requestCmd = command.getRequestCmd();
153         int retry = 0;
154
155         if (requestCmd != null) {
156             // Switch support for app or ccease control
157             if (requestCmd == ComfoAirCommandType.Constants.REQUEST_SET_RS232) {
158                 isSuspended = !isSuspended;
159             } else if (requestCmd == ComfoAirCommandType.Constants.REPLY_SET_RS232) {
160                 return new int[] { isSuspended ? RS232_DISABLED_VALUE : RS232_ENABLED_VALUE };
161             } else if (isSuspended) {
162                 logger.trace("Ignore cmd. Service is currently suspended");
163                 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
164             }
165
166             do {
167                 // If preRequestData param was send (preRequestData is sending for write command)
168                 int[] requestData;
169
170                 if (preRequestData.length <= 0) {
171                     requestData = command.getRequestData();
172                 } else {
173                     requestData = buildRequestData(command, preRequestData);
174
175                     if (requestData.length <= 0) {
176                         logger.debug("Unable to build data for write command: {}",
177                                 String.format("%02x", command.getReplyCmd()));
178                         return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
179                     }
180                 }
181
182                 byte[] requestBlock = calculateRequest(requestCmd, requestData);
183                 if (logger.isTraceEnabled()) {
184                     logger.trace("send DATA: {}", dumpData(requestBlock));
185                 }
186
187                 if (!send(requestBlock)) {
188                     return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
189                 }
190
191                 byte[] responseBlock = new byte[0];
192
193                 try {
194                     InputStream inputStream = this.inputStream;
195                     // 31 is max. response length
196                     byte[] readBuffer = new byte[31];
197                     do {
198                         while (inputStream != null && inputStream.available() > 0) {
199                             int bytes = inputStream.read(readBuffer);
200
201                             // merge bytes
202                             byte[] mergedBytes = new byte[responseBlock.length + bytes];
203                             System.arraycopy(responseBlock, 0, mergedBytes, 0, responseBlock.length);
204                             System.arraycopy(readBuffer, 0, mergedBytes, responseBlock.length, bytes);
205
206                             responseBlock = mergedBytes;
207                         }
208                         try {
209                             // add wait states around reading the stream, so that
210                             // interrupted transmissions are merged
211                             Thread.sleep(100);
212                         } catch (InterruptedException e) {
213                             Thread.currentThread().interrupt();
214                             logger.warn("Transmission was interrupted: {}", e.getMessage());
215                             throw new RuntimeException(e);
216                         }
217                     } while (inputStream != null && inputStream.available() > 0);
218
219                     // check for ACK
220                     if (responseBlock.length >= 2 && responseBlock[0] == ACK[0] && responseBlock[1] == ACK[1]) {
221                         if (command.getReplyCmd() == null) {
222                             // confirm additional data with an ACK
223                             if (responseBlock.length > 2) {
224                                 send(ACK);
225                             }
226                             return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
227                         }
228
229                         // check for start and end sequence and if the response cmd
230                         // matches
231                         // 11 is the minimum response length with one data byte
232                         if (responseBlock.length >= 11 && responseBlock[2] == START[0] && responseBlock[3] == START[1]
233                                 && responseBlock[responseBlock.length - 2] == END[0]
234                                 && responseBlock[responseBlock.length - 1] == END[1]
235                                 && (responseBlock[5] & 0xff) == command.getReplyCmd()) {
236                             if (logger.isTraceEnabled()) {
237                                 logger.trace("receive RAW DATA: {}", dumpData(responseBlock));
238                             }
239
240                             byte[] cleanedBlock = cleanupBlock(responseBlock);
241
242                             int dataSize = cleanedBlock[2];
243
244                             // the cleanedBlock size should equal dataSize + 2 cmd
245                             // bytes and + 1 checksum byte
246                             if (dataSize + 3 == cleanedBlock.length - 1) {
247                                 byte checksum = cleanedBlock[dataSize + 3];
248                                 int[] replyData = new int[dataSize];
249                                 for (int i = 0; i < dataSize; i++) {
250                                     replyData[i] = cleanedBlock[i + 3] & 0xff;
251                                 }
252
253                                 byte[] block = Arrays.copyOf(cleanedBlock, 3 + dataSize);
254
255                                 // validate calculated checksum against submitted
256                                 // checksum
257                                 if (calculateChecksum(block) == checksum) {
258                                     if (logger.isTraceEnabled()) {
259                                         logger.trace("receive CMD: {} DATA: {}",
260                                                 String.format("%02x", command.getReplyCmd()), dumpData(replyData));
261                                     }
262                                     send(ACK);
263
264                                     return replyData;
265                                 }
266
267                                 logger.debug("Unable to handle data. Checksum verification failed");
268                             } else {
269                                 logger.debug("Unable to handle data. Data size not valid");
270                             }
271
272                             if (logger.isTraceEnabled()) {
273                                 logger.trace("skip CMD: {} DATA: {}", String.format("%02x", command.getReplyCmd()),
274                                         dumpData(cleanedBlock));
275                             }
276                         }
277                     }
278                 } catch (InterruptedIOException e) {
279                     Thread.currentThread().interrupt();
280                     logger.warn("Transmission was interrupted: {}", e.getMessage());
281                     throw new RuntimeException(e);
282                 } catch (IOException e) {
283                     logger.debug("IO error: {}", e.getMessage());
284                 }
285
286                 try {
287                     Thread.sleep(1000);
288                     if (logger.isDebugEnabled()) {
289                         logger.debug("Retry cmd. Last call was not successful. Request: {} Response: {}",
290                                 dumpData(requestBlock), (responseBlock.length > 0 ? dumpData(responseBlock) : "null"));
291                     }
292                 } catch (InterruptedException e) {
293                     Thread.currentThread().interrupt();
294                     logger.warn("Transmission was interrupted: {}", e.getMessage());
295                     throw new RuntimeException(e);
296                 }
297             } while (retry++ < 5);
298
299             if (retry == 5) {
300                 logger.debug("Unable to send command. {} retries failed.", retry);
301             }
302         }
303         return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
304     }
305
306     /**
307      * Generate the byte sequence for sending to ComfoAir (incl. START & END
308      * sequence and checksum).
309      *
310      * @param command
311      * @param requestData
312      * @return response byte value block with cmd, data and checksum
313      */
314     private byte[] calculateRequest(int command, int[] requestData) {
315         // generate the command block (cmd and request data)
316         int length = requestData.length;
317         byte[] block = new byte[4 + length];
318
319         block[0] = 0x00;
320         block[1] = (byte) command;
321         block[2] = (byte) length;
322
323         if (requestData.length > 0) {
324             for (int i = 0; i < requestData.length; i++) {
325                 block[i + 3] = (byte) requestData[i];
326             }
327         }
328
329         // calculate checksum for command block
330         byte checksum = calculateChecksum(block);
331         block[block.length - 1] = checksum;
332
333         // escape the command block with checksum included
334         block = escapeBlock(block);
335         byte[] request = new byte[4 + block.length];
336
337         request[0] = START[0];
338         request[1] = START[1];
339         System.arraycopy(block, 0, request, 2, block.length);
340         request[request.length - 2] = END[0];
341         request[request.length - 1] = END[1];
342
343         return request;
344     }
345
346     /**
347      * Calculates a checksum for a command block (cmd, data and checksum).
348      *
349      * @param block
350      * @return checksum byte value
351      */
352     private byte calculateChecksum(byte[] block) {
353         int datasum = 0;
354         for (int i = 0; i < block.length; i++) {
355             datasum += block[i];
356         }
357         datasum += 173;
358
359         return (byte) (datasum & 0xFF);
360     }
361
362     /**
363      * Cleanup a commandblock from quoted 0x07 characters.
364      *
365      * @param processBuffer
366      * @return the 0x07 cleaned byte values
367      */
368     private byte[] cleanupBlock(byte[] processBuffer) {
369         int pos = 0;
370         byte[] cleanedBuffer = new byte[processBuffer.length];
371
372         for (int i = 4; i < processBuffer.length - 2; i++) {
373             if (CTRL == processBuffer[i] && CTRL == processBuffer[i + 1]) {
374                 i++;
375             }
376             cleanedBuffer[pos] = processBuffer[i];
377             pos++;
378         }
379         return Arrays.copyOf(cleanedBuffer, pos);
380     }
381
382     /**
383      * Escape special 0x07 character.
384      *
385      * @param cleanedBuffer
386      * @return escaped byte value array
387      */
388     private byte[] escapeBlock(byte[] cleanedBuffer) {
389         int pos = 0;
390         byte[] processBuffer = new byte[50];
391
392         for (int i = 0; i < cleanedBuffer.length; i++) {
393             if (CTRL == cleanedBuffer[i]) {
394                 processBuffer[pos] = CTRL;
395                 pos++;
396             }
397             processBuffer[pos] = cleanedBuffer[i];
398             pos++;
399         }
400         return Arrays.copyOf(processBuffer, pos);
401     }
402
403     /**
404      * Send the byte values.
405      *
406      * @param request
407      * @return successful flag
408      */
409     private boolean send(byte[] request) {
410         if (logger.isTraceEnabled()) {
411             logger.trace("send DATA: {}", dumpData(request));
412         }
413
414         try {
415             if (outputStream != null) {
416                 outputStream.write(request);
417             }
418             return true;
419         } catch (IOException e) {
420             logger.debug("Error writing to serial port {}: {}", serialPortName, e.getLocalizedMessage());
421             return false;
422         }
423     }
424
425     /**
426      * Is used to debug byte values.
427      *
428      * @param replyData
429      * @return
430      */
431     public static String dumpData(int[] replyData) {
432         StringBuilder sb = new StringBuilder();
433         for (int ch : replyData) {
434             sb.append(String.format(" %02x", ch));
435         }
436         return sb.toString();
437     }
438
439     private String dumpData(byte[] data) {
440         StringBuilder sb = new StringBuilder();
441         for (byte ch : data) {
442             sb.append(String.format(" %02x", ch));
443         }
444         return sb.toString();
445     }
446
447     /**
448      * Build request data based on reply data
449      *
450      * @param command
451      * @param preRequestData
452      * @return new build int values array
453      */
454     private int[] buildRequestData(ComfoAirCommand command, int[] preRequestData) {
455         int[] newRequestData;
456         Integer requestCmd = command.getRequestCmd();
457         Integer dataPosition = command.getDataPosition();
458         Integer requestValue = command.getRequestValue();
459
460         if (requestCmd != null && dataPosition != null && requestValue != null) {
461             switch (requestCmd) {
462                 case ComfoAirCommandType.Constants.REQUEST_SET_DELAYS:
463                     newRequestData = new int[8];
464
465                     if (preRequestData.length > 0 && newRequestData.length <= preRequestData.length) {
466                         System.arraycopy(preRequestData, 0, newRequestData, 0, newRequestData.length);
467                         newRequestData[dataPosition] = requestValue;
468                     } else {
469                         return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
470                     }
471                     break;
472                 case ComfoAirCommandType.Constants.REQUEST_SET_FAN_LEVEL:
473                     newRequestData = new int[9];
474
475                     if (preRequestData.length > 0 && newRequestData.length <= preRequestData.length) {
476                         System.arraycopy(preRequestData, 0, newRequestData, 0, 6);
477                         System.arraycopy(preRequestData, 10, newRequestData, 6, newRequestData.length - 6);
478                         newRequestData[dataPosition] = requestValue;
479                     } else {
480                         return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
481                     }
482                     break;
483                 case ComfoAirCommandType.Constants.REQUEST_SET_STATES:
484                     newRequestData = new int[8];
485
486                     if (preRequestData.length > 0 && newRequestData.length <= preRequestData.length) {
487                         if (dataPosition == 4) {
488                             requestValue = preRequestData[dataPosition]
489                                     + checkByteAndCalculateValue(command, requestValue, preRequestData[dataPosition]);
490                         }
491                         System.arraycopy(preRequestData, 0, newRequestData, 0, 6);
492                         System.arraycopy(preRequestData, 9, newRequestData, 6, newRequestData.length - 6);
493                         newRequestData[dataPosition] = requestValue;
494                     } else {
495                         return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
496                     }
497                     break;
498                 case ComfoAirCommandType.Constants.REQUEST_SET_GHX:
499                     newRequestData = new int[5];
500
501                     if (preRequestData.length > 0 && newRequestData.length <= preRequestData.length) {
502                         System.arraycopy(preRequestData, 0, newRequestData, 0, 4);
503                         System.arraycopy(preRequestData, 6, newRequestData, 4, newRequestData.length - 4);
504                         newRequestData[dataPosition] = requestValue;
505                     } else {
506                         return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
507                     }
508                     break;
509                 case ComfoAirCommandType.Constants.REQUEST_SET_ANALOGS:
510                     newRequestData = new int[19];
511
512                     if (preRequestData.length > 0 && newRequestData.length <= preRequestData.length) {
513                         switch (dataPosition) {
514                             case 0:
515                             case 1:
516                             case 2:
517                                 requestValue = preRequestData[dataPosition] + checkByteAndCalculateValue(command,
518                                         requestValue, preRequestData[dataPosition]);
519                         }
520                         System.arraycopy(preRequestData, 0, newRequestData, 0, newRequestData.length);
521                         newRequestData[dataPosition] = requestValue;
522                     } else {
523                         return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
524                     }
525                     break;
526                 default:
527                     return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
528             }
529             return newRequestData;
530         } else {
531             return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
532         }
533     }
534
535     /**
536      * Check if preValue contains possible byte and calculate new value
537      *
538      * @param command
539      * @param requestValue
540      * @param preValue
541      * @return new int value
542      */
543     private int checkByteAndCalculateValue(ComfoAirCommand command, int requestValue, int preValue) {
544         String key = command.getKeys().get(0);
545         ComfoAirCommandType commandType = ComfoAirCommandType.getCommandTypeByKey(key);
546         if (commandType != null) {
547             int[] possibleValues = commandType.getPossibleValues();
548             if (possibleValues != null) {
549                 int possibleValue = possibleValues[0];
550                 boolean isActive = (preValue & possibleValue) == possibleValue;
551                 int newValue;
552
553                 if (isActive) {
554                     newValue = requestValue == 1 ? 0 : -possibleValue;
555                 } else {
556                     newValue = requestValue == 1 ? possibleValue : 0;
557                 }
558                 return newValue;
559             }
560         }
561         return 0;
562     }
563 }