Newer
Older
pyenergenie / src / energenie / OpenHEMS.py
# 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