diff --git a/src/energenie/Devices.py b/src/energenie/Devices.py index 5f5d7a2..668e4c9 100644 --- a/src/energenie/Devices.py +++ b/src/energenie/Devices.py @@ -270,7 +270,7 @@ """A generic connected device abstraction""" def __init__(self, device_id=None, air_interface=None): self.air_interface = air_interface - self.device_id = device_id + self.device_id = self.parse_device_id(device_id) class Config(): pass self.config = Config() class Capabilities(): pass @@ -280,6 +280,59 @@ def get_config(self): raise RuntimeError("There is no configuration for a base Device") + @staticmethod + def parse_device_id(device_id): + """device_id could be a number, a hex string or a decimal string""" + ##print("**** parsing: %s" % str(device_id)) + if device_id == None: + raise ValueError("device_id is None, not allowed") + + if type(device_id) == int: + return device_id # does not need to be parsed + + if type(device_id) == tuple or type(device_id) == list: + # each part of the tuple could be encoded + res = [] + for p in device_id: + res.append(Device.parse_device_id(p)) + #TODO: could usefully convert to tuple here to be helpful + return res + + if type(device_id) == str: + # could be hex or decimal or strtuple or strlist + if device_id == "": + raise ValueError("device_id is blank, not allowed") + elif device_id.startswith("0x"): + return int(device_id, 16) + elif device_id[0] == '(' and device_id[-1] == ')': + ##print("**** parse tuple") + inner = device_id[1:-1] + parts = inner.split(',') + ##print(parts) + res = [] + for p in parts: + res.append(Device.parse_device_id(p)) + ##print(res) + return res + + elif device_id[0] == '[' and device_id[-1] == ']': + ##print("**** parse list") + inner = device_id[1:-1] + parts = inner.split(',') + ##print(parts) + res = [] + for p in parts: + res.append(Device.parse_device_id(p)) + #TODO: could usefully change to tuple here + ##print(res) + return res + else: + return int(device_id, 10) + + else: + raise ValueError("device_id unsupported type or format, got: %s %s" % (type(device_id), str(device_id))) + + def has_switch(self): return hasattr(self.capabilities, "switch") @@ -403,8 +456,8 @@ """Get the persistable config, enough to reconstruct this class from a factory""" return { "type": self.__class__.__name__, - "manufacturer_id": self.manufacturer_id, - "product_id": self.product_id, + ##"manufacturer_id": self.manufacturer_id, # not needed, known by class + ##"product_id": self.product_id, # not needed, known by class "device_id": self.device_id } @@ -463,6 +516,10 @@ 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]))) + + def turn_on(self): #TODO should this be here, or in LegacyDevice?? #addressing should probably be in LegacyDevice @@ -485,8 +542,6 @@ } self.send_message(payload) - def __repr__(self): - return "ENER002(%s,%s)" % (str(hex(self.device_id[0]), str(self.device_id[1]))) class MIHO004(MiHomeDevice): @@ -744,7 +799,7 @@ return DeviceFactory.device_from_name.keys() @staticmethod - def get_device_from_name(name, device_id=None, air_interface=None): + def get_device_from_name(name, device_id=None, air_interface=None, **kwargs): """Get a device by name, construct a new instance""" # e.g. This is useful when creating device class instances from a human readable config if not name in DeviceFactory.device_from_name: @@ -753,7 +808,7 @@ c = DeviceFactory.device_from_name[name] if air_interface == None: air_interface = DeviceFactory.default_air_interface - return c(device_id, air_interface) + return c(device_id, air_interface, **kwargs) @staticmethod def get_device_from_id(id, device_id=None, air_interface=None): diff --git a/src/energenie/KVS.py b/src/energenie/KVS.py index 45e5d80..e593ce8 100644 --- a/src/energenie/KVS.py +++ b/src/energenie/KVS.py @@ -30,6 +30,7 @@ key = None obj = None + ##print("load from %s" % filename) with open(filename) as f: while True: line = f.readline() @@ -124,7 +125,7 @@ def append(self, key, values): """Append a new record to the persistent file""" - print("append:%s %s" % (key, values)) + ##print("append:%s %s" % (key, values)) if self.filename != None: with open(self.filename, 'a') as f: @@ -150,11 +151,11 @@ line = line.strip() # remove nl cmd, this_key = line.split(" ", 1) if this_key == key: - print("found: %s %s" % (cmd, this_key)) + ##print("found: %s %s" % (cmd, this_key)) f.seek(start) # back to start of line f.write('IGN') # patch it to be an ignore record but leave record intact f.seek(end) # back to end of line, to process next lines - print("Patched to IGN rec") + ##print("Patched to IGN rec") def write(self, filename=None): """Rewrite the whole in memory cache over the top of the external file""" diff --git a/src/energenie/Registry.py b/src/energenie/Registry.py index 6cfa5fb..e8ec308 100644 --- a/src/energenie/Registry.py +++ b/src/energenie/Registry.py @@ -77,8 +77,7 @@ DEFAULT_FILENAME = "registry.kvs" def __init__(self, filename=None): - if filename != None: - self.store = KVS(filename) + self.store = KVS(filename) @untested def load_from(self, filename=None): @@ -87,7 +86,7 @@ # Create a new in memory store, effectively removing any existing in memory device class instances #TODO: Not good if there are routes to those class instances? self.store = KVS(filename) #TODO: later we might make it possible to load_from multiple files - self.store.load(Devices.DeviceFactory) + self.store.load(filename, Devices.DeviceFactory.get_device_from_name) @unimplemented def reload(self): @@ -112,7 +111,7 @@ def add(self, device, name): """Add a device class instance to the registry, with a friendly name""" - self.store[name] = device.get_config() + self.store[name] = device def get(self, name): # -> Device """Get the description for a device class from the store, and construct a class instance""" diff --git a/src/energenie/Registry_test.py b/src/energenie/Registry_test.py index f74a1b5..d858600 100644 --- a/src/energenie/Registry_test.py +++ b/src/energenie/Registry_test.py @@ -19,7 +19,10 @@ def remove_file(filename): import os - os.unlink(filename) + try: + os.unlink(filename) + except OSError: + pass # ok if it does not already exist def show_file(filename): @@ -29,28 +32,43 @@ l = l.strip() # remove nl print(l) +def create_file(filename): + with open(filename, "w"): + pass + class TestRegistry(unittest.TestCase): + #----- HERE ----- + @test_0 def test_create(self): + """Make a registry file by calling methods, and see that the file is the correct format""" remove_file(REGISTRY_KVS) + create_file(REGISTRY_KVS) registry = DeviceRegistry(REGISTRY_KVS) # add some devices to the registry, it should auto update the file - registry.add(Devices.MIHO005(device_id=0x68b), "tv") - registry.add(Devices.ENER002(device_id=(0xC8C8C, 1)), "fan") + tv = Devices.MIHO005(device_id=0x68b) + fan = Devices.ENER002(device_id=(0xC8C8C, 1)) + registry.add(tv, "tv") + registry.add(fan, "fan") # see what the file looks like show_file(registry.DEFAULT_FILENAME) + @test_1 def test_load(self): + """Load back a persisted registry and create objects from them, in the registry""" + # create a registry file remove_file(REGISTRY_KVS) + create_file(REGISTRY_KVS) registry = DeviceRegistry(REGISTRY_KVS) registry.add(Devices.MIHO005(device_id=0x68b), "tv") registry.add(Devices.ENER002(device_id=(0xC8C8C, 1)), "fan") + registry.list() # clear the in memory registry registry = None @@ -62,18 +80,27 @@ # dump the registry state registry.list() - #TODO: What about receive route testing?? + #TODO loading the registry should set up receive routes also + #perhaps we have to get the registry to do that *after* loading all objects + #as an extra pass? + fsk_router.list() #### FAIL no routes created by registry + self.fail("no routes") #TODO:#### - @test_0 + @test_0 # DONE def test_load_into(self): - pass #TODO - # load from a persisted registry - # load_into some context - # make sure all the loaded context variables point to the right thing + # create an in memory registry + registry = DeviceRegistry() + registry.add(Devices.MIHO005(device_id=0x68b), "tv") + registry.add(Devices.ENER002(device_id=(0xC8C8C, 1)), "fan") + class MyContext():pass + context = MyContext() + registry.load_into(context) + print(context.tv) + print(context.fan)