diff --git a/src/energenie/radio.py b/src/energenie/radio.py index 62444bb..2420d0d 100644 --- a/src/energenie/radio.py +++ b/src/energenie/radio.py @@ -201,6 +201,30 @@ [ADDR_OPMODE, MODE_RECEIVER] # Operating mode to Receiver ] +config_OOK = [ + [ADDR_REGDATAMODUL, VAL_REGDATAMODUL_OOK], # modulation scheme OOK + [ADDR_FDEVMSB, 0], # frequency deviation -> 0kHz + [ADDR_FDEVLSB, 0], # frequency deviation -> 0kHz + [ADDR_FRMSB, VAL_FRMSB433], # carrier freq -> 433.92MHz 0x6C7AE1 + [ADDR_FRMID, VAL_FRMID433], # carrier freq -> 433.92MHz 0x6C7AE1 + [ADDR_FRLSB, VAL_FRLSB433], # carrier freq -> 433.92MHz 0x6C7AE1 + [ADDR_RXBW, VAL_RXBW120], # channel filter bandwidth 120kHz + [ADDR_BITRATEMSB, 0x40], # 1938b/s + [ADDR_BITRATELSB, 0x80], # 1938b/s + #[ADDR_BITRATEMSB, 0x1A], # 4800b/s + #[ADDR_BITRATELSB, 0x0B], # 4800b/s + [ADDR_PREAMBLELSB, 0], # preamble size LSB 3 + [ADDR_SYNCCONFIG, VAL_SYNCCONFIG4], # Size of the Synch word = 4 (SyncSize + 1) + [ADDR_SYNCVALUE1, VAL_SYNCVALUE1OOK], # sync value 1 + [ADDR_SYNCVALUE2, 0], # sync value 2 + [ADDR_SYNCVALUE3, 0], # sync value 3 + [ADDR_SYNCVALUE4, 0], # sync value 4 + [ADDR_PACKETCONFIG1, VAL_PACKETCONFIG1OOK], # Fixed length, no Manchester coding, OOK + [ADDR_PAYLOADLEN, VAL_PAYLOADLEN_OOK], # Payload Length + [ADDR_FIFOTHRESH, VAL_FIFOTHRESH30], # Condition to start packet transmission: wait for 30 bytes in FIFO + #[ADDR_OPMODE, MODE_TRANSMITER] # Transmitter mode +] + def HRF_wait_ready(): """Wait for HRF to be ready after last command""" @@ -221,6 +245,13 @@ HRF_wait_ready() +def HRF_config_OOK(): + """Configure HRF for OOK modulation""" + for cmd in config_OOK: + HRF_writereg(cmd[0], cmd[1]) + HRF_wait_ready() + + def HRF_change_mode(mode): HRF_writereg(ADDR_OPMODE, mode) @@ -257,6 +288,32 @@ if ((reg & MASK_FIFONOTEMPTY) != 0) or ((reg & MASK_FIFOOVERRUN) != 0): warning("Failed to send payload to HRF") + +def send_payload_repeat(payload, times=0): + """Send a payload multiple times""" + + # 32 bits enclosed in sync words + HRF_pollreg(ADDR_IRQFLAGS1, MASK_MODEREADY|MASK_TXREADY, MASK_MODEREADY|MASK_TXREADY) + + #write first payload without sync preamble + HRF_writefifo_burst(payload) + + # preceed all future payloads with a sync-word preamble + if times > 0: + preamble = [0x00,0x80,0x00,0x00,0x00] + preamble_payload = preamble + payload + for i in range(times): # Repeat the message a number of times + HRF_pollreg(ADDR_IRQFLAGS2, MASK_FIFOLEVEL, 0) + HRF_writefifo_burst(preamble_payload) + + HRF_pollreg(ADDR_IRQFLAGS2, MASK_PACKETSENT, MASK_PACKETSENT) # wait for Packet sent + + reg = HRF_readreg(ADDR_IRQFLAGS2) + trace(" irqflags2=%s" % hex(reg)) + if (reg & (MASK_FIFONOTEMPTY) != 0) or ((reg & MASK_FIFOOVERRUN) != 0): + warning("Failed to send repeated payload to HRF") + + def dumpPayloadAsHex(payload): length = payload[0] print(hex(length)) @@ -267,21 +324,65 @@ print("[%d] = %s" % (i, hex(payload[i]))) +def build_OOK_relay_msg(relayState=False): + """Temporary test code to prove we can turn the relay on or off""" + #This code does not live in this module, it lives in an EnergenieLegacy codec module + #also there are 4 switches, so should pass in up to 4 relayState values + # This generates a 20*4 bit address i.e. 10 bytes + # The number generated is always the same + # Presumably this is the 'Energenie address prefix' + # The switch number is encoded in the payload + # This code looks screwy, but it is correct compared to the C code. + # It's probably doing bit encoding on the fly, manchester or sync bits or something. + # Wait until we get the OOK spec from Energenie to better document this. + + # Looks like: 0000 00BA gets encoded as: + # 128 64 32 16 8 4 2 1 + # 1 0 B B 1 A A 0 + + #payload = [] + #for i in range(10): + # j = i + 5 + # payload.append(8 + (j&1) * 6 + 128 + (j&2) * 48) + #dumpPayloadAsHex(payload) + # + # 5 6 7 8 9 10 11 12 13 14 + # 1(01) 1(10) 1(11) 0(00) 0(01) 0(10) 0(11) 1(00) 1(01) 1(10) + payload = [0x8e, 0xe8, 0xee, 0x88, 0x8e, 0xe8, 0xee, 0x88, 0x8e, 0xe8] + + if relayState: # ON + # D0=high, D1=high, D2-high, D3=high (S1 on) + # 128 64 32 16 8 4 2 1 128 64 32 16 8 4 2 1 + # 1 0 B B 1 A A 0 1 0 B B 1 A A 0 + # 1 1 1 0 1 1 1 0 1 1 1 0 1 1 1 0 = 10 11 10 11 + payload += [0xEE, 0xEE] + else: # OFF + # D0=high, D1=high, D2=high, D3=low (S1 off) + # 128 64 32 16 8 4 2 1 128 64 32 16 8 4 2 1 + # 1 0 B B 1 A A 0 1 0 B B 1 A A 0 + # 1 1 1 0 1 1 1 0 1 1 1 0 1 0 0 0 = 10 11 10 00 + payload += [0xEE, 0xE8] + + return payload #----- USER API --------------------------------------------------------------- # # This is only a first-pass at a user API. # it might change quite a bit in the second pass. +# The HRF functions are intentionally not used by the caller, +# this allows mock testing, and also for that part to be rewritten in C +# for speed later if required. mode = None +modulation_fsk = None def init(): + """Initialise the module ready for use""" spi.init_defaults() - trace("######## RESET") + trace("RESET") spi.reset() # send a hardware reset to ensure radio in clean state - trace("######## END RESET") trace("config FSK") HRF_config_FSK() @@ -289,33 +390,62 @@ receiver() -def transmitter(): +def modulation(fsk=None, ook=None): + """Switch modulation, if needed""" + global modulation_fsk + + # Handle sensible module defaults for earlier versions of user code + if fsk == None and ook == None: + # Force FSK mode + fsk = True + + if fsk != None and fsk: + if modulation_fsk == None or modulation_fsk == False: + trace("switch to FSK") + HRF_config_FSK() + modulation_fsk = True + + elif ook != None and ook: + if modulation_fsk == None or modulation_fsk == True: + trace("switch to OOK") + HRF_config_OOK() + modulation_fsk = False + + +def transmitter(fsk=None, ook=None): """Change into transmitter mode""" global mode + trace("transmitter mode") + modulation(fsk, ook) HRF_change_mode(MODE_TRANSMITER) mode = "TRANSMITTER" HRF_wait_txready() def transmit(payload): + """Transmit a single payload using the present modulation scheme""" HRF_send_payload(payload) -def receiver(): +def receiver(fsk=None, ook=None): """Change into receiver mode""" global mode + trace("receiver mode") + modulation(fsk, ook) HRF_change_mode(MODE_RECEIVER) HRF_wait_ready() mode = "RECEIVER" def isReceiveWaiting(): + """Check to see if a payload is waiting in the receive buffer""" return HRF_check_payload() def receive(): + """Receive a single payload from the buffer using the present modulation scheme""" return HRF_receive_payload() @@ -324,5 +454,4 @@ spi.finished() - # END diff --git a/src/legacy.py b/src/legacy.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/legacy.py diff --git a/src/monitor.py b/src/monitor.py index c9c417e..a3089b7 100644 --- a/src/monitor.py +++ b/src/monitor.py @@ -1,6 +1,6 @@ # monitor.py 27/09/2015 D.J.Whale # -# Monitor settings of Energine MiHome plugs +# Monitor Energine MiHome plugs import time @@ -117,25 +117,6 @@ #not as a list index, else merging will be hard. -SWITCH_MESSAGE = { - "header": { - "mfrid": Devices.MFRID, - "productid": Devices.PRODUCTID_R1_MONITOR_AND_CONTROL, - "encryptPIP": Devices.CRYPT_PIP, - "sensorid": 0 # FILL IN - }, - "recs": [ - { - "wr": True, - "paramid": OpenHEMS.PARAM_SWITCH_STATE, - "typeid": OpenHEMS.Value.UINT, - "length": 1, - "value": 0 # FILL IN - } - ] -} - - JOIN_ACK_MESSAGE = { "header": { "mfrid": 0, # FILL IN @@ -153,16 +134,22 @@ ] } +def send_join_ack(mfrid, productid, sensorid): + # send back a JOIN ACK, so that join light stops flashing + response = OpenHEMS.alterMessage(JOIN_ACK_MESSAGE, + header_mfrid=mfrid, + header_productid=productid, + header_sensorid=sensorid) + p = OpenHEMS.encode(response) + radio.transmitter() + radio.transmit(p) + radio.receiver() def monitor(): - """Send discovery and monitor messages, and capture any responses""" + """Capture any incoming messages and log to CSV file""" - # Define the schedule of message polling - sendSwitchTimer = Timer(5, 1) # every n seconds offset by initial 1 - switch_state = 0 # OFF radio.receiver() - decoded = None while True: # See if there is a payload, and if there is, process it @@ -178,40 +165,22 @@ OpenHEMS.showMessage(decoded) updateDirectory(decoded) logMessage(decoded) - - #TODO: Should remember report time of each device, - #and reschedule command messages to avoid their transmit slot - #making it less likely to miss an incoming message due to - #the radio being in transmit mode - # handle messages with zero recs in them silently #trace(decoded) if len(decoded["recs"]) == 0: + # handle messages with zero recs in them silently print("Empty record:%s" % decoded) else: # assume only 1 rec in a join, for now + #TODO: write OpenHEMS.getFromMessage("header_mfrid") if decoded["recs"][0]["paramid"] == OpenHEMS.PARAM_JOIN: - #TODO: write OpenHEMS.getFromMessage("header_mfrid") - # send back a JOIN ACK, so that join light stops flashing - 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() + header = decoded["header"] + mfrid = header["mfrid"] + productid = header["productid"] + sensorid = header["sensorid"] + send_join_ack(mfrid, productid, sensorid) - if sendSwitchTimer.check() and decoded != None: - request = OpenHEMS.alterMessage(SWITCH_MESSAGE, - header_sensorid=decoded["header"]["sensorid"], - recs_0_value=switch_state) - p = OpenHEMS.encode(request) - radio.transmitter() - radio.transmit(p) - radio.receiver() - switch_state = (switch_state+1) % 2 # toggle - + if __name__ == "__main__": diff --git a/src/switch.py b/src/switch.py new file mode 100644 index 0000000..c9c417e --- /dev/null +++ b/src/switch.py @@ -0,0 +1,228 @@ +# monitor.py 27/09/2015 D.J.Whale +# +# Monitor settings of Energine MiHome plugs + +import time + +from energenie import OpenHEMS, Devices +from energenie import radio +from Timer import Timer +import os + +LOG_FILENAME = "energenie.csv" + +def warning(msg): + print("warning:%s" % str(msg)) +def trace(msg): + print("monitor:%s" % str(msg)) + +log_file = None + +def logMessage (msg): + HEADINGS = 'timestamp,mfrid,prodid,sensorid,flags,switch,voltage,freq,reactive,real' + + global log_file + if log_file == None: + if not os.path.isfile(LOG_FILENAME): + log_file = open(LOG_FILENAME, 'w') + log_file.write(HEADINGS + '\n') + else: + log_file = open(LOG_FILENAME, 'a') # append + + # get the header + header = msg['header'] + timestamp = time.time() + mfrid = header['mfrid'] + productid = header['productid'] + sensorid = header['sensorid'] + + # set defaults for any data that doesn't appear in this message + # but build flags so we know which ones this contains + flags = [0 for i in range(7)] + switch = None + voltage = None + freq = None + reactive = None + real = None + apparent = None + current = None + + # capture any data that we want + #print(msg) + for rec in msg['recs']: + paramid = rec['paramid'] + try: + value = rec['value'] + except: + value = None + + if paramid == OpenHEMS.PARAM_SWITCH_STATE: + switch = value + flags[0] = 1 + elif paramid == OpenHEMS.PARAM_VOLTAGE: + flags[1] = 1 + voltage = value + elif paramid == OpenHEMS.PARAM_FREQUENCY: + flags[2] = 1 + freq = value + elif paramid == OpenHEMS.PARAM_REACTIVE_POWER: + flags[3] = 1 + reactive = value + elif paramid == OpenHEMS.PARAM_REAL_POWER: + flags[4] = 1 + real = value + elif paramid == OpenHEMS.PARAM_APPARENT_POWER: + flags[5] = 1 + apparent = value + elif paramid == OpenHEMS.PARAM_CURRENT: + flags[6] = 1 + current = value + + # generate a line of CSV + flags = "".join([str(a) for a in flags]) + csv = "%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s" % (timestamp, mfrid, productid, sensorid, flags, switch, voltage, freq, reactive, real, apparent, current) + log_file.write(csv + '\n') + log_file.flush() + trace(csv) # testing + + +#----- TEST APPLICATION ------------------------------------------------------- + +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"]} + #trace(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. + + +SWITCH_MESSAGE = { + "header": { + "mfrid": Devices.MFRID, + "productid": Devices.PRODUCTID_R1_MONITOR_AND_CONTROL, + "encryptPIP": Devices.CRYPT_PIP, + "sensorid": 0 # FILL IN + }, + "recs": [ + { + "wr": True, + "paramid": OpenHEMS.PARAM_SWITCH_STATE, + "typeid": OpenHEMS.Value.UINT, + "length": 1, + "value": 0 # FILL IN + } + ] +} + + +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 + } + ] +} + + + +def monitor(): + """Send discovery and monitor messages, and capture any responses""" + + # Define the schedule of message polling + sendSwitchTimer = Timer(5, 1) # every n seconds offset by initial 1 + switch_state = 0 # OFF + radio.receiver() + decoded = None + + while True: + # See if there is a payload, and if there is, process it + if radio.isReceiveWaiting(): + #trace("receiving payload") + payload = radio.receive() + try: + decoded = OpenHEMS.decode(payload) + except OpenHEMS.OpenHEMSException as e: + warning("Can't decode payload:" + str(e)) + continue + + OpenHEMS.showMessage(decoded) + updateDirectory(decoded) + logMessage(decoded) + + #TODO: Should remember report time of each device, + #and reschedule command messages to avoid their transmit slot + #making it less likely to miss an incoming message due to + #the radio being in transmit mode + + # handle messages with zero recs in them silently + #trace(decoded) + if len(decoded["recs"]) == 0: + print("Empty record:%s" % decoded) + else: + # assume only 1 rec in a join, for now + if decoded["recs"][0]["paramid"] == OpenHEMS.PARAM_JOIN: + #TODO: write OpenHEMS.getFromMessage("header_mfrid") + # send back a JOIN ACK, so that join light stops flashing + 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 sendSwitchTimer.check() and decoded != None: + request = OpenHEMS.alterMessage(SWITCH_MESSAGE, + header_sensorid=decoded["header"]["sensorid"], + recs_0_value=switch_state) + p = OpenHEMS.encode(request) + radio.transmitter() + radio.transmit(p) + radio.receiver() + switch_state = (switch_state+1) % 2 # toggle + + +if __name__ == "__main__": + + trace("starting monitor") + radio.init() + OpenHEMS.init(Devices.CRYPT_PID) + + try: + monitor() + + finally: + radio.finished() + +# END