# OpenHEMS.py 27/09/2015 D.J.Whale
#
# Implement OpenHEMS message encoding and decoding
import crypto
# report has bit 7 clear
# command has bit 7 set
PARAM_ALARM = 0x21
PARAM_DEBUG_OUTPUT = 0x2D
PARAM_IDENTIFY = 0x3F
PARAM_SOURCE_SELECTOR = 0x40 # command only
PARAM_WATER_DETECTOR = 0x41
PARAM_GLASS_BREAKAGE = 0x42
PARAM_CLOSURES = 0x43
PARAM_DOOR_BELL = 0x44
PARAM_ENERGY = 0x45
PARAM_FALL_SENSOR = 0x46
PARAM_GAS_VOLUME = 0x47
PARAM_AIR_PRESSURE = 0x48
PARAM_ILLUMINANCE = 0x49
PARAM_LEVEL = 0x4C
PARAM_RAINFALL = 0x4D
PARAM_APPARENT_POWER = 0x50
PARAM_POWER_FACTOR = 0x51
PARAM_REPORT_PERIOD = 0x52
PARAM_SMOKE_DETECTOR = 0x53
PARAM_TIME_AND_DATE = 0x54
PARAM_VIBRATION = 0x56
PARAM_WATER_VOLUME = 0x57
PARAM_WIND_SPEED = 0x58
PARAM_GAS_PRESSURE = 0x61
PARAM_BATTERY_LEVEL = 0x62
PARAM_CO_DETECTOR = 0x63
PARAM_DOOR_SENSOR = 0x64
PARAM_EMERGENCY = 0x65
PARAM_FREQUENCY = 0x66
PARAM_GAS_FLOW_RATE = 0x67
PARAM_CURRENT = 0x69
PARAM_JOIN = 0x6A
PARAM_LIGHT_LEVEL = 0x6C
PARAM_MOTION_DETECTOR = 0x6D
PARAM_OCCUPANCY = 0x6F
PARAM_REAL_POWER = 0x70
PARAM_REACTIVE_POWER = 0x71
PARAM_ROTATION_SPEED = 0x72
PARAM_SWITCH_STATE = 0x73
PARAM_TEMPERATURE = 0x74
PARAM_VOLTAGE = 0x76
PARAM_WATER_FLOW_RATE = 0x77
PARAM_WATER_PRESSURE = 0x78
PARAM_TEST = 0xAA
crypt_pid = None
crypt_pip = None
def init(pid, pip):
global crypt_pid, crypt_pip
crypt_pid = pid
crypt_pip = pip
def warning(msg):
print("warning:" + str(msg))
#TODO decode OpenHEMS message payload structure
#TODO decrypt OpenHEMS message payload
#TODO check the CRC is correct
"""
case S_MSGLEN: // Read message length
case S_MANUFACT_ID: // Read manufacturer identifier
case S_PRODUCT_ID: // Read product identifier
case S_ENCRYPTPIP: // Read encryption pip
case S_SENSORID: // Read sensor ID
/******************* start reading RECORDS ********************/
case S_DATA_PARAMID: // Read record parameter identifier
msgPtr->paramId = msgPtr->value & 0x7F;
temp = getIdName(msgPtr->paramId);
printf(" %s=", temp);
if (msgPtr->paramId == 0) // Parameter identifier CRC. Go to CRC
{
msgPtr->state = S_CRC;
msgPtr->recordBytesToRead = SIZE_CRC;
}
else
{
msgPtr->state = S_DATA_TYPEDESC;
msgPtr->recordBytesToRead = SIZE_DATA_TYPEDESC;
}
if (strcmp(temp, "Unknown") == 0) // Unknown parameter, finish fetching message
msgPtr->state = S_FINISH;
break;
case S_DATA_TYPEDESC: // Read record type description
if ((msgPtr->value & 0x0F) == 0) // No more data to read in that record
{
msgPtr->state = S_DATA_PARAMID;
msgPtr->recordBytesToRead = SIZE_DATA_PARAMID;
}
else
{
msgPtr->state = S_DATA_VAL;
msgPtr->recordBytesToRead = msgPtr->value & 0x0F;
}
msgPtr->type = msgPtr->value;
break;
case S_DATA_VAL: // Read record data
temp = getValString(msgPtr->value, msgPtr->type >> 4, msgPtr->recordBytesToRead);
printf("%s", temp);
msgPtr->state = S_DATA_PARAMID;
msgPtr->recordBytesToRead = SIZE_DATA_PARAMID;
if (strcmp(temp, "Reserved") == 0)
msgPtr->state = S_FINISH;
break;
/******************* finish reading RECORDS ********************/
case S_CRC: // Check CRC
msgPtr->state = S_FINISH;
if ((int16_t)msgPtr->value == crc(msgPtr->buf + NON_CRC, msgPtr->bufCnt - NON_CRC - SIZE_CRC))
{
printf("OK\n");
}
else
{
printf("FAIL expVal=%04x, pip=%04x, val=%04x\n", (int16_t)msgPtr->value, msgPtr->pip, crc(msgPtr->buf + NON_CRC, msgPtr->bufCnt - NON_CRC - SIZE_CRC));
}
break;
"""
#TODO if can't decode message throw an exception
def decode(payload):
buffer = ""
length = payload[0]
if length+1 != len(payload):
warning("rx payload length mismatch")
mfrId = payload[1]
productId = payload[2]
buffer += "len:" + str(length) + " "
buffer += "mfr:" + hex(mfrId) + " "
buffer += "prod:" + hex(productId) + " "
pip = (payload[3]<<8) + payload[4]
crypto.init(crypt_pid, pip)
crypto.cryptPayload(payload, 5, len(payload)-5)
for n in payload[5:]:
buffer += hex(n) + " "
#TODO check CRC matches
return buffer
#----- MESSAGE ENCODER --------------------------------------------------------
#
# Encodes a message using the OpenHEMS message payload structure
# R1 message product id 0x02 monitor and control (in switching program?)
# C1 message product id 0x01 monitor only (in listening program)
#TODO change this so it returns a pydict
#write an encoder that turns the pydict into a buffer for the radio
def make_monitor():
payload = [
7 + 3 + 3, # payload remaining length (header+records+footer)
0x04, # manufacturer id = Energenie
0x01, # product id = 0x01=C1(monitor) 0x02=R1(monitor+control)
0x01, # reserved1 (cryptSeedMSB)
0x00, # reserved2 (cryptSeedLSB)
# from here up until the NUL is crc'd
# from here up to and including the CRC is crypted
0xFF, # sensorIdHigh broadcast
0xFF, # sensorIdMid broadcast
0xFF, # sensorIdLow broadcast
# RECORDS
PARAM_SWITCH_STATE | 0x80, # set switch state
0x01, # type/length
0x00, # value off
0x00 # NUL
]
# Calculate and append the CRC bytes
crc = calcCRC(payload, 5, len(payload)-5)
payload.append((crc >> 8) & 0xFF) # MSB, big-endian
payload.append(crc & 0xFF) # LSB
crypto.init(crypt_pid, crypt_pip)
crypto.cryptPayload(payload, 5, len(payload)-5) # including CRC
return payload
#----- CRC CALCULATION --------------------------------------------------------
#int16_t crc(uint8_t const mes[], unsigned char siz)
#{
# uint16_t rem = 0;
# unsigned char byte, bit;
#
# for (byte = 0; byte < siz; ++byte)
# {
# rem ^= (mes[byte] << 8);
# for (bit = 8; bit > 0; --bit)
# {
# rem = ((rem & (1 << 15)) ? ((rem << 1) ^ 0x1021) : (rem << 1));
# }
# }
# return rem;
#}
def calcCRC(payload, start, length):
rem = 0
for b in payload[start:start+length]:
rem ^= (b<<8)
for bit in range(8):
if rem & (1<<15) != 0:
# bit is set
rem = ((rem<<1) ^ 0x1021) & 0xFFFF # always maintain U16
else:
# bit is clear
rem = (rem<<1) & 0xFFFF # always maintain U16
return rem
# END