2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.comfoair.internal;
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;
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;
35 * Connector class for serial communication with ComfoAir device
37 * @author Hans Böhm - Initial contribution
41 public class ComfoAirSerialConnector {
43 private final Logger logger = LoggerFactory.getLogger(ComfoAirSerialConnector.class);
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 };
50 private static final int MAX_RETRIES = 5;
52 private boolean isSuspended = true;
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;
61 public ComfoAirSerialConnector(final SerialPortManager serialPortManager, final String serialPortName,
63 this.serialPortManager = serialPortManager;
64 this.serialPortName = serialPortName;
65 this.baudRate = baudRate;
71 * @throws PortInUseException, UnsupportedCommOperationException, IOException
73 public void open() throws ComfoAirSerialException {
74 logger.debug("open(): Opening ComfoAir connection");
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);
85 serialPort.enableReceiveThreshold(1);
86 } catch (UnsupportedCommOperationException e) {
87 logger.debug("Enable receive threshold is unsupported");
90 serialPort.enableReceiveTimeout(1000);
91 } catch (UnsupportedCommOperationException e) {
92 logger.debug("Enable receive timeout is unsupported");
95 this.serialPort = serialPort;
97 inputStream = new DataInputStream(new BufferedInputStream(serialPort.getInputStream()));
98 outputStream = serialPort.getOutputStream();
100 ComfoAirCommand command = ComfoAirCommandType.getChangeCommand(ComfoAirCommandType.ACTIVATE.getKey(),
103 if (command != null) {
104 sendCommand(command, ComfoAirCommandType.Constants.EMPTY_INT_ARRAY);
106 logger.debug("Failure while creating COMMAND: {}", command);
109 throw new ComfoAirSerialException("No such Port: " + serialPortName);
111 } catch (PortInUseException | UnsupportedCommOperationException | IOException e) {
112 throw new ComfoAirSerialException(e);
119 public void close() {
120 logger.debug("close(): Close ComfoAir connection");
121 SerialPort serialPort = this.serialPort;
123 if (serialPort != null) {
124 ComfoAirCommand command = ComfoAirCommandType.getChangeCommand(ComfoAirCommandType.ACTIVATE.getKey(),
127 if (command != null) {
128 sendCommand(command, ComfoAirCommandType.Constants.EMPTY_INT_ARRAY);
130 logger.debug("Failure while creating COMMAND: {}", command);
133 if (inputStream != null) {
136 } catch (IOException e) {
137 logger.debug("Error while closing input stream: {}", e.getMessage());
141 if (outputStream != null) {
143 outputStream.close();
144 } catch (IOException e) {
145 logger.debug("Error while closing output stream: {}", e.getMessage());
154 * Prepare a command for sending using the serial port.
157 * @param preRequestData
158 * @return reply byte values
160 public synchronized int[] sendCommand(ComfoAirCommand command, int[] preRequestData) {
161 Integer requestCmd = command.getRequestCmd();
162 Integer requestValue = command.getRequestValue();
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) {
170 } else if (requestValue == 0) {
173 } else if (isSuspended) {
174 logger.trace("Ignore cmd. Service is currently suspended");
175 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
179 // If preRequestData param was send (preRequestData is sending for write command)
182 if (preRequestData.length <= 0) {
183 requestData = command.getRequestData();
185 requestData = buildRequestData(command, preRequestData);
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;
194 byte[] requestBlock = calculateRequest(requestCmd, requestData);
195 if (logger.isTraceEnabled()) {
196 logger.trace("send DATA: {}", dumpData(requestBlock));
199 if (!send(requestBlock)) {
200 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
203 byte[] responseBlock = new byte[0];
206 InputStream inputStream = this.inputStream;
207 // 31 is max. response length
208 byte[] readBuffer = new byte[31];
210 while (inputStream != null && inputStream.available() > 0) {
211 int bytes = inputStream.read(readBuffer);
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);
218 responseBlock = mergedBytes;
221 // add wait states around reading the stream, so that
222 // interrupted transmissions are merged
224 } catch (InterruptedException e) {
225 Thread.currentThread().interrupt();
226 logger.warn("Transmission was interrupted: {}", e.getMessage());
227 throw new RuntimeException(e);
229 } while (inputStream != null && inputStream.available() > 0);
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) {
238 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
241 boolean isValidData = false;
243 // check for start and end sequence and if the response cmd
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()) {
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())) {
259 for (int j = startIndex; j < responseBlock.length; j++) {
260 if (responseBlock[j] == END[0] && responseBlock[j + 1] == END[1]) {
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;
279 if (logger.isTraceEnabled()) {
280 logger.trace("receive RAW DATA: {}", dumpData(responseBlock));
283 byte[] cleanedBlock = cleanupBlock(responseBlock);
285 int dataSize = cleanedBlock[2];
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;
296 byte[] block = Arrays.copyOf(cleanedBlock, 3 + dataSize);
298 // validate calculated checksum against submitted
300 if (calculateChecksum(block) == checksum) {
301 if (logger.isTraceEnabled()) {
302 logger.trace("receive CMD: {} DATA: {}",
303 String.format("%02x", command.getReplyCmd()), dumpData(replyData));
310 logger.debug("Unable to handle data. Checksum verification failed");
312 logger.debug("Unable to handle data. Data size not valid");
315 if (logger.isTraceEnabled()) {
316 logger.trace("skip CMD: {} DATA: {}", String.format("%02x", command.getReplyCmd()),
317 dumpData(cleanedBlock));
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());
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"));
335 } catch (InterruptedException e) {
336 Thread.currentThread().interrupt();
337 logger.warn("Transmission was interrupted: {}", e.getMessage());
338 throw new RuntimeException(e);
340 } while (retry++ < MAX_RETRIES);
342 if (retry >= MAX_RETRIES) {
343 logger.debug("Unable to send command. {} retries failed.", retry);
346 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
350 * Generate the byte sequence for sending to ComfoAir (incl. START & END
351 * sequence and checksum).
355 * @return response byte value block with cmd, data and checksum
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];
363 block[1] = (byte) command;
364 block[2] = (byte) length;
366 if (requestData.length > 0) {
367 for (int i = 0; i < requestData.length; i++) {
368 block[i + 3] = (byte) requestData[i];
372 // calculate checksum for command block
373 byte checksum = calculateChecksum(block);
374 block[block.length - 1] = checksum;
376 // escape the command block with checksum included
377 block = escapeBlock(block);
378 byte[] request = new byte[4 + block.length];
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];
390 * Calculates a checksum for a command block (cmd, data and checksum).
393 * @return checksum byte value
395 private byte calculateChecksum(byte[] block) {
397 for (int i = 0; i < block.length; i++) {
402 return (byte) (datasum & 0xFF);
406 * Cleanup a commandblock from quoted 0x07 characters.
408 * @param processBuffer
409 * @return the 0x07 cleaned byte values
411 private byte[] cleanupBlock(byte[] processBuffer) {
413 byte[] cleanedBuffer = new byte[processBuffer.length];
415 for (int i = 4; i < processBuffer.length - 2; i++) {
416 if (CTRL == processBuffer[i] && CTRL == processBuffer[i + 1]) {
419 cleanedBuffer[pos] = processBuffer[i];
421 // Trim unrequested data in response
422 if (END[0] == processBuffer[i + 1] && END[1] == processBuffer[i + 2]) {
426 return Arrays.copyOf(cleanedBuffer, pos);
430 * Escape special 0x07 character.
432 * @param cleanedBuffer
433 * @return escaped byte value array
435 private byte[] escapeBlock(byte[] cleanedBuffer) {
437 byte[] processBuffer = new byte[50];
439 for (int i = 0; i < cleanedBuffer.length; i++) {
440 if (CTRL == cleanedBuffer[i]) {
441 processBuffer[pos] = CTRL;
444 processBuffer[pos] = cleanedBuffer[i];
447 return Arrays.copyOf(processBuffer, pos);
451 * Send the byte values.
454 * @return successful flag
456 private boolean send(byte[] request) {
457 if (logger.isTraceEnabled()) {
458 logger.trace("send DATA: {}", dumpData(request));
462 if (outputStream != null) {
463 outputStream.write(request);
466 } catch (IOException e) {
467 logger.debug("Error writing to serial port {}: {}", serialPortName, e.getLocalizedMessage());
473 * Is used to debug byte values.
478 public static String dumpData(int[] replyData) {
479 StringBuilder sb = new StringBuilder();
480 for (int ch : replyData) {
481 sb.append(String.format(" %02x", ch));
483 return sb.toString();
486 private String dumpData(byte[] data) {
487 StringBuilder sb = new StringBuilder();
488 for (byte ch : data) {
489 sb.append(String.format(" %02x", ch));
491 return sb.toString();
495 * Build request data based on reply data
498 * @param preRequestData
499 * @return new build int values array
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();
507 if (requestCmd != null && dataPosition != null && requestValue != null) {
508 switch (requestCmd) {
509 case ComfoAirCommandType.Constants.REQUEST_SET_DELAYS:
510 newRequestData = new int[8];
512 if (preRequestData.length > 0 && newRequestData.length <= preRequestData.length) {
513 System.arraycopy(preRequestData, 0, newRequestData, 0, newRequestData.length);
514 newRequestData[dataPosition] = requestValue;
516 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
519 case ComfoAirCommandType.Constants.REQUEST_SET_FAN_LEVEL:
520 newRequestData = new int[9];
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);
527 newRequestData[dataPosition] = requestValue;
529 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
532 case ComfoAirCommandType.Constants.REQUEST_SET_STATES:
533 newRequestData = new int[8];
535 if (preRequestData.length > 0 && newRequestData.length <= preRequestData.length) {
536 if (dataPosition == 4) {
537 requestValue = preRequestData[dataPosition]
538 + checkByteAndCalculateValue(command, requestValue, preRequestData[dataPosition]);
540 System.arraycopy(preRequestData, 0, newRequestData, 0, 6);
541 System.arraycopy(preRequestData, 9, newRequestData, 6, newRequestData.length - 6);
542 newRequestData[dataPosition] = requestValue;
544 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
547 case ComfoAirCommandType.Constants.REQUEST_SET_GHX:
548 newRequestData = new int[5];
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;
555 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
558 case ComfoAirCommandType.Constants.REQUEST_SET_ANALOGS:
559 newRequestData = new int[19];
561 if (preRequestData.length > 0 && newRequestData.length <= preRequestData.length) {
562 switch (dataPosition) {
566 requestValue = preRequestData[dataPosition] + checkByteAndCalculateValue(command,
567 requestValue, preRequestData[dataPosition]);
569 System.arraycopy(preRequestData, 0, newRequestData, 0, newRequestData.length);
570 newRequestData[dataPosition] = requestValue;
572 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
576 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
578 return newRequestData;
580 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
585 * Check if preValue contains possible byte and calculate new value
588 * @param requestValue
590 * @return new int value
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;
603 newValue = requestValue == 1 ? 0 : -possibleValue;
605 newValue = requestValue == 1 ? possibleValue : 0;
613 public boolean getIsSuspended() {