diff --git a/README.md b/README.md index f2ba1bc..9cafed2 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Purpose ==== -This release, as of 27/09/2015, is the beginnings of this work. +This is an early release, and is the beginnings of this work. It is not representative of the final API, but it is a starting point for me to start experimenting with ideas and testing out reliability, with a view to using these products to integrate into an Internet of Things solution provided by @@ -68,10 +68,8 @@ ``` After a few seconds, you should see some packet dumps appearing on the screen. -The last few bytes will be 0x73 0x01 0x01 or 0x73 0x01 0x00 and these indicate -the switch state of the plug. Press the button on the front of the plug to -turn the switch on and off, and you should see the 0x01 change to 0x00 and -back again. +These packets are then decoded and displayed in a dictionary format, +and for certain messages, also in a more friendly format. If it crashes, it sometimes leaves the radio in an indeterminite state, remove and replace the radio board and it should reset it (but see notes below about this). @@ -84,28 +82,10 @@ state and I have to remove the board to reset the radio. There might be a RESET line or a RESET command that can be sent at startup to solve this. -2. Write an OpenHEMS decoder to decode the messages for friendly display. I will -probably decode the hex buffer into a pydict, and then write a pydict to text -formatter. This will expose the whole of OpenHEMS in a really nice Python structure -to improve further innovation within Python. - -3. Write an OpenHEMS encoder to encode friendly messages. I will probably -take a pydict with header and records in it and encode into a buffer that is -then transmitted via the radio interface. As above, this will expose message -creation in a really nice Python structure to improve further innovation within -Python. - -4. Construct commands for switch-on and switch-off, and test sending these to a +2. Construct commands for switch-on and switch-off, and test sending these to a specific sensorid. -5. Write a discovery service that sends a monitor command, then collects all -the receive messages and builds an internal dictionary of devices that respond. -I will probably at this point build a Python object for each device that responds, -and this object will be a proxy that can be used to monitor and control that device, -thus allowing any number of devices to be monitored and controlled in a 'Pythonic' -way. - -6. Push a fair amount of the radio interface and some of OpenHEMS back down into +3. Push a fair amount of the radio interface and some of OpenHEMS back down into a C library that implements the same interface as what we have at this point in the Python. Write a ctypes wrapper around this, so that the identical Python internal API is presented. The idea being that the first pass of Python coding defines the diff --git a/src/energenie/Devices.py b/src/energenie/Devices.py new file mode 100644 index 0000000..a1570d1 --- /dev/null +++ b/src/energenie/Devices.py @@ -0,0 +1,33 @@ +# Devices.py 30/09/2015 D.J.Whale +# +# Information about specific Energenie devices + +MFRID = 0x04 +PRODUCTID_C1_MONITOR = 0x01 +PRODUCTID_R1_MONITOR_AND_CONTROL = 0x02 +CRYPT_PID = 242 +CRYPT_PIP = 0x0100 + +# OpenHEMS does not support a broadcast id, but Energine added one for their +# MiHome Adaptors. This makes simple discovery possible. +BROADCAST_ID = 0xFFFFFF # energenie broadcast + +# TODO put additional products in here from the Energenie directory + +def getDescription(mfrid, productid): + if mfrid == MFRID: + mfr = "Energenie" + if productid == PRODUCTID_C1_MONITOR: + product = "C1 MONITOR" + elif productid == PRODUCTID_R1_MONITOR_AND_CONTROL: + product = "R1 MONITOR/CONTROL" + else: + product = "UNKNOWN" + else: + mfr = "UNKNOWN" + product = "UNKNOWN" + + return "Manufactuer:%s Product:%s" % (mfr, product) + + +# END diff --git a/src/energenie/OpenHEMS.py b/src/energenie/OpenHEMS.py index 65695f2..8826701 100644 --- a/src/energenie/OpenHEMS.py +++ b/src/energenie/OpenHEMS.py @@ -158,7 +158,7 @@ # [0]len,mfrid,productid,pipH,pipL,[5] crypto.init(crypt_pid, encryptPIP) crypto.cryptPayload(payload, 5, len(payload)-5) # including CRC - + printhex(payload) # sensorId is in encrypted region sensorId = (payload[5]<<16) + (payload[6]<<8) + payload[7] header["sensorid"] = sensorId @@ -200,26 +200,27 @@ plen = payload[i] & 0x0F i += 1 - if plen == 0: continue # no more data in this record - - # VALUE - valuebytes = [] - for x in range(plen): - valuebytes.append(payload[i]) - i += 1 - value = Value.decode(valuebytes, typeid, plen) - - # store rec - recs.append({ + rec = { "wr": wr, "paramid": paramid, "paramname": paramname, "paramunit": paramunit, "typeid": typeid, - "length": plen, - "valuebytes": valuebytes, - "value": value - }) + "length": plen + } + + if plen != 0: + # VALUE + valuebytes = [] + for x in range(plen): + valuebytes.append(payload[i]) + i += 1 + value = Value.decode(valuebytes, typeid, plen) + rec["valuebytes"] = valuebytes + rec["value"] = value + + # store rec + recs.append(rec) return { "type": "OK", @@ -248,17 +249,14 @@ payload.append(header["mfrid"]) payload.append(header["productid"]) - if encrypt: - if not header.has_key("encryptPIP"): + if not header.has_key("encryptPIP"): + if encrypt: warning("no encryptPIP in header, assuming 0x0100") - encryptPIP = 0x0100 - else: - encryptPIP = header["encryptPIP"] - payload.append((encryptPIP&0xFF00)>>8) # MSB - payload.append((encryptPIP&0xFF)) # LSB + encryptPIP = 0x0100 else: - payload.append(0) - payload.append(0) + encryptPIP = header["encryptPIP"] + payload.append((encryptPIP&0xFF00)>>8) # MSB + payload.append((encryptPIP&0xFF)) # LSB sensorId = header["sensorid"] payload.append((sensorId>>16) & 0xFF) # HIGH @@ -270,8 +268,10 @@ wr = rec["wr"] paramid = rec["paramid"] typeid = rec["typeid"] - length = rec["length"] - value = rec["value"] + if rec.has_key("length"): + length = rec["length"] + else: + length = None # auto detect # PARAMID if wr: @@ -280,12 +280,19 @@ payload.append(paramid) # READ # TYPE/LENGTH - payload.append((typeid<<4) | length) + payload.append((typeid)) # need to back patch length for auto detect + lenpos = len(payload)-1 # for backpatch # VALUE - valueenc = Value.encode(value, typeid, length) - for b in valueenc: - payload.append(b) + valueenc = [] # in case of no value + if rec.has_key("value"): + value = rec["value"] + valueenc = Value.encode(value, typeid, length) + if len(valueenc) > 15: + raise ValueError("value longer than 15 bytes") + for b in valueenc: + payload.append(b) + payload[lenpos] = (typeid) | len(valueenc) # FOOTER payload.append(0) # NUL @@ -324,8 +331,162 @@ FLOAT = 0xF0 @staticmethod - def encode(value, typeid, length): - return [int(value) for i in range(length)] # TODO + def typebits(typeid): + """work out number of bits to represent this type""" + if typeid == Value.UINT_BP4: return 4 + if typeid == Value.UINT_BP8: return 8 + if typeid == Value.UINT_BP12: return 12 + if typeid == Value.UINT_BP16: return 16 + if typeid == Value.UINT_BP20: return 20 + if typeid == Value.UINT_BP24: return 24 + if typeid == Value.SINT_BP8: return 8 + if typeid == Value.SINT_BP16: return 16 + if typeid == Value.SINT_BP24: return 24 + raise ValueError("Can't calculate number of bits for type:" + str(typeid)) + + + @staticmethod + def highestClearBit(value, maxbits=15*8): + """Find the highest clear bit scanning MSB to LSB""" + mask = 1<<(maxbits-1) + bitno = maxbits-1 + while mask != 0: + #print("compare %s with %s" %(hex(value), hex(mask))) + if (value & mask) == 0: + #print("zero at bit %d" % bitno) + return bitno + mask >>= 1 + bitno-=1 + #print("not found") + return None # NOT FOUND + + + @staticmethod + def valuebits(value): + """Work out number of bits required to represent this value""" + if value >= 0 or type(value) != int: + raise RuntimeError("valuebits only on -ve int at moment") + + if value == -1: # always 0xFF, so always needs exactly 2 bits to represent (sign and value) + return 2 # bits required + #print("valuebits of:%d" % value) + # Turn into a 2's complement representation + MAXBYTES=15 + MAXBITS = 1<<(MAXBYTES*8) + #TODO check for truncation? + value = value & MAXBITS-1 + #print("hex:%s" % hex(value)) + highz = Value.highestClearBit(value, MAXBYTES*8) + #print("highz at bit:%d" % highz) + # allow for a sign bit, and bit numbering from zero + neededbits = highz+2 + + #print("needed bits:%d" % neededbits) + return neededbits + + + @staticmethod + def encode(value, typeid, length=None): + #print("encoding:" + str(value)) + if typeid == Value.CHAR: + if type(value) != str: + value = str(value) + if length != None and len(str) > length: + raise ValueError("String too long") + result = [] + for ch in value: + result.append(ord(ch)) + if len != None and len(result) < length: + for a in range(length-len(result)): + result.append(0) # zero pad + return result + + if typeid == Value.FLOAT: + raise ValueError("IEEE-FLOAT not yet supported") + + if typeid <= Value.UINT_BP24: + # unsigned integer + if value < 0: + raise ValueError("Cannot encode negative number as an unsigned int") + + if typeid != Value.UINT: + # pre-adjust for BP + if type(value) == float: + value *= (2**Value.typebits(typeid)) # shifts float into int range using BP + value = round(value, 0) # take off any unstorable bits + value = int(value) # It must be an integer for the next part of encoding + + # code it in the minimum length bytes required + # Note that this codes zero in 0 bytes (might not be correct?) + v = value + result = [] + while v != 0: + result.insert(0, v&0xFF) # MSB first, so reverse bytes as inserting + v >>= 8 + + # check length mismatch and zero left pad if required + if length != None: + if len(result) < length: + result = [0 for x in range(length-len(result))] + result + elif len(result) > length: + raise ValueError("Field width overflow, not enough bits") + return result + + + if typeid >= Value.SINT and typeid <= Value.SINT_BP24: + # signed int + if typeid != Value.SINT: + # pre-adjust for BP + if type(value) == float: + value *= (2**Value.typebits(typeid)) # shifts float into int range using BP + value = round(value, 0) # take off any unstorable bits + value = int(value) # It must be an integer for the next part of encoding + + #If negative, take complement by masking with the length mask + # This turns -1 (8bit) into 0xFF, which is correct + # -1 (16bit) into 0xFFFF, which is correct + # -128(8bit) into 0x80, which is correct + #i.e. top bit will always be set as will all following bits up to number + + if value < 0: # -ve + if typeid == Value.SINT: + bits = Value.valuebits(value) + else: + bits = Value.typebits(typeid) + #print("need bits:" + str(bits)) + # NORMALISE BITS TO BYTES + ####HERE#### round up to nearest number of 8 bits + # if already 8, leave 1,2,3,4,5,6,7,8 = 8 0,1,2,3,4,5,6,7 (((b-1)/8)+1)*8 + # 9,10,11,12,13,14,15,16=16 + bits = (((bits-1)/8)+1)*8 # snap to nearest byte boundary + #print("snap bits to 8:" + str(bits)) + + value &= ((2**bits)-1) + neg = True + else: + neg = False + + #encode in minimum bytes possible + v = value + result = [] + while v != 0: + result.insert(0, v&0xFF) # MSB first, so reverse when inserting + v >>= 8 + + # if desired length mismatch, zero pad or sign extend to fit + if length != None: # fixed size + if len(result) < length: # pad + if not neg: + result = [0 for x in range(length-len(result))] + result + else: # negative + result = [0xFF for x in range(length-len(result))] + result + elif len(result) >length: # overflow + raise ValueError("Field width overflow, not enough bits") + + return result + + raise ValueError("Unknown typeid:%d" % typeid) + @staticmethod def decode(valuebytes, typeid, length): @@ -338,18 +499,7 @@ # process any fixed binary points if typeid == Value.UINT: return result # no BP adjustment - if typeid == Value.UINT_BP4: - return (float(result))/(2**4) # 4 bits - if typeid == Value.UINT_BP8: - return (float(result))/(2**8) # 8 bits - if typeid == Value.UINT_BP12: - return (float(result))/(2**12) # 12 bits - if typeid == Value.UINT_BP16: - return (float(result))/(2**16) # 16 bits - if typeid == Value.UINT_BP20: - return (float(result))/(2**20) # 20 bits - if typeid == Value.UINT_BP24: - return (float(result))/(2**24) # 24 bits + return (float(result)) / (2**Value.typebits(typeid)) elif typeid == Value.CHAR: result = "" @@ -371,16 +521,12 @@ onescomp = (~result) & ((2**(length*8))-1) result = -(onescomp + 1) - # adjust binary point + # adjust for binary point if typeid == Value.SINT: - return result # no BP - elif typeid == Value.SINT_BP8: - return (float(result))/(2**8) # 8 bits - elif typeid == Value.SINT_BP16: - return (float(result))/(2**16) # 16 bits - elif typeid == Value.SINT_BP24: - return (float(result))/(2**24) # 24 bits - return result + return result # no BP, return as int + else: + # There is a BP, return as float + return (float(result))/(2**Value.typebits(typeid)) elif typeid == Value.FLOAT: return "TODO_FLOAT_IEEE_754-2008" #TODO: IEEE 754-2008 @@ -420,6 +566,57 @@ return rem +def showMessage(msg): + """Show the message in a friendly format""" + pprint.pprint(msg) + + # HEADER + header = msg["header"] + mfrid = header["mfrid"] + productid = header["productid"] + sensorid = header["sensorid"] + print("mfrid:%s prodid:%s sensorid:%s" % (hex(mfrid), hex(productid), hex(sensorid))) + + # RECORDS + for rec in msg["recs"]: + wr = rec["wr"] + if wr == True: + write = "write" + else: + write = "read " + + paramid = rec["paramid"] + paramname = rec["paramname"] + paramunit = rec["paramunit"] + if rec.has_key("value"): + value = rec["value"] + else: + value = None + print("%s %s %s = %s" % (write, paramname, paramunit, str(value))) + + +def alterMessage(message, **kwargs): + """Change parameters in-place in a message template""" + # e.g. header_sensorid=1234, recs_0_value=1 + for arg in kwargs: + + path = arg.split("_") + value = kwargs[arg] + + m = message + for p in path[:-1]: + try: + p = int(p) + except: + pass + m = m[p] + #print("old value:%s" % m[path[-1]]) + m[path[-1]] = value + + #print("modified:" + str(message)) + + return message + #----- TEST HARNESS ----------------------------------------------------------- def printhex(payload): @@ -430,40 +627,124 @@ print line -if __name__ == "__main__": - TEST_PAYLOAD = [ - 0x1C, #len 16 + 10 + 2 = 0001 1100 - 0x04, #mfrid - 0x02, #prodid - 0x01, #pipmsb - 0x00, #piplsb - 0x00, 0x06, 0x8B, #sensorid - 0x70, 0x82, 0x00, 0x07, #power - 0x71, 0x82, 0xFF, 0xFD, #reactive_power - 0x76, 0x01, 0xF0, #voltage - 0x66, 0x22, 0x31, 0xDA, #freq - 0x73, 0x01, 0x01, #switch_state - 0x00, #NUL - 0x97, 0x64 #CRC +TEST_PAYLOAD = [ + 0x1C, #len 16 + 10 + 2 = 0001 1100 + 0x04, #mfrid + 0x02, #prodid + 0x01, #pipmsb + 0x00, #piplsb + 0x00, 0x06, 0x8B, #sensorid + 0x70, 0x82, 0x00, 0x07, #SINT(2) power + 0x71, 0x82, 0xFF, 0xFD, #SINT(2) reactive_power + 0x76, 0x01, 0xF0, #UINT(1) voltage + 0x66, 0x22, 0x31, 0xDA, #UINT_BP8(2) freq + 0x73, 0x01, 0x01, #UINT(1) switch_state + 0x00, #NUL + 0x97, 0x64 #CRC - ] +] - import pprint +import pprint + + +def test_payload_unencrypted(): init(242) - print("RAW PAYLOAD, UNENCRYPTED") printhex(TEST_PAYLOAD) - spec = decode(TEST_PAYLOAD, decrypt=False) - print("DECODED") + pprint.pprint(spec) + + payload = encode(spec, encrypt=False) + printhex(payload) + + spec2 = decode(payload, decrypt=False) + pprint.pprint(spec2) + + payload2 = encode(spec2, encrypt=False) + + printhex(TEST_PAYLOAD) + printhex(payload2) + + if TEST_PAYLOAD != payload: + print("FAILED") + else: + print("PASSED") + + +def test_payload_encrypted(): + init(242) + + printhex(TEST_PAYLOAD) + spec = decode(TEST_PAYLOAD, decrypt=False) pprint.pprint(spec) payload = encode(spec, encrypt=True) - print("ENCODED ENCRYTED") printhex(payload) spec2 = decode(payload, decrypt=True) - print("DECODED, DECRYPTED") pprint.pprint(spec2) + payload2 = encode(spec2, encrypt=False) + + printhex(TEST_PAYLOAD) + printhex(payload2) + + if TEST_PAYLOAD != payload: + print("FAILED") + else: + print("PASSED") + + +def test_value_encoder(): + pass + # test cases (auto, forced, overflow, -min, -min-1, 0, 1, +max, +max+1 + # UINT + # UINT_BP4 + # UINT_BP8 + # UINT_BP12 + # UINT_BP16 + # UINT_BP20 + # UINT_BP24 + # SINT + # SINT(2) + vin = [1,255,256,32767,32768,0,-1,-2,-3,-127,-128,-129,-32767,-32768] + for v in vin: + vout = Value.encode(v, Value.SINT) + print("encode " + str(v) + " " + str(vout)) + # SINT_BP8 + # SINT_BP16 + # SINT_BP24 + # CHAR + # FLOAT + + +def test_value_decoder(): + pass + # test cases (auto, forced, overflow, -min, -min-1, 0, 1, +max, +max+1 + # UINT + # UINT_BP4 + # UINT_BP8 + # UINT_BP12 + # UINT_BP16 + # UINT_BP20 + # UINT_BP24 + # SINT + vin = [255, 253] + print("input value:" + str(vin)) + vout = Value.decode(vin, Value.SINT, 2) + print("encoded as:" + str(vout)) + + # SINT_BP8 + # SINT_BP16 + # SINT_BP24 + # CHAR + # FLOAT + + +if __name__ == "__main__": + #test_value_encoder() + #test_value_decoder() + test_payload_unencrypted() + #test_payload_encrypted() + # END diff --git a/src/energenie/radio.py b/src/energenie/radio.py index 7c768cd..fdc0b7e 100644 --- a/src/energenie/radio.py +++ b/src/energenie/radio.py @@ -45,6 +45,13 @@ spi.deselect() +def ashex(buf): + result = [] + for b in buf: + result.append(hex(b)) + return result + + def HRF_readfifo_burst(): """Read bytes from the payload FIFO using burst read""" #first byte read is the length in remaining bytes @@ -61,7 +68,7 @@ count -= 1 buf.append(data) spi.deselect() - trace("readfifo:" + str(buf)) + trace("readfifo:" + str(ashex(buf))) return buf diff --git a/src/monitor.py b/src/monitor.py index b23b3b0..bc74cbd 100644 --- a/src/monitor.py +++ b/src/monitor.py @@ -3,117 +3,123 @@ # Monitor settings of Energine MiHome plugs import time -import pprint -from energenie import OpenHEMS, radio + +from energenie import OpenHEMS, Devices +from energenie import radio +from Timer import Timer def trace(msg): print(str(msg)) -#----- TIMER ------------------------------------------------------------------ - -class Timer(): - def __init__(self, ratesec=1): - self.rate = ratesec - self.nexttick = time.time() - - - def check(self): - """Maintain the timer and see if it is time for the next tick""" - now = time.time() - - if now >= self.nexttick: - # asynchronous tick, might drift, but won't stack up if late - self.nexttick = now + self.rate - return True - - return False - - #----- TEST APPLICATION ------------------------------------------------------- -class ENERGENIE(): - MFRID = 0x04 - PRODUCTID_C1_MONITOR = 0x01 - PRODUCTID_R1_MONITOR_AND_CONTROL = 0x02 - CRYPT_PID = 242 - CRYPT_PIP = 0x0100 +directory = {} + +def allkeys(d): + result = "" + for k in d: + if len(result) != 0: + result += ',' + result += str(k) + return result + + +def updateDirectory(message): + """Update the local directory with information about this device""" + now = time.time() + header = message["header"] + sensorId = header["sensorid"] + + if not directory.has_key(sensorId): + # new device discovered + desc = Devices.getDescription(header["mfrid"], header["productid"]) + print("ADD device:%s %s" % (hex(sensorId), desc)) + directory[sensorId] = {"header": message["header"]} + print(allkeys(directory)) + + directory[sensorId]["time"] = now + #TODO would be good to keep recs, but need to iterate through all and key by paramid, + #not as a list index, else merging will be hard. -MONITOR_MESSAGE = { +SWITCH_MESSAGE = { "header": { - "mfrid": ENERGENIE.MFRID, - "productid": ENERGENIE.PRODUCTID_C1_MONITOR, - "encryptPIP": ENERGENIE.CRYPT_PIP, - "sensorid": 0xFFFFFF # energenie broadcast + "mfrid": Devices.MFRID, + "productid": Devices.PRODUCTID_R1_MONITOR_AND_CONTROL, + "encryptPIP": Devices.CRYPT_PIP, + "sensorid": 0 # FILL IN }, "recs": [ { - "wr": True, # monitor only will ignore this + "wr": True, "paramid": OpenHEMS.PARAM_SWITCH_STATE, "typeid": OpenHEMS.Value.UINT, "length": 1, - "value": 0 + "value": 0 # FILL IN } ] } -def showMessage(msg): - """Show the message in a friendly format""" - #pprint.pprint(msg) +JOIN_ACK_MESSAGE = { + "header": { + "mfrid": 0, # FILL IN + "productid": 0, # FILL IN + "encryptPIP": Devices.CRYPT_PIP, + "sensorid": 0 # FILL IN + }, + "recs": [ + { + "wr": False, + "paramid": OpenHEMS.PARAM_JOIN, + "typeid": OpenHEMS.Value.UINT, + "length": 0 + } + ] +} - # HEADER - header = msg["header"] - mfrid = header["mfrid"] - productid = header["productid"] - sensorid = header["sensorid"] - print("mfrid:%s prodid:%s sensorid:%s" % (hex(mfrid), hex(productid), hex(sensorid))) - - # RECORDS - for rec in msg["recs"]: - wr = rec["wr"] - if wr == True: - write = "write" - else: - write = "read " - - paramid = rec["paramid"] - paramname = rec["paramname"] - paramunit = rec["paramunit"] - value = rec["value"] - print("%s %s %s = %s" % (write, paramname, paramunit, str(value))) def monitor(): - """Send monitor poke messages and capture any responses""" + """Send discovery and monitor messages, and capture any responses""" - sendMonitorTimer = Timer(9) # every 9 secs - #pollReceiveTimer = Timer(0.1) # 10 times per sec + # Define the schedule of message polling + sendSwitchTimer = Timer(5, 1) # every 5 seconds offset by initial 1 + switch_state = 0 # OFF radio.receiver() while True: # See if there is a payload, and if there is, process it - #if pollReceiveTimer.check(): if radio.isReceiveWaiting(): trace("receiving payload") payload = radio.receive() - decoded = OpenHEMS.decode(payload) - showMessage(decoded) + try: + decoded = OpenHEMS.decode(payload) + except OpenHEMS.OpenHEMSException as e: + print("Can't decode payload:" + str(e)) + continue + + OpenHEMS.showMessage(decoded) + updateDirectory(decoded) - # If it is time to send a monitor message, send it - if sendMonitorTimer.check(): - trace("sending monitor message") - payload = OpenHEMS.encode(MONITOR_MESSAGE) - radio.transmitter() - radio.transmit(payload) - radio.receiver() # Keep in receiver mode as much as possible - + # assume only 1 rec in a join, for now + if decoded["recs"][0]["paramid"] == OpenHEMS.PARAM_JOIN: + #TODO: write OpenHEMS.getFromMessage("header_mfrid") + response = OpenHEMS.alterMessage(JOIN_ACK_MESSAGE, + header_mfrid=decoded["header"]["mfrid"], + header_productid=decoded["header"]["productid"], + header_sensorid=decoded["header"]["sensorid"]) + p = OpenHEMS.encode(response) + radio.transmitter() + radio.transmit(p) + radio.receiver() + if __name__ == "__main__": radio.init() - OpenHEMS.init(ENERGENIE.CRYPT_PID) + OpenHEMS.init(Devices.CRYPT_PID) try: monitor()