/** * Copyright (c) 2010-2016 by the respective copyright holders. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.comfoair.handling; import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Enumeration; import java.util.TooManyListenersException; import org.apache.commons.io.IOUtils; import org.openhab.binding.comfoair.internal.InitializationException; import org.openhab.core.library.types.DecimalType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import gnu.io.CommPortIdentifier; import gnu.io.NoSuchPortException; import gnu.io.PortInUseException; import gnu.io.SerialPort; import gnu.io.SerialPortEvent; import gnu.io.SerialPortEventListener; import gnu.io.UnsupportedCommOperationException; /** * With this connector the hole serial communication is handled. * * @author Holger Hees * @since 1.3.0 */ public class ComfoAirConnector { private static final Logger logger = LoggerFactory.getLogger(ComfoAirConnector.class); private static byte[] START = { (byte) 0x07, (byte) 0xf0 }; private static byte[] END = { (byte) 0x07, (byte) 0x0f }; private static byte[] ACK = { (byte) 0x07, (byte) 0xf3 }; private boolean isSuspended = true; private String port; private SerialPort serialPort; private InputStream inputStream; private OutputStream outputStream; /** * Open and initialize a serial port. * * @param portName * e.g. /dev/ttyS0 * @param listener * the listener which is informed after a successful response * read * @throws InitializationException */ public void open(String portName) throws InitializationException { logger.debug("Open ComfoAir connection"); port = portName; CommPortIdentifier portIdentifier; try { portIdentifier = CommPortIdentifier.getPortIdentifier(port); try { serialPort = portIdentifier.open("openhab", 3000); serialPort.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE); serialPort.enableReceiveThreshold(1); serialPort.enableReceiveTimeout(1000); // RXTX serial port library causes high CPU load // Start event listener, which will just sleep and slow down event loop serialPort.addEventListener(new CPUWorkaroundThread()); serialPort.notifyOnDataAvailable(true); inputStream = new DataInputStream(new BufferedInputStream(serialPort.getInputStream())); outputStream = serialPort.getOutputStream(); ComfoAirCommand command = ComfoAirCommandType.getChangeCommand(ComfoAirCommandType.ACTIVATE.key, new DecimalType(1)); sendCommand(command); } catch (PortInUseException e) { throw new InitializationException(e); } catch (UnsupportedCommOperationException e) { throw new InitializationException(e); } catch (IOException e) { throw new InitializationException(e); } catch (TooManyListenersException e) { throw new InitializationException(e); } } catch (NoSuchPortException e) { StringBuilder sb = new StringBuilder(); @SuppressWarnings("rawtypes") Enumeration portList = CommPortIdentifier.getPortIdentifiers(); while (portList.hasMoreElements()) { CommPortIdentifier id = (CommPortIdentifier) portList.nextElement(); if (id.getPortType() == CommPortIdentifier.PORT_SERIAL) { sb.append(id.getName() + "\n"); } } throw new InitializationException( "Serial port '" + port + "' could not be found. Available ports are:\n" + sb.toString()); } } /** * Close the serial port. */ public void close() { logger.debug("Close ComfoAir connection"); ComfoAirCommand command = ComfoAirCommandType.getChangeCommand(ComfoAirCommandType.ACTIVATE.key, new DecimalType(0)); sendCommand(command); IOUtils.closeQuietly(inputStream); IOUtils.closeQuietly(outputStream); serialPort.close(); } /** * Prepare a command for sending using the serial port. * * @param command * @return reply byte values */ public synchronized int[] sendCommand(ComfoAirCommand command) { int requestCmd = command.getRequestCmd(); int retry = 0; // Switch support for app or ccease control if (requestCmd == 0x9b) { isSuspended = !isSuspended; } else if (requestCmd == 0x9c) { return new int[] { isSuspended ? 0x00 : 0x03 }; } else if (isSuspended) { logger.debug("Ignore cmd. Service is currently suspended"); return null; } do { int[] requestData = command.getRequestData(); // Fake read request for ccease properties if (requestData == null && requestCmd == 0x37) { requestData = new int[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; } byte[] requestBlock = calculateRequest(requestCmd, requestData); if (!send(requestBlock)) { return null; } byte[] responseBlock = new byte[0]; try { // 31 is max. response length byte[] readBuffer = new byte[31]; do { while (inputStream.available() > 0) { int bytes = inputStream.read(readBuffer); // merge bytes byte[] mergedBytes = new byte[responseBlock.length + bytes]; System.arraycopy(responseBlock, 0, mergedBytes, 0, responseBlock.length); System.arraycopy(readBuffer, 0, mergedBytes, responseBlock.length, bytes); responseBlock = mergedBytes; } try { // add wait states around reading the stream, so that // interrupted transmissions are merged Thread.sleep(100); } catch (InterruptedException e) { // ignore interruption } } while (inputStream.available() > 0); // check for ACK if (responseBlock.length >= 2 && responseBlock[0] == (byte) 0x07 && responseBlock[1] == (byte) 0xf3) { if (command.getReplyCmd() == null) { // confirm additional data with an ACK if (responseBlock.length > 2) { send(ACK); } return null; } // check for start and end sequence and if the response cmd // matches // 11 is the minimum response length with one data byte if (responseBlock.length >= 11 && responseBlock[2] == (byte) 0x07 && responseBlock[3] == (byte) 0xf0 && responseBlock[responseBlock.length - 2] == (byte) 0x07 && responseBlock[responseBlock.length - 1] == (byte) 0x0f && (responseBlock[5] & 0xff) == command.getReplyCmd()) { logger.debug("receive RAW DATA: " + dumpData(responseBlock)); byte[] cleanedBlock = cleanupBlock(responseBlock); int dataSize = cleanedBlock[2]; // the cleanedBlock size should equal dataSize + 2 cmd // bytes and + 1 checksum byte if (dataSize + 3 == cleanedBlock.length - 1) { byte checksum = cleanedBlock[dataSize + 3]; int[] replyData = new int[dataSize]; for (int i = 0; i < dataSize; i++) { replyData[i] = cleanedBlock[i + 3] & 0xff; } byte[] _block = new byte[3 + replyData.length]; System.arraycopy(cleanedBlock, 0, _block, 0, _block.length); // validate calculated checksum against submitted // checksum if (calculateChecksum(_block) == checksum) { logger.debug(String.format("receive CMD: %02x", command.getReplyCmd()) + " DATA: " + dumpData(replyData)); send(ACK); return replyData; } logger.warn("Unable to handle data. Checksum verification failed"); } else { logger.warn("Unable to handle data. Data size not valid"); } logger.warn(String.format("skip CMD: %02x", command.getReplyCmd()) + " DATA: " + dumpData(cleanedBlock)); } } } catch (IOException e) { logger.error(e.getMessage(), e); } try { Thread.sleep(1000); logger.warn("Retry cmd. Last call was not successful." + " Request: " + dumpData(requestBlock) + " Response: " + (responseBlock.length > 0 ? dumpData(responseBlock) : "null")); } catch (InterruptedException e) { // ignore interruption } } while (retry++ < 5); if (retry == 5) { logger.error("Unable to send command. " + retry + " retries failed."); } return null; } /** * Generate the byte sequence for sending to ComfoAir (incl. START & END * sequence and checksum). * * @param command * @param data * @return response byte value block with cmd, data and checksum */ private byte[] calculateRequest(int command, int[] data) { // generate the command block (cmd and request data) int length = data == null ? 0 : data.length; byte[] block = new byte[4 + length]; block[0] = 0x00; block[1] = (byte) command; block[2] = (byte) length; if (data != null) { for (int i = 0; i < data.length; i++) { block[i + 3] = (byte) data[i]; } } // calculate checksum for command block byte checksum = calculateChecksum(block); block[block.length - 1] = checksum; // escape the command block with checksum included block = escapeBlock(block); byte[] request = new byte[4 + block.length]; request[0] = START[0]; request[1] = START[1]; System.arraycopy(block, 0, request, 2, block.length); request[request.length - 2] = END[0]; request[request.length - 1] = END[1]; return request; } /** * Calculates a checksum for a command block (cmd, data and checksum). * * @param block * @return checksum byte value */ private byte calculateChecksum(byte[] block) { int datasum = 0; for (int i = 0; i < block.length; i++) { datasum += block[i]; } datasum += 173; String hexString = Integer.toHexString(datasum); if (hexString.length() > 2) { hexString = hexString.substring(hexString.length() - 2); } return (byte) Integer.parseInt(hexString, 16); } /** * Cleanup a commandblock from quoted 0x07 characters. * * @param processBuffer * @return the 0x07 cleaned byte values */ private byte[] cleanupBlock(byte[] processBuffer) { int pos = 0; byte[] cleanedBuffer = new byte[50]; for (int i = 4; i < processBuffer.length - 2; i++) { if ((byte) 0x07 == processBuffer[i]) { i++; } cleanedBuffer[pos] = processBuffer[i]; pos++; } byte[] _block = new byte[pos]; System.arraycopy(cleanedBuffer, 0, _block, 0, _block.length); return _block; } /** * Escape special 0x07 character. * * @param cleanedBuffer * @return escaped byte value array */ private byte[] escapeBlock(byte[] cleanedBuffer) { int pos = 0; byte[] processBuffer = new byte[50]; for (int i = 0; i < cleanedBuffer.length; i++) { if ((byte) 0x07 == cleanedBuffer[i]) { processBuffer[pos] = (byte) 0x07; pos++; } processBuffer[pos] = cleanedBuffer[i]; pos++; } byte[] _block = new byte[pos]; System.arraycopy(processBuffer, 0, _block, 0, _block.length); return _block; } /** * Send the byte values. * * @param request * @return successful flag */ private boolean send(byte[] request) { logger.debug("send DATA: " + dumpData(request)); try { outputStream.write(request); return true; } catch (IOException e) { logger.error("Error writing to serial port {}: {}", port, e.getLocalizedMessage()); return false; } } /** * Is used to debug byte values. * * @param data * @return */ public static String dumpData(int[] data) { StringBuffer sb = new StringBuffer(); for (int ch : data) { sb.append(String.format(" %02x", ch)); } return sb.toString(); } private String dumpData(byte[] data) { StringBuffer sb = new StringBuffer(); for (byte ch : data) { sb.append(String.format(" %02x", ch)); } return sb.toString(); } private class CPUWorkaroundThread implements SerialPortEventListener { @Override public void serialEvent(SerialPortEvent event) { try { logger.trace("RXTX library CPU load workaround, sleep forever"); Thread.sleep(Long.MAX_VALUE); } catch (InterruptedException e) { } } } }