/**
* 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.smarthomatic.internal;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.zip.CRC32;
import javax.xml.bind.DatatypeConverter;
import org.apache.commons.lang.NotImplementedException;
import org.openhab.binding.smarthomatic.internal.packetData.Array;
import org.openhab.binding.smarthomatic.internal.packetData.BoolValue;
import org.openhab.binding.smarthomatic.internal.packetData.IntValue;
import org.openhab.binding.smarthomatic.internal.packetData.Packet;
import org.openhab.binding.smarthomatic.internal.packetData.Packet.MessageGroup;
import org.openhab.binding.smarthomatic.internal.packetData.Packet.MessageGroup.Message;
import org.openhab.binding.smarthomatic.internal.packetData.UIntValue;
import org.openhab.core.items.Item;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.types.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* class represents a complete smarthomatic message it also includes the
* capability of parsing and creating smarthomatic messages for detailed
* description of smarthomatic messages
* {@link https://www.smarthomatic.org/basics/communication_protocol.html}
*
* @author mcjobo
* @author arohde
* @since 1.9.0
*/
public class SHCMessage {
private static final Logger logger = LoggerFactory.getLogger(SHCMessage.class);
/**
* constants
*
*/
public static final String DATA_FLAG = "PKT:";
public static final int DATA_FLAG_SIZE = DATA_FLAG.length();
public static final String SENDER_ID = "SID";
public static final String PACKET_COUNTER = "PC";
public static final String MESSAGE_TYPE = "MT";
public static final String MESSAGE_GROUP_ID = "MGID";
public static final String MESSAGE_ID = "MID";
public static final String MESSAGE_DATA = "MD";
// MessageGroupIDs
public static final int GRP_Generic = 0;
public static final int GRP_GPIO = 1;
public static final int GRP_Weather = 10;
public static final int GRP_Environment = 11;
public static final int GRP_Powerswitch = 20;
public static final int GRP_Dimmer = 60;
@SuppressWarnings("unused")
private String originalMessage;
private String editedMessage;
private SHCHeader header;
private Packet packet;
public static Charset charset = Charset.forName("UTF-8");
public static CharsetEncoder encoder = charset.newEncoder();
/**
* the smarthomatic header class represents the part of a message that is
* common to all messages for details of header format see
* {@link https://www.smarthomatic.org/basics/communication_protocol.html}
*
* @author mcjobo
* @author arohde
* @since 1.9.0
*/
public class SHCHeader {
private int SenderID; // =
// tokens[0].substring(tokens[0].indexOf("=")+1);
private int MessageType; // =
// tokens[2].substring(tokens[2].indexOf("=")+1);
private long MessageGroupID;// =
// tokens[3].substring(tokens[3].indexOf("=")+1);
private int MessageID; // =
// tokens[4].substring(tokens[4].indexOf("=")+1);
private byte[] MessageData; // =
// tokens[5].substring(tokens[5].indexOf("=")+1);
/**
* getter for the parsed sender id
*
* @return
*/
public int getSenderID() {
return SenderID;
}
/**
* getter for the parsed message type
*
* @return
*/
public int getMessageType() {
return MessageType;
}
/**
* getter for the parsed message group id
*
* @return
*/
public long getMessageGroupID() {
return MessageGroupID;
}
/**
* getter for the parsed message id
*
* @return
*/
public int getMessageID() {
return MessageID;
}
/**
* getter for the parsed message data
*
* @return
*/
public byte[] getMessageData() {
return MessageData;
}
/**
* constructor to create a new smarthomatic header with the given data
*
* @param data
*/
public SHCHeader(String data) {
StringTokenizer dataTok = new StringTokenizer(data, ";");
String[] tokens = new String[dataTok.countTokens()];
Map<String, String> tokensMap = new HashMap<String, String>();
int i = 0;
while (dataTok.hasMoreTokens()) {
String nextToken = dataTok.nextToken();
tokens[i++] = nextToken;
String[] split = nextToken.split("=");
if (split.length == 2) {
tokensMap.put(split[0].trim(), split[1].trim());
}
}
String crc = tokens[tokens.length - 1].trim();
CRC32 crc32 = new CRC32();
String substring = data.substring(0, data.lastIndexOf(";") + 1);
crc32.update(substring.getBytes());
long value = crc32.getValue();
String decode = Long.toHexString(value);
decode = fillStringWithLeadingZeros(decode, crc.length());
if (!crc.equals(decode)) {
RuntimeException runtimeException = new RuntimeException("SHC Binding CRC Error");
logger.error("SHC Binding CRC Error calculated CRC: {} found CRC: {}", runtimeException, decode, crc);
throw runtimeException;
}
SenderID = Integer.parseInt(tokensMap.get(SENDER_ID));
MessageType = Integer.parseInt(tokensMap.get(MESSAGE_TYPE));
if (MessageType == 0 || MessageType == 1 || MessageType == 2 || MessageType == 8 || MessageType == 10) {
MessageGroupID = Integer.parseInt(tokensMap.get(MESSAGE_GROUP_ID));
MessageID = Integer.parseInt(tokensMap.get(MESSAGE_ID));
}
if (MessageType == 1 || MessageType == 2 || MessageType == 8 || MessageType == 10) {
MessageData = DatatypeConverter.parseHexBinary(tokensMap.get(MESSAGE_DATA));
}
logger.trace("BaseStation SenderID: {}", SenderID);
logger.trace("BaseStation MessageType: {}", MessageType);
logger.trace("BaseStation MessageGroupID: {}", MessageGroupID);
logger.trace("BaseStation MessageID: {}", MessageID);
logger.trace("BaseStation MessageData: {}", MessageData);
}
}
private String fillStringWithLeadingZeros(String value, int length) {
while (value.length() < length) {
value = "0" + value;
}
return value;
}
/**
* these part represent the message data of the different smarthomatic
* messages details of a messages data is described here:
* {@link https://www.smarthomatic.org/basics/message_catalog.html}
*
* @author mcjobo
* @author arohde
* @since 1.9.0
*/
public class SHCData {
private List<Type> openHABTypes = new ArrayList<Type>();
public SHCData(SHCHeader header) {
byte[] data = header.getMessageData();
MessageGroup group = null;
Message message = null;
int messageType = header.getMessageType();
if (messageType == 8 || messageType == 10) {
for (MessageGroup messageGroup : packet.getMessageGroup()) {
if (messageGroup.getMessageGroupID() == header.getMessageGroupID()) {
group = messageGroup;
break;
}
}
for (Message message1 : group.getMessage()) {
if (message1.getMessageID() == header.getMessageID()) {
message = message1;
break;
}
}
int startBit = 0;
startBit = getDataValues(startBit, message.getDataValue(), data);
}
}
private int getDataValues(int startBit, List<Object> dataValues, byte[] data) {
for (Object object : dataValues) {
if (object instanceof UIntValue) {
UIntValue value = (UIntValue) object;
Integer result = parseData(data, value.getBits(), startBit, false);
openHABTypes.add(new DecimalType(result));
startBit += value.getBits();
} else if (object instanceof IntValue) {
IntValue value = (IntValue) object;
Integer result = parseData(data, value.getBits(), startBit, true);
openHABTypes.add(new DecimalType(result));
startBit += value.getBits();
} else if (object instanceof BoolValue) {
Integer value = parseData(data, 1, startBit, false);
if (value > 0) {
openHABTypes.add(OnOffType.ON);
} else {
openHABTypes.add(OnOffType.OFF);
}
startBit += 1;
} else if (object instanceof Array) {
Array value = (Array) object;
for (int i = 0; i < value.getLength(); i++) {
startBit = getDataValues(startBit, value.getArrayDataValue(), data);
}
} else {
throw new NotImplementedException();
}
}
return startBit;
}
private Integer parseData(byte[] data, long bits, int startBit, boolean signed) {
int bits2skip = startBit;
long bits2add = bits;
int result = 0;
// Iterate over bytes and bits
for (byte b : data) {
for (int mask = 0x80; mask != 0x00; mask >>= 1) {
boolean bitvalue = (b & mask) != 0;
// no more bits2add, do nothing
if (bits2add < 0) {
}
// skip until start bit is reached
else if (bits2skip > 0) {
bits2skip--;
} else {
if (bitvalue) {
result = result + (int) Math.pow(2, bits2add - 1);
}
bits2add--;
}
}
}
if (signed) {
if (result >= (int) Math.pow(2, bits - 1)) {
result = result - (int) Math.pow(2, bits);
}
}
return result;
}
/**
* returns the openhab types found
*
* @return
*/
public List<Type> getOpenHABTypes() {
return openHABTypes;
}
/**
* string representation of the smarthomatic data
*/
@Override
public String toString() {
String res = "SHCData [";
for (Type type : openHABTypes) {
res += type.toString();
}
res += "]";
return res;
}
}
/**
* constructor to create a new smarthomatic message
*
* @param message
* @param packet
*/
public SHCMessage(String message, Packet packet) {
originalMessage = message;
editedMessage = message.substring(message.indexOf(DATA_FLAG) + DATA_FLAG_SIZE);
header = new SHCHeader(editedMessage);
this.packet = packet;
}
/**
* getter to return the parsed header
*
* @return
*/
public SHCHeader getHeader() {
return header;
}
/**
* getter to return the parsed data object
*
* @return
*/
public SHCData getData() {
return new SHCData(header);
}
/**
* getter to return the openhab states parsed from the smarthomtic message
*
* @param object
* @return
*/
public List<Type> openHABStateFromSHCMessage(Item object) {
return getData().getOpenHABTypes();
}
}