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