/** * 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.stiebelheatpump.protocol; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.openhab.binding.stiebelheatpump.internal.StiebelHeatPumpException; import org.openhab.binding.stiebelheatpump.protocol.RecordDefinition.Type; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Class for parse data packets from Stiebel heat pumps * * @author Peter Kreutzer * @param <T> * original protocol parser was written by Robert Penz in python * @since 1.5.0 * * Each response has the same structure as request header (four bytes), * optional data and footer: * * Header: 01 Read/Write: 00 for Read (get) response, 80 for Write (set) * response; in case of error during command exchange device stores error * code here; know error code : 03 = unknown command Checksum: ? 1 byte - * the same algorithm as for request Command: ? 1 byte - should match * Request.Command Data: ? only when Read, length depends on data type * Footer: 10 03 */ public class DataParser { private static final Logger logger = LoggerFactory.getLogger(DataParser.class); public static byte ESCAPE = (byte) 0x10; public static byte HEADERSTART = (byte) 0x01; public static byte END = (byte) 0x03; public static byte GET = (byte) 0x00; public static byte SET = (byte) 0x80; public static byte STARTCOMMUNICATION = (byte) 0x02; public static byte[] FOOTER = { ESCAPE, END }; public static byte[] DATAAVAILABLE = { ESCAPE, STARTCOMMUNICATION }; final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); public List<Request> parserConfiguration = new ArrayList<Request>(); public DataParser() { } /** * verifies response on availability of data * * @param response * of heat pump * @param request * request defined for heat pump response * @return Map of Strings with name and values */ public Map<String, String> parseRecords(final byte[] response, Request request) throws StiebelHeatPumpException { Map<String, String> map = new HashMap<String, String>(); logger.debug("Parse bytes: {}", DataParser.bytesToHex(response)); if (response.length < 2) { logger.error("response does not have a valid length of bytes: {}", DataParser.bytesToHex(response)); return map; } // parse response and fill map for (RecordDefinition recordDefinition : request.getRecordDefinitions()) { try { String value = parseRecord(response, recordDefinition); logger.debug("Parsed value {} -> {} with pos: {} , len: {}", recordDefinition.getName(), value, recordDefinition.getPosition(), recordDefinition.getLength()); map.put(recordDefinition.getName(), value); } catch (StiebelHeatPumpException e) { continue; } } return map; } /** * parses a single record * * @param response * of heat pump * @param RecordDefinition * that shall be used for parsing the heat pump response * @return string value of the parse response * @throws StiebelHeatPumpException */ public String parseRecord(byte[] response, RecordDefinition recordDefinition) throws StiebelHeatPumpException { try { if (response.length < 2) { logger.error("response does not have a valid length of bytes: {}", DataParser.bytesToHex(response)); throw new StiebelHeatPumpException(); } ByteBuffer buffer = ByteBuffer.wrap(response); short number = 0; byte[] bytes = null; switch (recordDefinition.getLength()) { case 1: bytes = new byte[1]; System.arraycopy(response, recordDefinition.getPosition(), bytes, 0, 1); number = Byte.valueOf(buffer.get(recordDefinition.getPosition())); break; case 2: bytes = new byte[2]; System.arraycopy(response, recordDefinition.getPosition(), bytes, 0, 2); number = buffer.getShort(recordDefinition.getPosition()); break; } if (recordDefinition.getBitPosition() > 0) { int returnValue = getBit(bytes, recordDefinition.getBitPosition()); return String.valueOf(returnValue); } if (recordDefinition.getScale() != 1.0) { double myDoubleNumber = number * recordDefinition.getScale(); myDoubleNumber = Math.round(myDoubleNumber * 100.0) / 100.0; String returnString = String.format("%s", myDoubleNumber); return returnString; } return String.valueOf(number); } catch (Exception e) { logger.error("response {} could not be parsed for record definition {} ", DataParser.bytesToHex(response), recordDefinition.getName()); throw new StiebelHeatPumpException(); } } /** * composes the new value of a record definition into a updated set command * that can be send back to heat pump * * @param response * of heat pump that should be updated with new value * @param RecordDefinition * that shall be used for compose the new value into the heat * pump set command * @param string * value to be compose * @return byte[] ready to send to heat pump * @throws StiebelHeatPumpException */ public byte[] composeRecord(String value, byte[] response, RecordDefinition recordDefinition) throws StiebelHeatPumpException { short newValue = 0; if (recordDefinition.getDataType() != Type.Settings) { logger.warn("The record {} can not be set as it is not a setable value!", recordDefinition.getName()); throw new StiebelHeatPumpException("record is not a setting!"); } double number = Double.parseDouble(value); if (number > recordDefinition.getMax() || number < recordDefinition.getMin()) { logger.warn("The record {} can not be set to value {} as allowed range is {}<-->{} !", recordDefinition.getName(), value, recordDefinition.getMax(), recordDefinition.getMin()); throw new StiebelHeatPumpException("invalid value !"); } // change response byte to setting command response[1] = SET; // reverse the scale if (recordDefinition.getScale() != 1.0) { number = number / recordDefinition.getScale(); newValue = (short) number; } // set new bit values in a byte if (recordDefinition.getBitPosition() > 0) { byte[] abyte = new byte[] { response[recordDefinition.getPosition()] }; abyte = setBit(abyte, recordDefinition.getBitPosition(), newValue); response[recordDefinition.getPosition()] = abyte[0]; return response; } // create byte values for single and double byte values // and update response switch (recordDefinition.getLength()) { case 1: byte newByteValue = (byte) number; response[recordDefinition.getPosition()] = newByteValue; break; case 2: byte[] newByteValues = shortToByte(newValue); int position = recordDefinition.getPosition(); response[position] = newByteValues[1]; response[position + 1] = newByteValues[0]; break; } response[2] = this.calculateChecksum(response); response = this.addDuplicatedBytes(response); logger.debug("Updated record {} at position {} to value {}.", recordDefinition.getName(), recordDefinition.getPosition(), value); return response; } /** * verifies response on availability of data * * @param response * of heat pump * @return true if the response of the heat pump indicates availability of * data */ public boolean dataAvailable(byte[] response) throws StiebelHeatPumpException { if (response.length == 0 || response.length > 2) { throw new StiebelHeatPumpException("invalid response length on request of data " + new String(response)); } if (response[0] != ESCAPE) { throw new StiebelHeatPumpException("invalid response on request of data " + new String(response)); } if (response.length == 2 && response[1] == DATAAVAILABLE[1]) { return true; } return false; } /** * verifies the header of the heat pump response * * @param response * of heat pump */ public void verifyHeader(byte[] response) throws StiebelHeatPumpException { if (response.length < 4) { throw new StiebelHeatPumpException("invalide response length on request of data " + new String(response)); } if (response[0] != HEADERSTART) { throw new StiebelHeatPumpException( "invalid response on request of data, found no header start: " + new String(response)); } if (response[1] != GET & response[1] != SET) { throw new StiebelHeatPumpException( "invalid response on request of data, response is neither get nor set: " + new String(response)); } if (response[2] != calculateChecksum(response)) { throw new StiebelHeatPumpException("invalid checksum on request of data " + new String(response)); } } /** * verifies the header of the heat pump response * * @param response * of heat pump * @return true if header is valid */ public boolean headerCheck(byte[] response) { try { verifyHeader(response); } catch (StiebelHeatPumpException e) { logger.debug("verification of response failed " + e.toString()); return false; } return true; } /** * verifies the heat pump response after data has been updated * * @param response * of heat pump * @return true if data set has been confirmed */ public boolean setDataCheck(byte[] response) { try { verifyHeader(response); } catch (StiebelHeatPumpException e) { return false; } return true; } /** * calculates the checksum of a byte data array * * @param data * to calculate the checksum for * @param withReplace * to set if the byte array shall be corrected by special replace * method * @return calculated checksum as short */ public byte calculateChecksum(byte[] data) throws StiebelHeatPumpException { if (data.length < 5) { throw new StiebelHeatPumpException("no valid byte[] for calulation of checksum!"); } int checkSum = 0, i = 0; for (i = 0; i < data.length - 2; i++) { if (i == 2) { continue; } checkSum += (short) (data[i] & 0xFF); } return shortToByte((short) checkSum)[0]; } /** * converts short to byte[] * * @return array of bytes */ public byte[] shortToByte(short value) { byte[] returnByteArray = new byte[2]; returnByteArray[0] = (byte) (value & 0xff); returnByteArray[1] = (byte) ((value >> 8) & 0xff); return returnByteArray; } /** * converts integer to byte[] * * @return array of bytes */ public byte[] intToByte(int checkSum) { byte[] returnByteArray = ByteBuffer.allocate(4).putInt(checkSum).array(); return returnByteArray; } /** * converts byte to short * * @return short */ private short byteToShort(byte[] bytes) throws StiebelHeatPumpException { return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getShort(); } /** * Search the data byte array for the first occurrence of the byte array * pattern. raw data received from device have to be de-escaped before * header evaluation and data use: - each sequence 2B 18 must be replaced * with single byte 2B - each sequence 10 10 must be replaced with single * byte 10 * * @param data * as byte array representing response from heat pump which shall * be fixed * @return byte array with fixed byte entries */ public byte[] fixDuplicatedBytes(byte[] data) { // first copy the data except the last 2 bytes , the footer byte[] bytesToBeAnalyzed = new byte[data.length - 2]; System.arraycopy(data, 0, bytesToBeAnalyzed, 0, data.length - 2); byte[] fixedData = findReplace(bytesToBeAnalyzed, new byte[] { (byte) 0x10, (byte) 0x10 }, new byte[] { (byte) 0x10 }); fixedData = findReplace(fixedData, new byte[] { (byte) 0x2b, (byte) 0x18 }, new byte[] { (byte) 0x2b }); byte[] result = new byte[fixedData.length + FOOTER.length]; // copy fixedData to result System.arraycopy(fixedData, 0, result, 0, fixedData.length); // copy footer to result System.arraycopy(FOOTER, 0, result, fixedData.length, FOOTER.length); return result; } /** * Search the data byte array for the first occurrence of the byte array * pattern. raw data received from device have to be de-escaped before * header evaluation and data use: - each sequence 2B must be replaced with * single byte 2B 18 - each sequence 10 must be replaced with single byte 10 * 10 * * @param data * as byte array representing response from heat pump which shall * be fixed * @return byte array with fixed byte entries */ public byte[] addDuplicatedBytes(byte[] data) { ByteBuffer byteBuffer = ByteBuffer.allocate(data.length * 2); // add header without changes for (int i = 0; i < 2; i++) { byteBuffer.put(data[i]); } // add now duplicates for (int i = 2; i < data.length - 2; i++) { byteBuffer.put(data[i]); if (data[i] == (byte) 0x10) { byteBuffer.put(data[i]); } if (data[i] == (byte) 0x2b) { byteBuffer.put((byte) 0x18); } } // add footer without changes for (int i = data.length - 2; i < data.length; i++) { byteBuffer.put(data[i]); } byte[] newdata = new byte[byteBuffer.position()]; byteBuffer.rewind(); byteBuffer.get(newdata); return newdata; } /** * Search the data byte array for the first occurrence of the byte array * pattern. * * @param data * as byte array to search into and to replace the pattern bytes * with replace bytes * @param pattern * as byte array to search for * @param replace * as byte array to replace with * @return byte array which has pattern bytes been replaced with replace * bytes */ public byte[] findReplace(byte[] data, byte[] pattern, byte[] replace) { int position = indexOf(data, pattern); while (position >= 0) { byte[] newData = new byte[data.length - pattern.length + replace.length]; System.arraycopy(data, 0, newData, 0, position); System.arraycopy(replace, 0, newData, position, replace.length); System.arraycopy(data, position + pattern.length, newData, position + replace.length, data.length - position - pattern.length); position = indexOf(newData, pattern); data = new byte[newData.length]; System.arraycopy(newData, 0, data, 0, newData.length); } return data; } /** * Search the data byte array for the first occurrence of the byte array * pattern. * * @param data * to find pattern in * @param pattern * to be searched * @return byte number were pattern was found in data */ private int indexOf(byte[] data, byte[] pattern) { int[] failure = computeFailure(pattern); int j = 0; for (int i = 0; i < data.length; i++) { while (j > 0 && pattern[j] != data[i]) { j = failure[j - 1]; } if (pattern[j] == data[i]) { j++; } if (j == pattern.length) { return i - pattern.length + 1; } } return -1; } /** * Computes the failure function using a boot-strapping process, where the * pattern is matched against itself. */ private int[] computeFailure(byte[] pattern) { int[] failure = new int[pattern.length]; int j = 0; for (int i = 1; i < pattern.length; i++) { while (j > 0 && pattern[j] != pattern[i]) { j = failure[j - 1]; } if (pattern[j] == pattern[i]) { j++; } failure[i] = j; } return failure; } /** * Gets one bit back from a bit string stored in a byte array at the * specified position. * * @param data * , byte array to pick short value from * @param position * to get the bit value * @return integer value 1 or 0 that represents the bit */ private int getBit(byte[] data, int pos) { int posByte = pos / 8; int posBit = pos % 8; byte valByte = data[posByte]; int valInt = valByte >> (8 - (posBit + 1)) & 0x0001; return valInt; } /** * Sets one bit to a bit string at the specified position with the specified * bit value. * * @param data * , byte array to pick short value from * @param position * to set the bit * @param value * to set the bit to (0 or 1) */ private byte[] setBit(byte[] data, int position, int value) { int posByte = position / 8; int posBit = position % 8; byte oldByte = data[posByte]; oldByte = (byte) (((0xFF7F >> posBit) & oldByte) & 0x00FF); byte newByte = (byte) ((value << (8 - (posBit + 1))) | oldByte); data[posByte] = newByte; return data; } /** * Converts a byte array to good readable string. * * @param bytes * to be converted * @return string representing the bytes */ public static String bytesToHex(byte[] bytes) { int dwords = bytes.length / 4 + 1; char[] hexChars = new char[bytes.length * 3 + dwords * 4]; int position = 0; for (int j = 0; j < bytes.length; j++) { int v = bytes[j] & 0xFF; if (j % 4 == 0) { String str = "(" + String.format("%02d", j) + ")"; char[] charArray = str.toCharArray(); for (char character : charArray) { hexChars[position] = character; position++; } } hexChars[position] = hexArray[v >>> 4]; position++; hexChars[position] = hexArray[v & 0x0F]; position++; hexChars[position] = ' '; position++; } return new String(hexChars); } }