diff --git a/doc/branch_new_devices_june16.txt b/doc/branch_new_devices_june16.txt deleted file mode 100644 index 2b5f247..0000000 --- a/doc/branch_new_devices_june16.txt +++ /dev/null @@ -1,26 +0,0 @@ -New devices to add: - --------------------------------------------------------------------------------- - -MiHome Light - MIHO008 OOK White - MIHO024 OOK Black Nickel - MIHO025 OOK Chrome - MIHO026 OOK Brushed Steel - - RX ONLY - OOK, OUTER_TIMES=1, OUTER_DELAY=0, INNER_TIMES=75 - - --------------------------------------------------------------------------------- - -KNOCK ON TODOs -- need to pass forward radio config parameters to get INNER_TIMES=75 for the MiHome Light. - - need to think about config.tx_repeats vs get_config() - these are different concepts - - --------------------------------------------------------------------------------- - - - - diff --git a/src/energenie/Devices.py b/src/energenie/Devices.py index 0202c23..5a998f8 100644 --- a/src/energenie/Devices.py +++ b/src/energenie/Devices.py @@ -22,7 +22,7 @@ MFRID = MFRID_ENERGENIE ##PRODUCTID_MIHO001 = # Home Hub -##PRODUCTID_MIHO002 = # Control only (Uses Legacy OOK protocol) +##PRODUCTID_MIHO002 = # OOK Control only ##PRODUCTID_MIHO003 = 0x0? # Hand Controller PRODUCTID_MIHO004 = 0x01 # Monitor only PRODUCTID_MIHO005 = 0x02 # Adaptor Plus @@ -34,7 +34,7 @@ ##PRODUCTID_MIHO011 not used ##PRODUCTID_MIHO012 not used PRODUCTID_MIHO013 = 0x03 # eTRV -##PRODUCTID_MIHO014 = 0x0? # In-line Relay +##PRODUCTID_MIHO014 # OOK In-line Relay ##PRODUCTID_MIHO015 not used ##PRODUCTID_MIHO016 not used ##PRODUCTID_MIHO017 @@ -222,8 +222,8 @@ def __init__(self, device_id=None, air_interface=None): self.air_interface = air_interface self.device_id = self.parse_device_id(device_id) - class Config(): pass - self.config = Config() + class RadioConfig(): pass + self.radio_config = RadioConfig() class Capabilities(): pass self.capabilities = Capabilities() self.updated_cb = None @@ -295,7 +295,7 @@ return hasattr(self.capabilities, "receive") def get_radio_config(self): - return self.config + return self.radio_config def get_last_receive_time(self): # ->timestamp """The timestamp of the last time any message was received by this device""" @@ -372,7 +372,7 @@ return self.device_id def __repr__(self): - return "Device(%s)" % str(self.device_id) + return "EnergenieDevice(%s)" % str(self.device_id) class LegacyDevice(EnergenieDevice): @@ -390,10 +390,10 @@ device_id = (LegacyDevice.DEFAULT_HOUSE_ADDRESS, device_id[1]) EnergenieDevice.__init__(self, device_id, ook_interface) - #TODO: These might now just be implied by the ook_interface adaptor - self.config.frequency = 433.92 - self.config.modulation = "OOK" - self.config.codec = "4bit" + #TODO: These are now just be implied by the ook_interface adaptor + ##self.radio_config.frequency = 433.92 + ##self.radio_config.modulation = "OOK" + ##self.radio_config.codec = "4bit" def __repr__(self): return "LegacyDevice(%s)" % str(self.device_id) @@ -407,18 +407,8 @@ def send_message(self, payload): - #TODO: interface with air_interface - # Encode the payload two bits per byte as per OOK spec - #TODO: should we just pass a payload (as a pydict or tuple) to the air_interface adaptor - #and let it encode it, to be consistent with the FSK MiHome devices? - #payload could be a 3-tuple of (house_address, device_address, state) - ##bytes = TwoBit.build_switch_msg(payload, house_address=self.device_id[0], device_address=self.device_id[1]) - if self.air_interface != None: - #TODO: might want to send the config, either as a send parameter, - #or by calling air_interface.configure() first? - #i.e. radio.modulation(MODULATION_OOK) - self.air_interface.send(payload) #TODO: or (ha, da, s) + self.air_interface.send(payload, radio_config=self.radio_config) else: d = self.device_id print("send_message(mock[%s]):%s" % (str(d), payload)) @@ -430,11 +420,12 @@ if air_interface == None: air_interface = fsk_interface EnergenieDevice.__init__(self, device_id, air_interface) - self.config.frequency = 433.92 - self.config.modulation = "FSK" - self.config.codec = "OpenThings" - self.manufacturer_id = MFRID_ENERGENIE - self.product_id = None + #TODO: These are now implied by the air_interface adaptor + ##self.radio_config.frequency = 433.92 + ##self.radio_config.modulation = "FSK" + ##self.radio_config.codec = "OpenThings" + self.manufacturer_id = MFRID_ENERGENIE + self.product_id = None #Different devices might have different PIP's #if we are cycling codes on each message? @@ -499,16 +490,16 @@ #------------------------------------------------------------------------------ -class ENER002(LegacyDevice): - """A green-button switch""" +class OOKSwitch(LegacyDevice): + """Any OOK controlled switch""" def __init__(self, device_id, air_interface=None): LegacyDevice.__init__(self, device_id, air_interface) - self.config.tx_repeats = 8 + self.radio_config.inner_times = 8 self.capabilities.switch = True self.capabilities.receive = True def __repr__(self): - return "ENER002(%s,%s)" % (str(hex(self.device_id[0])), str(hex(self.device_id[1]))) + return "OOKSwitch(%s,%s)" % (str(hex(self.device_id[0])), str(hex(self.device_id[1]))) def turn_on(self): @@ -540,13 +531,31 @@ self.turn_off() +class ENER002(OOKSwitch): + """A green button switch""" + def __repr__(self): + return "ENER002(%s,%s)" % (str(hex(self.device_id[0])), str(hex(self.device_id[1]))) + + +class MIHO002(OOKSwitch): + """A purple button MiHome switch""" + def __repr__(self): + return "MIHO002(%s,%s)" % (str(hex(self.device_id[0])), str(hex(self.device_id[1]))) + + +class MIHO014(OOKSwitch): + """Energenie 3kW switchable relay""" + def __repr__(self): + return "MIHO014(%s,%s)" % (str(hex(self.device_id[0])), str(hex(self.device_id[1]))) + + #------------------------------------------------------------------------------ class MiHomeLight(LegacyDevice): """Base for all MiHomeLight variants. Receive only OOK device""" def __init__(self, device_id, air_interface=None): LegacyDevice.__init__(self, device_id, air_interface) - self.config.tx_repeats = 75 + self.radio_config.inner_times = 75 self.capabilities.switch = True self.capabilities.receive = True @@ -562,8 +571,6 @@ "device_index": self.device_id[1], "on": True } - #TODO: Need to pass forward the new radio config OUTER_TIMES=1 OUTER_DELAY=1 INNER_TIMES=75 - #using self.config.tx_repeats self.send_message(payload) def turn_off(self): @@ -575,8 +582,6 @@ "device_index": self.device_id[1], "on": False } - #TODO: Need to pass forward the new radio config OUTER_TIMES=1 OUTER_DELAY=1 INNER_TIMES=75 - #using self.config.tx_repeats self.send_message(payload) def set_switch(self, state): @@ -610,8 +615,89 @@ #------------------------------------------------------------------------------ class MIHO004(MiHomeDevice): - """Monitor only Adaptor""" - pass #TODO + """Energenie Monitor-only Adaptor""" + def __init__(self, device_id, air_interface=None): + MiHomeDevice.__init__(self, device_id, air_interface) + self.product_id = PRODUCTID_MIHO004 + class Readings(): + voltage = None + frequency = None + current = None + apparent_power = None + reactive_power = None + real_power = None + self.readings = Readings() + self.radio_config.inner_times = 4 + self.capabilities.send = True + self.capabilities.switch = True + + def __repr__(self): + return "MIHO004(%s)" % str(hex(self.device_id)) + + @staticmethod + def get_join_req(deviceid): + """Get a synthetic join request from this device, for testing""" + return MiHomeDevice.get_join_req(MFRID_ENERGENIE, PRODUCTID_MIHO004, deviceid) + + def handle_message(self, payload): + ##print("MIHO005 new data %s %s" % (self.device_id, payload)) + for rec in payload["recs"]: + paramid = rec["paramid"] + #TODO: consider making this table driven and allowing our base class to fill our readings in for us + # then just define the mapping table in __init__ (i.e. paramid->Readings field name) + value = rec["value"] + if paramid == OpenThings.PARAM_VOLTAGE: + self.readings.voltage = value + elif paramid == OpenThings.PARAM_CURRENT: + self.readings.current = value + elif paramid == OpenThings.PARAM_REAL_POWER: + self.readings.real_power = value + elif paramid == OpenThings.PARAM_APPARENT_POWER: + self.readings.apparent_power = value + elif paramid == OpenThings.PARAM_REACTIVE_POWER: + self.readings.reactive_power = value + elif paramid == OpenThings.PARAM_FREQUENCY: + self.readings.frequency = value + else: + try: + param_name = OpenThings.param_info[paramid]['n'] # name + except: + param_name = "UNKNOWN_%s" % str(hex(paramid)) + print("unwanted paramid: %s" % param_name) + + def get_readings(self): # -> readings:pydict + """A way to get all readings as a single consistent set""" + return self.readings + + def get_voltage(self): # -> voltage:float + """Last stored state of voltage reading, None if unknown""" + if self.readings.voltage == None: + raise RuntimeError("No voltage reading received yet") + return self.readings.voltage + + def get_frequency(self): # -> frequency:float + """Last stored state of frequency reading, None if unknown""" + if self.readings.frequency == None: + raise RuntimeError("No frequency reading received yet") + return self.readings.frequency + + def get_apparent_power(self): # ->power:float + """Last stored state of apparent power reading, None if unknown""" + if self.readings.apparent_power == None: + raise RuntimeError("No apparent power reading received yet") + return self.readings.apparent_power + + def get_reactive_power(self): # -> power:float + """Last stored state of reactive power reading, None if unknown""" + if self.readings.reactive_power == None: + raise RuntimeError("No reactive power reading received yet") + return self.readings.reactive_power + + def get_real_power(self): #-> power:float + """Last stored state of real power reading, None if unknown""" + if self.readings.real_power == None: + raise RuntimeError("No real power reading received yet") + return self.readings.real_power #------------------------------------------------------------------------------ @@ -630,7 +716,7 @@ reactive_power = None real_power = None self.readings = Readings() - self.config.tx_repeats = 4 + self.radio_config.inner_times = 4 self.capabilities.send = True self.capabilities.receive = True self.capabilities.switch = True @@ -759,15 +845,47 @@ class Readings(): battery_voltage = None current = None + apparent_power = None self.readings = Readings() self.capabilities.send = True + def __repr__(self): + return "MIHO006(%s)" % str(hex(self.device_id)) + + def handle_message(self, payload): + for rec in payload["recs"]: + paramid = rec["paramid"] + #TODO: consider making this table driven and allowing our base class to fill our readings in for us + #TODO: consider using @OpenThings.parameter as a decorator to the receive function + #it will then register a handler for that message for itself as a handler + #we still need Readings() defined too as a cache. The decorator could add + #an entry into the cache too for us perhaps? + if "value" in rec: + value = rec["value"] + if paramid == OpenThings.PARAM_VOLTAGE: + self.readings.battery_voltage = value + elif paramid == OpenThings.PARAM_CURRENT: + self.readings.current = value + elif paramid == OpenThings.PARAM_APPARENT_POWER: + self.readings.apparent_power = value + else: + try: + param_name = OpenThings.param_info[paramid]['n'] # name + except: + param_name = "UNKNOWN_%s" % str(hex(paramid)) + print("unwanted paramid: %s" % param_name) + pass + def get_battery_voltage(self): # -> voltage:float return self.readings.battery_voltage def get_current(self): # -> current:float return self.readings.current + def get_apparent_power(self): # -> power:float + return self.reading.apparent_power + + #------------------------------------------------------------------------------ @@ -783,7 +901,7 @@ setpoint_temperature = None valve_position = None self.readings = Readings() - self.config.tx_repeats = 10 + self.radio_config.inner_times = 10 self.capabilities.send = True self.capabilities.receive = True @@ -924,29 +1042,43 @@ # i.e. this might be the EnergenieDeviceFactory, there might be others # for other product ranges like wirefree doorbells + class DeviceFactory(): """A place to come to, to get instances of device classes""" # If you know the name of the device, use this table device_from_name = { # official name friendly name - "ENER002": ENER002, "GreenButton": ENER002, - "MIHO005": MIHO005, "AdaptorPlus": MIHO005, - "MIHO006": MIHO006, "HomeMonitor": MIHO006, - "MIHO013": MIHO013, "eTRV": MIHO013, - "MIHO032": MIHO032, "MotionSensor": MIHO032, - "MIHO033": MIHO033, "OpenSensor": MIHO033 + "ENER002": ENER002, "GreenButton": ENER002, # OOK(rx) + "MIHO002": MIHO002, "Controller": MIHO002, # OOK(rx) + "MIHO004": MIHO004, "Monitor": MIHO004, # FSK(rx) + "MIHO005": MIHO005, "AdaptorPlus": MIHO005, # FSK(tx,rx) + "MIHO006": MIHO006, "HomeMonitor": MIHO006, # FSK(tx) + "MIHO008": MIHO008, "MiHomeLightWhite": MIHO008, # OOK(rx) + "MIHO013": MIHO013, "eTRV": MIHO013, # FSK(tx,rx) + "MIHO014": MIHO014, "3kWRelay": MIHO014, # OOK(rx) + "MIHO024": MIHO024, "MiHomeLightBlack": MIHO024, # OOK(rx) + "MIHO025": MIHO025, "MiHomeLightChrome": MIHO025, # OOK(rx) + "MIHO026": MIHO026, "MiHomeLightSteel": MIHO026, # OOK(rx) + "MIHO032": MIHO032, "MotionSensor": MIHO032, # FSK(tx) + "MIHO033": MIHO033, "OpenSensor": MIHO033, # FSK(tx) } #TODO: These are MiHome devices only, but might add in mfrid prefix too # If you know the mfrid, productid of the device, use this table device_from_id = { + #ENER002 is anOOK + #MIHO002 control only switch is an OOK PRODUCTID_MIHO004: MIHO004, PRODUCTID_MIHO005: MIHO005, PRODUCTID_MIHO006: MIHO006, + #MIHO008 is an OOK PRODUCTID_MIHO013: MIHO013, + #MIHO014 is an OOK + #MIHO024 is an OOK + #MIHO025 is an OOK + #MIHO026 is an OOK PRODUCTID_MIHO032: MIHO032, PRODUCTID_MIHO033: MIHO033 - #ENER product range does not have deviceid, as it does not transmit } default_air_interface = None diff --git a/src/energenie/OnAir.py b/src/energenie/OnAir.py index 3b4b9a1..1f3db92 100644 --- a/src/energenie/OnAir.py +++ b/src/energenie/OnAir.py @@ -13,7 +13,7 @@ # NOTE: This also might include intelligent power level selection based # on RSSI reports from different devices. -##from lifecycle import * +from lifecycle import * import time try: @@ -37,10 +37,10 @@ modulation = radio.RADIO_MODULATION_FSK class TxDefaults(RadioDefaults): - power_level = 0 - inner_repeats = 4 + ##power_level = 0 + inner_times = 4 outer_delay = 0 - outer_repeats = 0 + outer_times = 1 self.tx_defaults = TxDefaults() class RxDefaults(RadioDefaults): @@ -49,21 +49,35 @@ self.rx_defaults = RxDefaults() ##@log_method - def send(self, payload, radio_params=None): + def send(self, payload, radio_config=None): # payload is a pydict suitable for OpenThings # radio_params is an overlay on top of radio tx defaults - pass #TODO p = OpenThings.encode(payload) - #TODO: merge radio_params with self.tx_defaults - #TODO: configure radio modulation based on merged params + + # Set radio defaults, if no override + outer_times = self.tx_defaults.outer_times + outer_delay = self.tx_defaults.outer_delay + inner_times = self.tx_defaults.inner_times + + # Merge any wanted radio params, if provided + if radio_config != None: + try: + outer_times = radio_config.outer_times + except AttributeError: pass + try: + outer_delay = radio_config.outer_delay + except AttributeError: pass + try: + inner_times = radio_config.inner_times + except AttributeError: pass + radio.transmitter(fsk=True) - #TODO: configure other radio parameters - #TODO: transmit payload - radio.transmit(p, outer_times=1, inner_times=4, outer_delay=0) + ##print("inner times %s" % inner_times) + radio.transmit(p, outer_times=outer_times, inner_times=inner_times, outer_delay=outer_delay) # radio auto-returns to previous state after transmit completes ##@log_method - def receive(self, radio_params): # -> (radio_measurements, address or None, payload or None) + def receive(self, radio_config=None): # -> (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 pass # TODO @@ -106,9 +120,9 @@ class TxDefaults(RadioDefaults): power_level = 0 - inner_repeats = 8 + inner_times = 8 outer_delay = 0 - outer_repeats = 0 + outer_times = 1 self.tx_defaults = TxDefaults() class RxDefaults(RadioDefaults): @@ -117,9 +131,9 @@ self.rx_defaults = RxDefaults() ##@log_method - def send(self, payload, radio_params=None): + def send(self, payload, radio_config=None): # payload is just a list of bytes, or a byte buffer - # radio_params is an overlay on top of radio tx defaults + # radio_config is an overlay on top of radio tx defaults house_address = payload["house_address"] device_index = payload["device_index"] @@ -127,15 +141,29 @@ bytes = TwoBit.encode_switch_message(state, device_index, house_address) radio.modulation(ook=True) - #TODO: merge radio_params with self.tx_defaults - #TODO: configure radio modulation based on merged params - #TODO: transmit payload + # Set radio defaults, if no override + outer_times = self.tx_defaults.outer_times + outer_delay = self.tx_defaults.outer_delay + inner_times = self.tx_defaults.inner_times - radio.transmit(bytes, outer_times=1, inner_times=8, outer_delay=0) #TODO: radio params + # Merge any wanted radio params, if provided + if radio_config != None: + try: + outer_times = radio_config.outer_times + except AttributeError: pass + try: + outer_delay = radio_config.outer_delay + except AttributeError: pass + try: + inner_times = radio_config.inner_times + except AttributeError: pass + + ##print("inner times %s" % inner_times) + radio.transmit(bytes, outer_times=outer_times, inner_times=inner_times, outer_delay=outer_delay) # radio auto-pops to state before transmit ##@log_method - def receive(self, radio_params): # -> (radio_measurements, address or None, payload or None) + def receive(self, radio_config=None): # -> (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 #TODO: merge radio_params with self.tx_defaults diff --git a/src/registry.kvs b/src/registry.kvs index 678732d..cbcae8d 100644 --- a/src/registry.kvs +++ b/src/registry.kvs @@ -2,15 +2,53 @@ type=ENER002 device_id=[0x6C6C6, 1] +IGN controller +type=MIHO002 +device_id=0000 + +IGN monitor +type=MIHO004 +device_id=0000 + ADD tv type=MIHO005 device_id=1675 +ADD house +type=MIHO006 +device_id=441 + +ADD light_white +type=MIHO008 +device_id=[0x6C6C6, 1] + +IGN radiator +type=MIHO013 +device_id=0000 + +ADD relay +type=MIHO014 +device_id=[0x6C6C6, 1] + +ADD light_black +type=MIHO024 +device_id=[0x6C6C6, 2] + +ADD light_chrome +type=MIHO025 +device_id=[0x6C6C6, 3] + +ADD light_steel +type=MIHO026 +device_id=[0x6C6C6, 4] + +ADD motion +type=MIHO032 +device_id=4042 + ADD door type=MIHO033 device_id=2817 -ADD PIR -type=MIHO032 -device_id=4042 + diff --git a/src/setup_tool.py b/src/setup_tool.py index 30f5b66..122c309 100644 --- a/src/setup_tool.py +++ b/src/setup_tool.py @@ -110,7 +110,8 @@ # get device house_code = get_house_code() device_index = get_device_index() - device = energenie.Devices.ENER002((house_code, device_index)) + # Use a MiHomeLight as it has the longest TX time + device = energenie.Devices.MIHO008((house_code, device_index)) # in a loop until Ctrl-C print("Legacy learn broadcasting, Ctrl-C to stop")