diff --git a/src/energenie/Devices.py b/src/energenie/Devices.py index db3397e..a6beee3 100644 --- a/src/energenie/Devices.py +++ b/src/energenie/Devices.py @@ -3,15 +3,16 @@ # Information about specific Energenie devices # This table is mostly reverse-engineered from various websites and web catalogues. -#TODO: Might move this into an air_interface adaptor, so that TwoBit encodes -#are done consistently outside of this module, just like OpenThings encodes and decodes are. -#They are done externally, because you need the address before you can route it to these classes. -#import TwoBit +import OnAir + +# This level of indirection allows easy mocking for testing +ook_interface = OnAir.TwoBitAirInterface() +fsk_interface = OnAir.OpenThingsAirInterface() + MFRID_ENERGENIE = 0x04 MFRID = MFRID_ENERGENIE - #PRODUCTID_MIHO001 = # Home Hub #PRODUCTID_MIHO002 = # Control only (Uses Legacy OOK protocol) #PRODUCTID_MIHO003 = 0x0? # Hand Controller @@ -97,18 +98,16 @@ # this might be a real air_interface (a radio), or an adaptor interface # (a message scheduler with a queue). # -# TODO: As such, we need to handle: # synchronous send # synchronous receive -# asynchronous send (deferred) -# asynchronous receive (deferred) +# TODO: asynchronous send (deferred) - implies a callback on 'done, fail, timeout' +# TODO: asynchronous receive (deferred) - implies a callback on 'done, fail, timeout' # air_interface has: # configure(parameters) # send(payload) # send(payload, parameters) -# listen(parameters) -# check() -> payload or None +# receive() -> (radio_measurements, address, payload) #----- NEW DEVICE CLASSES ----------------------------------------------------- @@ -170,8 +169,8 @@ class LegacyDevice(EnergenieDevice): """An abstraction for Energenie green button legacy OOK devices""" - def __init__(self, air_interface): - EnergenieDevice.__init__(self, air_interface) + def __init__(self): + EnergenieDevice.__init__(self, ook_interface) self.config.frequency = 433.92 self.config.modulation = "OOK" self.config.codec = "4bit" @@ -199,8 +198,8 @@ class MiHomeDevice(EnergenieDevice): """An abstraction for Energenie new style MiHome FSK devices""" - def __init__(self, air_interface, device_id=None): - EnergenieDevice.__init__(self, air_interface, device_id) + def __init__(self, device_id=None): + EnergenieDevice.__init__(self, fsk_interface, device_id) self.config.frequency = 433.92 self.config.modulation = "FSK" self.config.codec = "OpenThings" @@ -261,8 +260,8 @@ class ENER002(LegacyDevice): """A green-button switch""" - def __init__(self, air_interface=None, device_id=None): - LegacyDevice.__init__(self, air_interface) + def __init__(self, device_id=None): + LegacyDevice.__init__(self) self.device_id = device_id # (house_address, device_index) self.config.tx_repeats = 8 self.capabilities.switch = True @@ -277,8 +276,8 @@ class MIHO005(MiHomeDevice): """An Energenie MiHome Adaptor Plus""" - def __init__(self, air_interface=None, device_id=None): - MiHomeDevice.__init__(self, air_interface) + def __init__(self, device_id=None): + MiHomeDevice.__init__(self) self.product_id = PRODUCTID_MIHO005 self.device_id = device_id class Readings(): @@ -354,8 +353,8 @@ class MIHO006(MiHomeDevice): """An Energenie MiHome Home Monitor""" - def __init__(self, air_interface=None, device_id=None): - MiHomeDevice.__init__(self, air_interface) + def __init__(self, device_id=None): + MiHomeDevice.__init__(self) self.product_id = PRODUCTID_MIHO006 self.device_id = device_id class Readings(): @@ -373,8 +372,8 @@ class MIHO013(MiHomeDevice): """An Energenie MiHome eTRV Radiator Valve""" - def __init__(self, air_interface=None, device_id=None): - MiHomeDevice.__init__(self, air_interface) + def __init__(self, device_id=None): + MiHomeDevice.__init__(self) self.product_id = PRODUCTID_MIHO013 self.device_id = device_id class Readings(): diff --git a/src/energenie/OnAir.py b/src/energenie/OnAir.py index 8e39284..00ca7e8 100644 --- a/src/energenie/OnAir.py +++ b/src/energenie/OnAir.py @@ -17,81 +17,146 @@ import TwoBit import radio +def log_method(m): + def inner(*args, **kwargs): + print("CALL %s with %s %s" % (m, args, kwargs)) + r = m(*args, **kwargs) + print("RETURN %s with %s" % (m, r)) + return r + return inner + class OpenThingsAirInterface(): def __init__(self): self.radio = radio # aids mocking later - #TODO: tx defaults - # FSK, inner_repeats, outer_delay, outer_repeats, power_level, frequency - #TODO: rx defaults - # FSK, poll_rate, timeout, frequency + class RadioDefaults(): + frequency = 433.92 + modulation = radio.RADIO_MODULATION_FSK + + class TxDefaults(RadioDefaults): + power_level = 0 + inner_repeats = 4 + outer_delay = 0 + outer_repeats = 0 + self.tx_defaults = TxDefaults() + + class RxDefaults(RadioDefaults): + poll_rate = 100 #ms + timeout = 1000 #ms + self.rx_defaults = RxDefaults() + + @log_method def send(self, payload, radio_params=None): # payload is a pydict suitable for OpenThings # radio_params is an overlay on top of radio tx defaults - # tx, pydict payload and radio params in pass #TODO - #TODO: OpenThings.encode() - #TODO: configure radio modulation - #TODO: set radio to transmit mode + p = OpenThings.encode(payload) + #TODO: merge radio_params with self.tx_defaults + #TODO: configure radio modulation based on merged params + radio.transmitter(fsk=True) #TODO: configure other radio parameters #TODO: transmit payload - #TODO: return radio to state before transmit + radio.transmit(p, outer_times=0, inner_times=4, outer_delay=0) + # radio auto-returns to previous state after transmit completes + @log_method def receive(self, radio_params): # -> (radio_measurements, address or None, payload or None) # radio_params is an overlay on top of radio rx defaults (e.g. poll rate, timeout, min payload, max payload) # radio_measurements might include rssi reading, short payload report, etc - # rx, configure radio for FSK receive OpenThings decode and decrypt, - # pydict payload and metadata (RSSI etc) out pass # TODO - #TODO: configure radio modulation #TODO: set radio to receive mode - #TODO: set other radio parameters + #TODO: merge radio_params with self.tx_defaults + #TODO: configure radio modulation based on merged params + #TODO: poll radio at rate until timeout or received - #TODO: return radio to state it was before receive - #TODO: OpenThings.decode - #TODO: report damaged payload (crc failure) - #TODO: extract addresses - #TODO: return (radio_measurements, address, payload) + #TODO: start timeout timer + payload = None + radio.receiver(fsk=True) + while True: # timer not expired + if radio.is_receive_waiting(): + payload = radio.receive() # TODO payload, radio_measurements = radio.receive() + p = OpenThings.decode(payload) + #TODO: if crc failure, report it, but keep trying + #if crc check passes... + break + #TODO: inter-try delay + #TODO: return radio to state it was before receiver (e.g. standby) - radio needs a pop() on this too? + + if payload == None: # nothing received in timeout + return (None, None, None) # (radio_measurements, address, payload) # TODO: might be measurements, average min max? + + #TODO: extract addresses: header_manufacturerid, header_productid header_deviceid -> (m, p, d) + m, p, d = None, None, None + radio_measurements = None # TODO get from radio.receive() + address = (m, p, d) + return (radio_measurements, address, payload) class TwoBitAirInterface(): def __init__(self): self.radio = radio # aids mocking later - #TODO: tx defaults - # OOK, inner_repeats, outer_delay, outer_repeats, power_level, frequency - #TODO: rx defaults - # OOK, poll_rate, timeout, frequency + class RadioDefaults(): + frequency = 433.92 + modulation = radio.RADIO_MODULATION_OOK + + class TxDefaults(RadioDefaults): + power_level = 0 + inner_repeats = 8 + outer_delay = 0 + outer_repeats = 0 + self.tx_defaults = TxDefaults() + + class RxDefaults(RadioDefaults): + poll_rate = 100 #ms + timeout = 1000 #ms + self.rx_defaults = RxDefaults() + + @log_method def send(self, payload, radio_params=None): # payload is just a list of bytes, or a byte buffer # radio_params is an overlay on top of radio tx defaults - # tx, pydict payload and radio params in - # TwoBit encode, configure radio for OOK transmit, pass repeats - pass #TODO - #TODO: TwoBit.encode() - #TODO: configure radio modulation - #TODO: set radio to transmit mode - #TODO: configure other radio parameters + p = TwoBit.encode(payload) + radio.modulation(ook=True) + #TODO: merge radio_params with self.tx_defaults + #TODO: configure radio modulation based on merged params #TODO: transmit payload - #TODO: return radio to state before transmit + radio.transmit(p, outer_times=1, inner_times=8, outer_delay=0) #TODO: radio params + # radio auto-pops to state before transmit + @log_method def receive(self, radio_params): # -> (radio_measurements, address or None, payload or None) # radio_params is an overlay on top of radio rx defaults (e.g. poll rate, timeout, min payload, max payload) # radio_measurements might include rssi reading, short payload report, etc - # rx, configure radio for OOK receive, TwoBit decode - # pydict payload and metadata (RSSI etc) out - pass # TODO - #TODO: configure radio modulation - #TODO: set radio to receive mode - #TODO: set other radio parameters + #TODO: merge radio_params with self.tx_defaults + #TODO: configure radio modulation based on merged params + #TODO: poll radio at rate until timeout or received - #TODO: return radio to state it was before receive - #TODO: TwoBit.decode - #TODO: report damaged payload?? + #TODO: start timeout timer + payload = None + radio.receiver(ook=True) + while True: # timer not expired + if radio.is_receive_waiting(): + #TODO: radio config should set receive preamble 4 bytes to prevent false triggers + payload = radio.receive(size=12) # TODO payload, radio_measurements = radio.receive() + p = TwoBit.decode(payload) + #TODO: if failure, report it, but keep trying + #if check passes... + break + #TODO: inter-try delay + #TODO: return radio to state it was before receiver (e.g. standby) - radio needs a pop() on this too? + + if payload == None: # nothing received in timeout + return (None, None, None) # (radio_measurements, address, payload) # TODO: might be measurements, average min max? + #TODO: extract addresses (house_address, device_index) - #TODO: return (radio_measurements, address, payload) + radio_measurements = None #TODO: return this from radio.receive() + h = 0xC8C8C #TODO: Get house address from TwoBit.decode()[:10] + d = 0xEE #TODO: Get device command from TwoBit.decode()[11:12] + address = (h, d) + return (radio_measurements, address, payload) # END diff --git a/src/energenie/TwoBit.py b/src/energenie/TwoBit.py index 484a914..283373a 100644 --- a/src/energenie/TwoBit.py +++ b/src/energenie/TwoBit.py @@ -58,6 +58,7 @@ return line +#TODO: encode_relay_msg def build_relay_msg(relayState=False): """Temporary test code to prove we can turn the relay on or off""" @@ -72,6 +73,7 @@ return payload +#TODO: encode_test_message def build_test_message(pattern): """build a test message for a D3D2D1D0 control patter""" payload = PREAMBLE + DEFAULT_ADDR_ENC @@ -81,6 +83,7 @@ return payload +#TODO: encode_switch_msg def build_switch_msg(state, device_address=ALL_SOCKETS, house_address=None): """Build a message to turn a switch on or off""" #print("build: state:%s, device:%d, house:%s" % (str(state), device_address, str(house_address))) @@ -133,6 +136,7 @@ #print("encoded as:%s" % ashex(payload)) return payload +#TODO: decode_switch_msg def encode_bytes(data): """Turn a list of bytes into a modulated pattern equivalent""" @@ -165,5 +169,22 @@ return encoded +#TODO: decode_bytes + +#TODO: decode_bits + +#TODO: decode_command +# 0, False (all off) +# 0, True (all on) +# 1, False (1 off) +# 1, True (1 on) +# 2, False (2 off) +# 2, True (2 on) +# 3, False (3 off) +# 3, True (3 on) +# 4, False (4 off) +# 4, True (4 on) +# UNKNOWN (6 of the other patterns, that are not recognised) + # END