2 * Copyright (c) 2010-2020 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 RS232_ENABLED_VALUE = 0x03;
51 private static final int RS232_DISABLED_VALUE = 0x00;
53 private boolean isSuspended = true;
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;
62 public ComfoAirSerialConnector(final SerialPortManager serialPortManager, final String serialPortName,
64 this.serialPortManager = serialPortManager;
65 this.serialPortName = serialPortName;
66 this.baudRate = baudRate;
72 * @throws PortInUseException, UnsupportedCommOperationException, IOException
74 public void open() throws ComfoAirSerialException {
75 logger.debug("open(): Opening ComfoAir connection");
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;
88 inputStream = new DataInputStream(new BufferedInputStream(serialPort.getInputStream()));
89 outputStream = serialPort.getOutputStream();
91 ComfoAirCommand command = ComfoAirCommandType.getChangeCommand(ComfoAirCommandType.ACTIVATE.getKey(),
94 if (command != null) {
95 sendCommand(command, ComfoAirCommandType.Constants.EMPTY_INT_ARRAY);
97 logger.debug("Failure while creating COMMAND: {}", command);
100 throw new ComfoAirSerialException("No such Port: " + serialPortName);
102 } catch (PortInUseException | UnsupportedCommOperationException | IOException e) {
103 throw new ComfoAirSerialException(e);
110 public void close() {
111 logger.debug("close(): Close ComfoAir connection");
112 SerialPort serialPort = this.serialPort;
114 if (serialPort != null) {
115 ComfoAirCommand command = ComfoAirCommandType.getChangeCommand(ComfoAirCommandType.ACTIVATE.getKey(),
118 if (command != null) {
119 sendCommand(command, ComfoAirCommandType.Constants.EMPTY_INT_ARRAY);
121 logger.debug("Failure while creating COMMAND: {}", command);
124 if (inputStream != null) {
127 } catch (IOException e) {
128 logger.debug("Error while closing input stream: {}", e.getMessage());
132 if (outputStream != null) {
134 outputStream.close();
135 } catch (IOException e) {
136 logger.debug("Error while closing output stream: {}", e.getMessage());
145 * Prepare a command for sending using the serial port.
148 * @param preRequestData
149 * @return reply byte values
151 public synchronized int[] sendCommand(ComfoAirCommand command, int[] preRequestData) {
152 Integer requestCmd = command.getRequestCmd();
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;
167 // If preRequestData param was send (preRequestData is sending for write command)
170 if (preRequestData.length <= 0) {
171 requestData = command.getRequestData();
173 requestData = buildRequestData(command, preRequestData);
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;
182 byte[] requestBlock = calculateRequest(requestCmd, requestData);
183 if (logger.isTraceEnabled()) {
184 logger.trace("send DATA: {}", dumpData(requestBlock));
187 if (!send(requestBlock)) {
188 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
191 byte[] responseBlock = new byte[0];
194 InputStream inputStream = this.inputStream;
195 // 31 is max. response length
196 byte[] readBuffer = new byte[31];
198 while (inputStream != null && inputStream.available() > 0) {
199 int bytes = inputStream.read(readBuffer);
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);
206 responseBlock = mergedBytes;
209 // add wait states around reading the stream, so that
210 // interrupted transmissions are merged
212 } catch (InterruptedException e) {
213 Thread.currentThread().interrupt();
214 logger.warn("Transmission was interrupted: {}", e.getMessage());
215 throw new RuntimeException(e);
217 } while (inputStream != null && inputStream.available() > 0);
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) {
226 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
229 // check for start and end sequence and if the response cmd
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));
240 byte[] cleanedBlock = cleanupBlock(responseBlock);
242 int dataSize = cleanedBlock[2];
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;
253 byte[] block = Arrays.copyOf(cleanedBlock, 3 + dataSize);
255 // validate calculated checksum against submitted
257 if (calculateChecksum(block) == checksum) {
258 if (logger.isTraceEnabled()) {
259 logger.trace("receive CMD: {} DATA: {}",
260 String.format("%02x", command.getReplyCmd()), dumpData(replyData));
267 logger.debug("Unable to handle data. Checksum verification failed");
269 logger.debug("Unable to handle data. Data size not valid");
272 if (logger.isTraceEnabled()) {
273 logger.trace("skip CMD: {} DATA: {}", String.format("%02x", command.getReplyCmd()),
274 dumpData(cleanedBlock));
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());
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"));
292 } catch (InterruptedException e) {
293 Thread.currentThread().interrupt();
294 logger.warn("Transmission was interrupted: {}", e.getMessage());
295 throw new RuntimeException(e);
297 } while (retry++ < 5);
300 logger.debug("Unable to send command. {} retries failed.", retry);
303 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
307 * Generate the byte sequence for sending to ComfoAir (incl. START & END
308 * sequence and checksum).
312 * @return response byte value block with cmd, data and checksum
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];
320 block[1] = (byte) command;
321 block[2] = (byte) length;
323 if (requestData.length > 0) {
324 for (int i = 0; i < requestData.length; i++) {
325 block[i + 3] = (byte) requestData[i];
329 // calculate checksum for command block
330 byte checksum = calculateChecksum(block);
331 block[block.length - 1] = checksum;
333 // escape the command block with checksum included
334 block = escapeBlock(block);
335 byte[] request = new byte[4 + block.length];
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];
347 * Calculates a checksum for a command block (cmd, data and checksum).
350 * @return checksum byte value
352 private byte calculateChecksum(byte[] block) {
354 for (int i = 0; i < block.length; i++) {
359 return (byte) (datasum & 0xFF);
363 * Cleanup a commandblock from quoted 0x07 characters.
365 * @param processBuffer
366 * @return the 0x07 cleaned byte values
368 private byte[] cleanupBlock(byte[] processBuffer) {
370 byte[] cleanedBuffer = new byte[processBuffer.length];
372 for (int i = 4; i < processBuffer.length - 2; i++) {
373 if (CTRL == processBuffer[i] && CTRL == processBuffer[i + 1]) {
376 cleanedBuffer[pos] = processBuffer[i];
379 return Arrays.copyOf(cleanedBuffer, pos);
383 * Escape special 0x07 character.
385 * @param cleanedBuffer
386 * @return escaped byte value array
388 private byte[] escapeBlock(byte[] cleanedBuffer) {
390 byte[] processBuffer = new byte[50];
392 for (int i = 0; i < cleanedBuffer.length; i++) {
393 if (CTRL == cleanedBuffer[i]) {
394 processBuffer[pos] = CTRL;
397 processBuffer[pos] = cleanedBuffer[i];
400 return Arrays.copyOf(processBuffer, pos);
404 * Send the byte values.
407 * @return successful flag
409 private boolean send(byte[] request) {
410 if (logger.isTraceEnabled()) {
411 logger.trace("send DATA: {}", dumpData(request));
415 if (outputStream != null) {
416 outputStream.write(request);
419 } catch (IOException e) {
420 logger.debug("Error writing to serial port {}: {}", serialPortName, e.getLocalizedMessage());
426 * Is used to debug byte values.
431 public static String dumpData(int[] replyData) {
432 StringBuilder sb = new StringBuilder();
433 for (int ch : replyData) {
434 sb.append(String.format(" %02x", ch));
436 return sb.toString();
439 private String dumpData(byte[] data) {
440 StringBuilder sb = new StringBuilder();
441 for (byte ch : data) {
442 sb.append(String.format(" %02x", ch));
444 return sb.toString();
448 * Build request data based on reply data
451 * @param preRequestData
452 * @return new build int values array
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();
460 if (requestCmd != null && dataPosition != null && requestValue != null) {
461 switch (requestCmd) {
462 case ComfoAirCommandType.Constants.REQUEST_SET_DELAYS:
463 newRequestData = new int[8];
465 if (preRequestData.length > 0 && newRequestData.length <= preRequestData.length) {
466 System.arraycopy(preRequestData, 0, newRequestData, 0, newRequestData.length);
467 newRequestData[dataPosition] = requestValue;
469 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
472 case ComfoAirCommandType.Constants.REQUEST_SET_FAN_LEVEL:
473 newRequestData = new int[9];
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;
480 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
483 case ComfoAirCommandType.Constants.REQUEST_SET_STATES:
484 newRequestData = new int[8];
486 if (preRequestData.length > 0 && newRequestData.length <= preRequestData.length) {
487 if (dataPosition == 4) {
488 requestValue = preRequestData[dataPosition]
489 + checkByteAndCalculateValue(command, requestValue, preRequestData[dataPosition]);
491 System.arraycopy(preRequestData, 0, newRequestData, 0, 6);
492 System.arraycopy(preRequestData, 9, newRequestData, 6, newRequestData.length - 6);
493 newRequestData[dataPosition] = requestValue;
495 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
498 case ComfoAirCommandType.Constants.REQUEST_SET_EWT:
499 newRequestData = new int[5];
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;
506 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
509 case ComfoAirCommandType.Constants.REQUEST_SET_ANALOGS:
510 newRequestData = new int[19];
512 if (preRequestData.length > 0 && newRequestData.length <= preRequestData.length) {
513 switch (dataPosition) {
517 requestValue = preRequestData[dataPosition] + checkByteAndCalculateValue(command,
518 requestValue, preRequestData[dataPosition]);
520 System.arraycopy(preRequestData, 0, newRequestData, 0, newRequestData.length);
521 newRequestData[dataPosition] = requestValue;
523 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
527 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
529 return newRequestData;
531 return ComfoAirCommandType.Constants.EMPTY_INT_ARRAY;
536 * Check if preValue contains possible byte and calculate new value
539 * @param requestValue
541 * @return new int value
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;
554 newValue = requestValue == 1 ? 0 : -possibleValue;
556 newValue = requestValue == 1 ? possibleValue : 0;