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.enableReceiveThreshold(1);
83 serialPort.enableReceiveTimeout(1000);
84 serialPort.notifyOnDataAvailable(true);
85 this.serialPort = serialPort;
87 inputStream = new DataInputStream(new BufferedInputStream(serialPort.getInputStream()));
88 outputStream = serialPort.getOutputStream();
90 ComfoAirCommand command = ComfoAirCommandType.getChangeCommand(ComfoAirCommandType.ACTIVATE.getKey(),
93 if (command != null) {
94 sendCommand(command, ComfoAirCommandType.Constants.EMPTY_INT_ARRAY);
96 logger.debug("Failure while creating COMMAND: {}", command);
99 throw new ComfoAirSerialException("No such Port: " + serialPortName);
101 } catch (PortInUseException | UnsupportedCommOperationException | IOException e) {
102 throw new ComfoAirSerialException(e);
109 public void close() {
110 logger.debug("close(): Close ComfoAir connection");
111 SerialPort serialPort = this.serialPort;
113 if (serialPort != null) {
114 ComfoAirCommand command = ComfoAirCommandType.getChangeCommand(ComfoAirCommandType.ACTIVATE.getKey(),
117 if (command != null) {
118 sendCommand(command, ComfoAirCommandType.Constants.EMPTY_INT_ARRAY);
120 logger.debug("Failure while creating COMMAND: {}", command);
123 if (inputStream != null) {
126 } catch (IOException e) {
127 logger.debug("Error while closing input stream: {}", e.getMessage());
131 if (outputStream != null) {
133 outputStream.close();
134 } catch (IOException e) {
135 logger.debug("Error while closing output stream: {}", e.getMessage());
144 * Prepare a command for sending using the serial port.
147 * @param preRequestData
148 * @return reply byte values
150 public synchronized int[] sendCommand(ComfoAirCommand command, int[] preRequestData) {
151 Integer requestCmd = command.getRequestCmd();
152 Integer requestValue = command.getRequestValue();
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) {
160 } else if (requestValue == 0) {
163 } else if (isSuspended) {
164 logger.trace("Ignore cmd. Service is currently suspended");
165 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
169 // If preRequestData param was send (preRequestData is sending for write command)
172 if (preRequestData.length <= 0) {
173 requestData = command.getRequestData();
175 requestData = buildRequestData(command, preRequestData);
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;
184 byte[] requestBlock = calculateRequest(requestCmd, requestData);
185 if (logger.isTraceEnabled()) {
186 logger.trace("send DATA: {}", dumpData(requestBlock));
189 if (!send(requestBlock)) {
190 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
193 byte[] responseBlock = new byte[0];
196 InputStream inputStream = this.inputStream;
197 // 31 is max. response length
198 byte[] readBuffer = new byte[31];
200 while (inputStream != null && inputStream.available() > 0) {
201 int bytes = inputStream.read(readBuffer);
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);
208 responseBlock = mergedBytes;
211 // add wait states around reading the stream, so that
212 // interrupted transmissions are merged
214 } catch (InterruptedException e) {
215 Thread.currentThread().interrupt();
216 logger.warn("Transmission was interrupted: {}", e.getMessage());
217 throw new RuntimeException(e);
219 } while (inputStream != null && inputStream.available() > 0);
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) {
228 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
231 boolean isValidData = false;
233 // check for start and end sequence and if the response cmd
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()) {
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())) {
249 for (int j = startIndex; j < responseBlock.length; j++) {
250 if (responseBlock[j] == END[0] && responseBlock[j + 1] == END[1]) {
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;
269 if (logger.isTraceEnabled()) {
270 logger.trace("receive RAW DATA: {}", dumpData(responseBlock));
273 byte[] cleanedBlock = cleanupBlock(responseBlock);
275 int dataSize = cleanedBlock[2];
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;
286 byte[] block = Arrays.copyOf(cleanedBlock, 3 + dataSize);
288 // validate calculated checksum against submitted
290 if (calculateChecksum(block) == checksum) {
291 if (logger.isTraceEnabled()) {
292 logger.trace("receive CMD: {} DATA: {}",
293 String.format("%02x", command.getReplyCmd()), dumpData(replyData));
300 logger.debug("Unable to handle data. Checksum verification failed");
302 logger.debug("Unable to handle data. Data size not valid");
305 if (logger.isTraceEnabled()) {
306 logger.trace("skip CMD: {} DATA: {}", String.format("%02x", command.getReplyCmd()),
307 dumpData(cleanedBlock));
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());
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"));
325 } catch (InterruptedException e) {
326 Thread.currentThread().interrupt();
327 logger.warn("Transmission was interrupted: {}", e.getMessage());
328 throw new RuntimeException(e);
330 } while (retry++ < MAX_RETRIES);
332 if (retry >= MAX_RETRIES) {
333 logger.debug("Unable to send command. {} retries failed.", retry);
336 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
340 * Generate the byte sequence for sending to ComfoAir (incl. START & END
341 * sequence and checksum).
345 * @return response byte value block with cmd, data and checksum
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];
353 block[1] = (byte) command;
354 block[2] = (byte) length;
356 if (requestData.length > 0) {
357 for (int i = 0; i < requestData.length; i++) {
358 block[i + 3] = (byte) requestData[i];
362 // calculate checksum for command block
363 byte checksum = calculateChecksum(block);
364 block[block.length - 1] = checksum;
366 // escape the command block with checksum included
367 block = escapeBlock(block);
368 byte[] request = new byte[4 + block.length];
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];
380 * Calculates a checksum for a command block (cmd, data and checksum).
383 * @return checksum byte value
385 private byte calculateChecksum(byte[] block) {
387 for (int i = 0; i < block.length; i++) {
392 return (byte) (datasum & 0xFF);
396 * Cleanup a commandblock from quoted 0x07 characters.
398 * @param processBuffer
399 * @return the 0x07 cleaned byte values
401 private byte[] cleanupBlock(byte[] processBuffer) {
403 byte[] cleanedBuffer = new byte[processBuffer.length];
405 for (int i = 4; i < processBuffer.length - 2; i++) {
406 if (CTRL == processBuffer[i] && CTRL == processBuffer[i + 1]) {
409 cleanedBuffer[pos] = processBuffer[i];
411 // Trim unrequested data in response
412 if (END[0] == processBuffer[i + 1] && END[1] == processBuffer[i + 2]) {
416 return Arrays.copyOf(cleanedBuffer, pos);
420 * Escape special 0x07 character.
422 * @param cleanedBuffer
423 * @return escaped byte value array
425 private byte[] escapeBlock(byte[] cleanedBuffer) {
427 byte[] processBuffer = new byte[50];
429 for (int i = 0; i < cleanedBuffer.length; i++) {
430 if (CTRL == cleanedBuffer[i]) {
431 processBuffer[pos] = CTRL;
434 processBuffer[pos] = cleanedBuffer[i];
437 return Arrays.copyOf(processBuffer, pos);
441 * Send the byte values.
444 * @return successful flag
446 private boolean send(byte[] request) {
447 if (logger.isTraceEnabled()) {
448 logger.trace("send DATA: {}", dumpData(request));
452 if (outputStream != null) {
453 outputStream.write(request);
456 } catch (IOException e) {
457 logger.debug("Error writing to serial port {}: {}", serialPortName, e.getLocalizedMessage());
463 * Is used to debug byte values.
468 public static String dumpData(int[] replyData) {
469 StringBuilder sb = new StringBuilder();
470 for (int ch : replyData) {
471 sb.append(String.format(" %02x", ch));
473 return sb.toString();
476 private String dumpData(byte[] data) {
477 StringBuilder sb = new StringBuilder();
478 for (byte ch : data) {
479 sb.append(String.format(" %02x", ch));
481 return sb.toString();
485 * Build request data based on reply data
488 * @param preRequestData
489 * @return new build int values array
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();
497 if (requestCmd != null && dataPosition != null && requestValue != null) {
498 switch (requestCmd) {
499 case ComfoAirCommandType.Constants.REQUEST_SET_DELAYS:
500 newRequestData = new int[8];
502 if (preRequestData.length > 0 && newRequestData.length <= preRequestData.length) {
503 System.arraycopy(preRequestData, 0, newRequestData, 0, newRequestData.length);
504 newRequestData[dataPosition] = requestValue;
506 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
509 case ComfoAirCommandType.Constants.REQUEST_SET_FAN_LEVEL:
510 newRequestData = new int[9];
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);
517 newRequestData[dataPosition] = requestValue;
519 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
522 case ComfoAirCommandType.Constants.REQUEST_SET_STATES:
523 newRequestData = new int[8];
525 if (preRequestData.length > 0 && newRequestData.length <= preRequestData.length) {
526 if (dataPosition == 4) {
527 requestValue = preRequestData[dataPosition]
528 + checkByteAndCalculateValue(command, requestValue, preRequestData[dataPosition]);
530 System.arraycopy(preRequestData, 0, newRequestData, 0, 6);
531 System.arraycopy(preRequestData, 9, newRequestData, 6, newRequestData.length - 6);
532 newRequestData[dataPosition] = requestValue;
534 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
537 case ComfoAirCommandType.Constants.REQUEST_SET_GHX:
538 newRequestData = new int[5];
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;
545 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
548 case ComfoAirCommandType.Constants.REQUEST_SET_ANALOGS:
549 newRequestData = new int[19];
551 if (preRequestData.length > 0 && newRequestData.length <= preRequestData.length) {
552 switch (dataPosition) {
556 requestValue = preRequestData[dataPosition] + checkByteAndCalculateValue(command,
557 requestValue, preRequestData[dataPosition]);
559 System.arraycopy(preRequestData, 0, newRequestData, 0, newRequestData.length);
560 newRequestData[dataPosition] = requestValue;
562 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
566 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
568 return newRequestData;
570 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
575 * Check if preValue contains possible byte and calculate new value
578 * @param requestValue
580 * @return new int value
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;
593 newValue = requestValue == 1 ? 0 : -possibleValue;
595 newValue = requestValue == 1 ? possibleValue : 0;
603 public boolean getIsSuspended() {