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