From 298ca9cfde6cc9d7115e572cd7dd3752230520d3 Mon Sep 17 00:00:00 2001 From: Florian Date: Sun, 22 Dec 2019 23:42:48 +0100 Subject: [PATCH 01/31] fix non newstyle config --- .../custom_components/duofern/__init__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/homeassistant/custom_components/duofern/__init__.py b/examples/homeassistant/custom_components/duofern/__init__.py index 81658b5..f25304c 100644 --- a/examples/homeassistant/custom_components/duofern/__init__.py +++ b/examples/homeassistant/custom_components/duofern/__init__.py @@ -40,11 +40,13 @@ def setup(hass, config): from pyduofern.duofern_stick import DuofernStickThreaded - newstyle_config = hass.config_entries.async_entries(DOMAIN)[0] - if newstyle_config: - serial_port = newstyle_config.data['serial_port'] - code = newstyle_config.data['code'] - configfile = newstyle_config.data['config_file'] + newstyle_config = hass.config_entries.async_entries(DOMAIN) + if len(newstyle_config) > 0: + newstyle_config = newstyle_config[0] + if newstyle_config: + serial_port = newstyle_config.data['serial_port'] + code = newstyle_config.data['code'] + configfile = newstyle_config.data['config_file'] elif config.get(DOMAIN) is not None: serial_port = config[DOMAIN].get(CONF_SERIAL_PORT) From 29f3a32cc13cb19268bcd741846a245b49abd6f6 Mon Sep 17 00:00:00 2001 From: Florian Date: Sun, 22 Dec 2019 23:43:59 +0100 Subject: [PATCH 02/31] doc: avoid overwriting config --- examples/readme.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/readme.rst b/examples/readme.rst index 803f768..3075bb3 100644 --- a/examples/readme.rst +++ b/examples/readme.rst @@ -15,7 +15,7 @@ The following is setup procedure with hassio git clone https://github.com/gluap/pyduofern/ cp -r pyduofern/examples/homeassistant/custom_components /config/ # next line only if you don't already have it yet: - echo "duofern:" >/config/configuration.yaml + echo "duofern:" >> /config/configuration.yaml # next use 4 digit hex code of your choice instead of ffff ("password" for your duofern net) # if you are migrating from FHEM: Skip the "6f" from the beginning of the code, # only use the last 4 characters From 2dc3f6a932e01107b7c5dbdc47745fd371a5b292 Mon Sep 17 00:00:00 2001 From: Florian Date: Sun, 22 Dec 2019 23:50:43 +0100 Subject: [PATCH 03/31] add and update channel-specific devices In the FHEM module, a dual actor is represented as three devices. One main device (e.g. 4376bb) and one for each channel with appended channel number (e.g. 4376bb01 and 4376bb02). For consistency, this should be also done for pyduofern, but was only partly implemented, yet. In particular, these channel-specific devices where not added as own devices and thus could not be addressed, leading to exceptions. This commit adds all three devices and also sets the state of the respective device. --- pyduofern/duofern.py | 352 ++++++++++++++++++++++--------------------- 1 file changed, 181 insertions(+), 171 deletions(-) diff --git a/pyduofern/duofern.py b/pyduofern/duofern.py index faf6b18..6231f9b 100644 --- a/pyduofern/duofern.py +++ b/pyduofern/duofern.py @@ -96,9 +96,11 @@ def __init__(self, send_hook=None, asyncio=False, changes_callback=None): def add_device(self, code, name=None): if name is None: - name = len(self.modules['by_code']) + name = code logger.debug("adding {}".format(code)) self.modules['by_code'][code] = {'name': name} + if len(code) == 8: + self.modules['by_code'][code]['chanNo'] = code[6:] def del_device(self, code, name=None): if name is None: @@ -107,14 +109,14 @@ def del_device(self, code, name=None): if code in self.modules['by_code']: del self.modules['by_code'][code] - def update_state(self, code, key, value, trigger=None): - self.modules['by_code'][code][key] = value + def update_state(self, hash, key, value, trigger=None): + hash[key] = value if self.changes_callback: self.changes_callback() - def delete_state(self, code, key): - if key in self.modules['by_code'][code]: - del self.modules['by_code'][code][key] + def delete_state(self, hash, key): + if key in hash: + del hash[key] def parse(self, msg): code = msg[30:36] @@ -149,15 +151,15 @@ def parse(self, msg): # Device paired if msg[0:4] == "0602": - self.update_state(code, "state", "paired", "1") + self.update_state(hash, "state", "paired", "1") # del hash['READINGS']['unpaired'] logger.info("DUOFERN device paired, ID {}".format(code)) # Device unpaired elif (msg[0:4] == "0603"): readingsBeginUpdate(hash) - self.update_state(code, "unpaired", 1, "1") - self.update_state(code, "state", "unpaired", "1") + self.update_state(hash, "unpaired", 1, "1") + self.update_state(hash, "state", "unpaired", "1") self.del_device(code) # readingsEndUpdate(hash, 1) # Notify is done by Dispatch logger.warning("DUOFERN device unpaired, code {}".format(code)) @@ -167,14 +169,14 @@ def parse(self, msg): format = msg[6:6 + 2] ver = msg[24:24 + 1] + msg[25:25 + 1] - self.update_state(code, "version", ver, "0") + self.update_state(hash, "version", ver, "0") # RemoveInternalTimer(hash) # del hash['helper']['timeout'] # Bewegungsmelder, Wettersensor, Mehrfachwandtaster not tested yet if code[0:2] in ("65", "69", "74"): # pragma: no cover - self.update_state(code, "state", "OK", "1") + self.update_state(hash, "state", "OK", "1") module_definition01 = self.modules['by_code'][code + "01"] if not module_definition01: DoTrigger("global", "UNDEFINED DUOFERN_code_actor DUOFERN code01") @@ -182,12 +184,20 @@ def parse(self, msg): # Universalaktor -- not tested yet elif code[0:2] == "43": # pragma: no cover - self.update_state(code, "state", "OK", "1") - module_definition01 = self.modules['by_code'][code] - if not module_definition01: - DoTrigger("global", "UNDEFINED DUOFERN_code+_01 DUOFERN code+01") + self.update_state(hash, "state", "OK", "1") + try: + module_definition01 = self.modules['by_code'][code + "01"] + except KeyError: + self.add_device(code + "01") + logger.info("detected unknown device, ID={}".format(code + "01")) + module_definition01 = self.modules['by_code'][code + "01"] - module_definition02 = None + try: + module_definition02 = self.modules['by_code'][code + "02"] + except KeyError: + self.add_device(code + "02") + logger.info("detected unknown device, ID={}".format(code + "02")) + module_definition02 = self.modules['by_code'][code + "02"] if module_definition01: hash = module_definition01 @@ -210,18 +220,18 @@ def parse(self, msg): state = "closed" if (pos == 100) else pos readingsBeginUpdate(hash) - self.update_state(code, "ventilatingPosition", ventPos, "1") - self.update_state(code, "ventilatingMode", ventMode, "1") - self.update_state(code, "sunPosition", sunPos, "1") - self.update_state(code, "sunMode", sunMode, "1") - self.update_state(code, "timeAutomatic", timerAuto, "1") - self.update_state(code, "sunAutomatic", sunAuto, "1") - self.update_state(code, "dawnAutomatic", dawnAuto, "1") - self.update_state(code, "duskAutomatic", duskAuto, "1") - self.update_state(code, "manualMode", manualMode, "1") - self.update_state(code, "position", pos, "1") - self.update_state(code, "state", state, "1") - self.update_state(code, "moving", "stop", "1") + self.update_state(hash, "ventilatingPosition", ventPos, "1") + self.update_state(hash, "ventilatingMode", ventMode, "1") + self.update_state(hash, "sunPosition", sunPos, "1") + self.update_state(hash, "sunMode", sunMode, "1") + self.update_state(hash, "timeAutomatic", timerAuto, "1") + self.update_state(hash, "sunAutomatic", sunAuto, "1") + self.update_state(hash, "dawnAutomatic", dawnAuto, "1") + self.update_state(hash, "duskAutomatic", duskAuto, "1") + self.update_state(hash, "manualMode", manualMode, "1") + self.update_state(hash, "position", pos, "1") + self.update_state(hash, "state", state, "1") + self.update_state(hash, "moving", "stop", "1") readingsEndUpdate(hash, 1) # Notify is done by Dispatch # Universal Aktor, Steckdosenaktor, Troll Comfort DuoFern (Lichtmodus) not tested yet @@ -242,17 +252,17 @@ def parse(self, msg): state = "on" if (level == 100) else level readingsBeginUpdate(hash) - self.update_state(code, "sunMode", sunMode, "1") - self.update_state(code, "timeAutomatic", timerAuto, "1") - self.update_state(code, "sunAutomatic", sunAuto, "1") - self.update_state(code, "dawnAutomatic", dawnAuto, "1") - self.update_state(code, "duskAutomatic", duskAuto, "1") - self.update_state(code, "manualMode", manualMode, "1") - self.update_state(code, "modeChange", modeChange, "1") - self.update_state(code, "stairwellFunction", stairwellFunction, "1") - self.update_state(code, "stairwellTime", stairwellTime, "1") - self.update_state(code, "level", level, "1") - self.update_state(code, "state", state, "1") + self.update_state(hash, "sunMode", sunMode, "1") + self.update_state(hash, "timeAutomatic", timerAuto, "1") + self.update_state(hash, "sunAutomatic", sunAuto, "1") + self.update_state(hash, "dawnAutomatic", dawnAuto, "1") + self.update_state(hash, "duskAutomatic", duskAuto, "1") + self.update_state(hash, "manualMode", manualMode, "1") + self.update_state(hash, "modeChange", modeChange, "1") + self.update_state(hash, "stairwellFunction", stairwellFunction, "1") + self.update_state(hash, "stairwellTime", stairwellTime, "1") + self.update_state(hash, "level", level, "1") + self.update_state(hash, "state", state, "1") readingsEndUpdate(hash, 1) if module_definition02: @@ -273,17 +283,17 @@ def parse(self, msg): state = "on" if (level == 100) else level readingsBeginUpdate(hash) - self.update_state(code, "sunMode", sunMode, "1") - self.update_state(code, "timeAutomatic", timerAuto, "1") - self.update_state(code, "sunAutomatic", sunAuto, "1") - self.update_state(code, "dawnAutomatic", dawnAuto, "1") - self.update_state(code, "duskAutomatic", duskAuto, "1") - self.update_state(code, "manualMode", manualMode, "1") - self.update_state(code, "modeChange", modeChange, "1") - self.update_state(code, "stairwellFunction", stairwellFunction, "1") - self.update_state(code, "stairwellTime", stairwellTime, "1") - self.update_state(code, "level", level, "1") - self.update_state(code, "state", state, "1") + self.update_state(hash, "sunMode", sunMode, "1") + self.update_state(hash, "timeAutomatic", timerAuto, "1") + self.update_state(hash, "sunAutomatic", sunAuto, "1") + self.update_state(hash, "dawnAutomatic", dawnAuto, "1") + self.update_state(hash, "duskAutomatic", duskAuto, "1") + self.update_state(hash, "manualMode", manualMode, "1") + self.update_state(hash, "modeChange", modeChange, "1") + self.update_state(hash, "stairwellFunction", stairwellFunction, "1") + self.update_state(hash, "stairwellTime", stairwellTime, "1") + self.update_state(hash, "level", level, "1") + self.update_state(hash, "state", state, "1") readingsEndUpdate(hash, 1) # Notify is done by Dispatch elif format == "23": pos = int(msg[22:22 + 2], 16) & 0x7F @@ -318,47 +328,47 @@ def parse(self, msg): state = "closed" if (pos == 100) else state readingsBeginUpdate(hash) - self.update_state(code, "ventilatingPosition", ventPos, "1") - self.update_state(code, "ventilatingMode", ventMode, "1") - self.update_state(code, "sunPosition", sunPos, "1") - self.update_state(code, "sunMode", sunMode, "1") - self.update_state(code, "timeAutomatic", timerAuto, "1") - self.update_state(code, "sunAutomatic", sunAuto, "1") - self.update_state(code, "dawnAutomatic", dawnAuto, "1") - self.update_state(code, "duskAutomatic", duskAuto, "1") - self.update_state(code, "manualMode", manualMode, "1") - self.update_state(code, "windAutomatic", windAuto, "1") - self.update_state(code, "windMode", windMode, "1") - self.update_state(code, "windDirection", windDir, "1") - self.update_state(code, "rainAutomatic", rainAuto, "1") - self.update_state(code, "rainMode", rainMode, "1") - self.update_state(code, "rainDirection", rainDir, "1") - self.update_state(code, "runningTime", runningTime, "1") - self.update_state(code, "motorDeadTime", deadTimes[deadTime], "1") - self.update_state(code, "position", pos, "1") - self.update_state(code, "reversal", reversal, "1") - self.update_state(code, "blindsMode", blindsMode, "1") + self.update_state(hash, "ventilatingPosition", ventPos, "1") + self.update_state(hash, "ventilatingMode", ventMode, "1") + self.update_state(hash, "sunPosition", sunPos, "1") + self.update_state(hash, "sunMode", sunMode, "1") + self.update_state(hash, "timeAutomatic", timerAuto, "1") + self.update_state(hash, "sunAutomatic", sunAuto, "1") + self.update_state(hash, "dawnAutomatic", dawnAuto, "1") + self.update_state(hash, "duskAutomatic", duskAuto, "1") + self.update_state(hash, "manualMode", manualMode, "1") + self.update_state(hash, "windAutomatic", windAuto, "1") + self.update_state(hash, "windMode", windMode, "1") + self.update_state(hash, "windDirection", windDir, "1") + self.update_state(hash, "rainAutomatic", rainAuto, "1") + self.update_state(hash, "rainMode", rainMode, "1") + self.update_state(hash, "rainDirection", rainDir, "1") + self.update_state(hash, "runningTime", runningTime, "1") + self.update_state(hash, "motorDeadTime", deadTimes[deadTime], "1") + self.update_state(hash, "position", pos, "1") + self.update_state(hash, "reversal", reversal, "1") + self.update_state(hash, "blindsMode", blindsMode, "1") # not tested yet if blindsMode == "on": # pragma: no cover - self.update_state(code, "tiltInSunPos", tiltInSunPos, "1") - self.update_state(code, "tiltInVentPos", tiltInVentPos, "1") - self.update_state(code, "tiltAfterMoveLevel", tiltAfterMoveLevel, "1") - self.update_state(code, "tiltAfterStopDown", tiltAfterStopDown, "1") - self.update_state(code, "module_definitionaultSlatPos", module_definitionaultSlatPos, "1") - self.update_state(code, "slatRunTime", slatRunTime, "1") - self.update_state(code, "slatPosition", slatPosition, "1") + self.update_state(hash, "tiltInSunPos", tiltInSunPos, "1") + self.update_state(hash, "tiltInVentPos", tiltInVentPos, "1") + self.update_state(hash, "tiltAfterMoveLevel", tiltAfterMoveLevel, "1") + self.update_state(hash, "tiltAfterStopDown", tiltAfterStopDown, "1") + self.update_state(hash, "module_definitionaultSlatPos", module_definitionaultSlatPos, "1") + self.update_state(hash, "slatRunTime", slatRunTime, "1") + self.update_state(hash, "slatPosition", slatPosition, "1") else: - self.delete_state(code, 'tiltInSunPos') - self.delete_state(code, 'tiltInVentPos') - self.delete_state(code, 'tiltAfterMoveLevel') - self.delete_state(code, 'tiltAfterStopDown') - self.delete_state(code, 'module_definitionaultSlatPos') - self.delete_state(code, 'slatRunTime') - self.delete_state(code, 'slatPosition') - - self.update_state(code, "moving", "stop", "1") - self.update_state(code, "state", state, "1") + self.delete_state(hash, 'tiltInSunPos') + self.delete_state(hash, 'tiltInVentPos') + self.delete_state(hash, 'tiltAfterMoveLevel') + self.delete_state(hash, 'tiltAfterStopDown') + self.delete_state(hash, 'module_definitionaultSlatPos') + self.delete_state(hash, 'slatRunTime') + self.delete_state(hash, 'slatPosition') + + self.update_state(hash, "moving", "stop", "1") + self.update_state(hash, "state", state, "1") readingsEndUpdate(hash, 1) # Notify is done by Dispatch # Rohrmotor, SX5 -- not tested yet elif format == "24": # pragma: no cover @@ -397,36 +407,36 @@ def parse(self, msg): state = "block" if (block == "1") else pos readingsBeginUpdate(hash) - self.update_state(code, "manualMode", manualMode, "1") - self.update_state(code, "timeAutomatic", timerAuto, "1") - self.update_state(code, "ventilatingPosition", ventPos, "1") - self.update_state(code, "ventilatingMode", ventMode, "1") - self.update_state(code, "position", pos, "1") - self.update_state(code, "state", state, "1") - self.update_state(code, "obstacle", obstacle, "1") - self.update_state(code, "block", block, "1") - self.update_state(code, "moving", "stop", "1") + self.update_state(hash, "manualMode", manualMode, "1") + self.update_state(hash, "timeAutomatic", timerAuto, "1") + self.update_state(hash, "ventilatingPosition", ventPos, "1") + self.update_state(hash, "ventilatingMode", ventMode, "1") + self.update_state(hash, "position", pos, "1") + self.update_state(hash, "state", state, "1") + self.update_state(hash, "obstacle", obstacle, "1") + self.update_state(hash, "block", block, "1") + self.update_state(hash, "moving", "stop", "1") if code[0:2] == "4E": # SX5 - self.update_state(code, "10minuteAlarm", alert10, "1") - self.update_state(code, "automaticClosing", closingTimes['autoClose'], "1") - self.update_state(code, "2000cycleAlarm", alert2000, "1") - self.update_state(code, "openSpeed", openSpeeds['openSpeed'], "1") - self.update_state(code, "backJump", backJump, "1") - self.update_state(code, "lightCurtain", lightCurtain, "1") + self.update_state(hash, "10minuteAlarm", alert10, "1") + self.update_state(hash, "automaticClosing", closingTimes['autoClose'], "1") + self.update_state(hash, "2000cycleAlarm", alert2000, "1") + self.update_state(hash, "openSpeed", openSpeeds['openSpeed'], "1") + self.update_state(hash, "backJump", backJump, "1") + self.update_state(hash, "lightCurtain", lightCurtain, "1") else: - self.update_state(code, "sunPosition", sunPos, "1") - self.update_state(code, "sunMode", sunMode, "1") - self.update_state(code, "sunAutomatic", sunAuto, "1") - self.update_state(code, "dawnAutomatic", dawnAuto, "1") - self.update_state(code, "duskAutomatic", duskAuto, "1") - self.update_state(code, "windAutomatic", windAuto, "1") - self.update_state(code, "windMode", windMode, "1") - self.update_state(code, "windDirection", windDir, "1") - self.update_state(code, "rainAutomatic", rainAuto, "1") - self.update_state(code, "rainMode", rainMode, "1") - self.update_state(code, "rainDirection", rainDir, "1") - self.update_state(code, "reversal", reversal, "1") + self.update_state(hash, "sunPosition", sunPos, "1") + self.update_state(hash, "sunMode", sunMode, "1") + self.update_state(hash, "sunAutomatic", sunAuto, "1") + self.update_state(hash, "dawnAutomatic", dawnAuto, "1") + self.update_state(hash, "duskAutomatic", duskAuto, "1") + self.update_state(hash, "windAutomatic", windAuto, "1") + self.update_state(hash, "windMode", windMode, "1") + self.update_state(hash, "windDirection", windDir, "1") + self.update_state(hash, "rainAutomatic", rainAuto, "1") + self.update_state(hash, "rainMode", rainMode, "1") + self.update_state(hash, "rainDirection", rainDir, "1") + self.update_state(hash, "reversal", reversal, "1") readingsEndUpdate(hash, 1) @@ -452,21 +462,21 @@ def parse(self, msg): state = "on" if (level == 100) else level readingsBeginUpdate(hash) - self.update_state(code, "stairwellFunction", stairwellFunction, "1") - self.update_state(code, "stairwellTime", stairwellTime, "1") - self.update_state(code, "timeAutomatic", timerAuto, "1") - self.update_state(code, "duskAutomatic", duskAuto, "1") - self.update_state(code, "sunAutomatic", sunAuto, "1") - self.update_state(code, "sunMode", sunMode, "1") - self.update_state(code, "manualMode", manualMode, "1") - self.update_state(code, "dawnAutomatic", dawnAuto, "1") - self.update_state(code, "saveIntermediateOnStop", intemedSave, "1") - self.update_state(code, "runningTime", runningTime, "1") - self.update_state(code, "intermediateValue", intemedVal, "1") - self.update_state(code, "intermediateMode", intermedMode, "1") - self.update_state(code, "level", level, "1") - self.update_state(code, "modeChange", modeChange, "1") - self.update_state(code, "state", state, "1") + self.update_state(hash, "stairwellFunction", stairwellFunction, "1") + self.update_state(hash, "stairwellTime", stairwellTime, "1") + self.update_state(hash, "timeAutomatic", timerAuto, "1") + self.update_state(hash, "duskAutomatic", duskAuto, "1") + self.update_state(hash, "sunAutomatic", sunAuto, "1") + self.update_state(hash, "sunMode", sunMode, "1") + self.update_state(hash, "manualMode", manualMode, "1") + self.update_state(hash, "dawnAutomatic", dawnAuto, "1") + self.update_state(hash, "saveIntermediateOnStop", intemedSave, "1") + self.update_state(hash, "runningTime", runningTime, "1") + self.update_state(hash, "intermediateValue", intemedVal, "1") + self.update_state(hash, "intermediateMode", intermedMode, "1") + self.update_state(hash, "level", level, "1") + self.update_state(hash, "modeChange", modeChange, "1") + self.update_state(hash, "state", state, "1") readingsEndUpdate(hash, 1) # Notify is done by Dispatch # Thermostat -- not tested yet @@ -487,20 +497,20 @@ def parse(self, msg): state = "T: temperature1 desired: desiredTemp" readingsBeginUpdate(hash) - self.update_state(code, "measured-temp", temperature1, "1") - self.update_state(code, "measured-temp2", temperature2, "1") - self.update_state(code, "temperatureThreshold1", tempThreshold1, "1") - self.update_state(code, "temperatureThreshold2", tempThreshold2, "1") - self.update_state(code, "temperatureThreshold3", tempThreshold3, "1") - self.update_state(code, "temperatureThreshold4", tempThreshold4, "1") - self.update_state(code, "desired-temp", desiredTemp, "1") - self.update_state(code, "output", output, "1") - self.update_state(code, "manualOverride", manualOverride, "1") - self.update_state(code, "actTempLimit", actTempLimit, "1") - self.update_state(code, "timeAutomatic", timerAuto, "1") - self.update_state(code, "manualMode", manualMode, "1") - - self.update_state(code, "state", state, "1") + self.update_state(hash, "measured-temp", temperature1, "1") + self.update_state(hash, "measured-temp2", temperature2, "1") + self.update_state(hash, "temperatureThreshold1", tempThreshold1, "1") + self.update_state(hash, "temperatureThreshold2", tempThreshold2, "1") + self.update_state(hash, "temperatureThreshold3", tempThreshold3, "1") + self.update_state(hash, "temperatureThreshold4", tempThreshold4, "1") + self.update_state(hash, "desired-temp", desiredTemp, "1") + self.update_state(hash, "output", output, "1") + self.update_state(hash, "manualOverride", manualOverride, "1") + self.update_state(hash, "actTempLimit", actTempLimit, "1") + self.update_state(hash, "timeAutomatic", timerAuto, "1") + self.update_state(hash, "manualMode", manualMode, "1") + + self.update_state(hash, "state", state, "1") readingsEndUpdate(hash, 1) else: @@ -541,18 +551,18 @@ def parse(self, msg): for chan in chans: if id[2:4] in ("1a", "18", "19", "01", "02", "03"): if (id[2:4] == "1a") or (id[0:2] == "0e") or (code[0:2] in ("a0", "a2")): - self.update_state(code, "state", sensorMsg[id]['state'] + "." + chan, "1") + self.update_state(hash, "state", sensorMsg[id]['state'] + "." + chan, "1") else: - self.update_state(code, "state", sensorMsg[id]['state'], "1") + self.update_state(hash, "state", sensorMsg[id]['state'], "1") - self.update_state(code, "channelchan", sensorMsg[id]['name'], "1") + self.update_state(hash, "channelchan", sensorMsg[id]['name'], "1") else: if (code[0:2] not in ("69", "73")) or (id[2:4] in ("11", "12")): chan = "" if code[0:2] in ("65", "a5", "aa", "ab"): - self.update_state(code, "state", sensorMsg[id]['state'], "1") + self.update_state(hash, "state", sensorMsg[id]['state'], "1") - self.update_state(code, "event", sensorMsg[id]['name'] + "." + chan, "1") + self.update_state(hash, "event", sensorMsg[id]['name'] + "." + chan, "1") DoTrigger(hash["name"], sensorMsg[id][name] + "." + chan) @@ -580,13 +590,13 @@ def parse(self, msg): state += " B: ".format(brightness) readingsBeginUpdate(hash) - self.update_state(code, "brightness", brightness, "1") - self.update_state(code, "sunDirection", sunDirection, "1") - self.update_state(code, "sunHeight", sunHeight, "1") - self.update_state(code, "temperature", temperature, "1") - self.update_state(code, "isRaining", isRaining, "1") - self.update_state(code, "state", state, "1") - self.update_state(code, "wind", wind, "1") + self.update_state(hash, "brightness", brightness, "1") + self.update_state(hash, "sunDirection", sunDirection, "1") + self.update_state(hash, "sunHeight", sunHeight, "1") + self.update_state(hash, "temperature", temperature, "1") + self.update_state(hash, "isRaining", isRaining, "1") + self.update_state(hash, "state", state, "1") + self.update_state(hash, "wind", wind, "1") readingsEndUpdate(hash, 1) # Notify is done by Dispatch # Umweltsensor Zeit @@ -606,8 +616,8 @@ def parse(self, msg): second = msg[24:24 + 2] readingsBeginUpdate(hash) - self.update_state(code, "date", "20" + str(year) + "-" + str(month) + "-" + str(day), "1") - self.update_state(code, "time", str(hour) + ":" + str(minute) + ":" + str(second), "1") + self.update_state(hash, "date", "20" + str(year) + "-" + str(month) + "-" + str(day), "1") + self.update_state(hash, "time", str(hour) + ":" + str(minute) + ":" + str(second), "1") readingsEndUpdate(hash, 1) # Notify is done by Dispatch # Umweltsensor Konfiguration @@ -623,8 +633,8 @@ def parse(self, msg): hash = module_definition01 del hash['READINGS']['configModified'] - self.update_state(code, ".regreg", "regVal", "1") - # self.update_state(code, "regreg", "regVal", "1") + self.update_state(hash, ".regreg", "regVal", "1") + # self.update_state(hash, "regreg", "regVal", "1") DUOFERN_DecodeWeatherSensorConfig(hash) # Rauchmelder Batterie @@ -633,8 +643,8 @@ def parse(self, msg): batteryLevel = int(msg[8:8 + 2], 16) readingsBeginUpdate(hash) - self.update_state(code, "battery", battery, "1") - self.update_state(code, "batteryLevel", batteryLevel, "1") + self.update_state(hash, "battery", battery, "1") + self.update_state(hash, "batteryLevel", batteryLevel, "1") readingsEndUpdate(hash, 1) # Notify is done by Dispatch # ACK, Befehl vom Aktor empfangen @@ -646,7 +656,7 @@ def parse(self, msg): # NACK, Befehl nicht vom Aktor empfangen elif msg[0:8] == "810108aa": logger.warning("missing ack for {}".format(hash)) - # self.update_state(code, "state", "MISSING ACK", "1") + # self.update_state(hash, "state", "MISSING ACK", "1") # foreach (grep (/^channel_/, keys%{hash})){ # chnHash = module_definitions{hash->{_}} # readingsSingleUpdate(chnHash, "state", "MISSING ACK", 1) @@ -1025,28 +1035,28 @@ def set(self, code, cmd, *args): if cmd in ('up', 'down', 'toggle'): if toggleUpDown: cmd = "stop" - # self.update_state(code,"moving","moving") + # self.update_state(hash,"moving","moving") if ((cmd == "toggle") and (position > -1)): - self.update_state(code, "moving", "moving", 1) + self.update_state(hash, "moving", "moving", 1) if ((cmd == "dawn") and (dawnAutomatic == "on") and (position > 0)): - self.update_state(code, "moving", "up", 1) + self.update_state(hash, "moving", "up", 1) if ((cmd == "dusk") and (duskAutomatic == "on") and (position < 100) and (position > -1)): - self.update_state(code, "moving", "down", 1) + self.update_state(hash, "moving", "down", 1) if timer == "00" or timeAutomatic == "on": if ((cmd == "up") and (position > 0)): - self.update_state(code, "moving", "up", 1) + self.update_state(hash, "moving", "up", 1) if ((cmd == "down") and (position < 100) and (position > -1)): - self.update_state(code, "moving", "down", 1) + self.update_state(hash, "moving", "down", 1) if cmd == "position": if arg > position: - self.update_state(code, "moving", "down", 1) + self.update_state(hash, "moving", "down", 1) elif (arg < position): - self.update_state(code, "moving", "up", 1) + self.update_state(hash, "moving", "up", 1) else: - self.update_state(code, "moving", "stop", 1) + self.update_state(hash, "moving", "stop", 1) command = commands[cmd][subCmd] From 938510d491e8f2cb9042695bd74af26b240c7b4f Mon Sep 17 00:00:00 2001 From: Florian Date: Sun, 22 Dec 2019 23:56:02 +0100 Subject: [PATCH 04/31] avoid channel-specific devices during SetPairs Since dual actors are now represented as three devices, but two are not actually pairable devices, avoid to SetPairs for these devices. --- pyduofern/duofern_stick.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/pyduofern/duofern_stick.py b/pyduofern/duofern_stick.py index 7e2e165..3da0a77 100644 --- a/pyduofern/duofern_stick.py +++ b/pyduofern/duofern_stick.py @@ -415,9 +415,14 @@ def handshake(self): if 'devices' in self.config and self.config['devices']: counter = 0 for device in self.config['devices']: - hex_to_write = duoSetPairs.replace('nn', '{:02X}'.format(counter)).replace('yyyyyy', device['id']) - yield from send_and_await_reply(self, hex_to_write, "SetPairs") - yield from self.send(duoACK) + # devices with id other than 6 characters + # are sub-devices of another device with 6 characters + # (i.e. devices representing a single channel) + # and thus do not require SetPairs + if len(device['id']) == 6: + hex_to_write = duoSetPairs.replace('nn', '{:02X}'.format(counter)).replace('yyyyyy', device['id']) + yield from send_and_await_reply(self, hex_to_write, "SetPairs") + yield from self.send(duoACK) counter += 1 self.duofern_parser.add_device(device['id'], device['name']) @@ -495,13 +500,18 @@ def _initialize(self): # DoInit if "devices" in self.config: counter = 0 for device in self.config['devices']: - hex_to_write = duoSetPairs.replace('nn', '{:02X}'.format(counter)).replace('yyyyyy', device['id']) - self._simple_write(hex_to_write) - try: - self._read_answer("SetPairs") - except DuofernTimeoutException: # pragma: no cover - continue - self._simple_write(duoACK) + # devices with id other than 6 characters + # are sub-devices of another device with 6 characters + # (i.e. devices representing a single channel) + # and thus do not require SetPairs + if len(device['id']) == 6: + hex_to_write = duoSetPairs.replace('nn', '{:02X}'.format(counter)).replace('yyyyyy', device['id']) + self._simple_write(hex_to_write) + try: + self._read_answer("SetPairs") + except DuofernTimeoutException: # pragma: no cover + continue + self._simple_write(duoACK) counter += 1 self.duofern_parser.add_device(device['id'], device['name']) From 48e8f6efc0b1f14357cb52b183b994cf96a7240d Mon Sep 17 00:00:00 2001 From: Florian Date: Sun, 22 Dec 2019 23:58:56 +0100 Subject: [PATCH 05/31] select correct device when executing command After representing a dual actor as three devices, it is important to select the correct one when executing a command in order to select the correct chanNo. The code itself in the current implementation of the command function does not contain the channel suffix. --- pyduofern/duofern.py | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/pyduofern/duofern.py b/pyduofern/duofern.py index 6231f9b..83f4f22 100644 --- a/pyduofern/duofern.py +++ b/pyduofern/duofern.py @@ -678,7 +678,7 @@ def send(self, cmd): yield from self.send_hook(cmd) @asyncio.coroutine - def set(self, code, cmd, *args): + def set(self, device_id, cmd, *args): # my (hash, @a) = @_ # b = @a @@ -688,8 +688,8 @@ def set(self, code, cmd, *args): # cmd = shift @a arg = args[0] if len(args) >= 1 else None arg2 = args[1] if len(args) > 1 else None - code = code[0:0 + 6] - name = self.modules['by_code'][code]['name'] + code = device_id[0:0 + 6] + name = self.modules['by_code'][device_id]['name'] # sets @@ -722,7 +722,7 @@ def set(self, code, cmd, *args): if code[0:2] in ("65", "74") and len(code) >= 8 and code[6:8] == "01": sets = merge_dicts(setsSwitchActor) # if (code =~ /^(65|74)....01/) - blindsMode = "off" if not "blindsMode" in self.modules['by_code'][code] else self.modules['by_code'][code] + blindsMode = "off" if not "blindsMode" in self.modules['by_code'][device_id] else self.modules['by_code'][device_id] if (blindsMode == "on"): sets = merge_dicts(sets, setsBlinds) @@ -735,10 +735,10 @@ def set(self, code, cmd, *args): return None elif cmd == "clear": - keys = self.modules['by_code'][code].keys() + keys = self.modules['by_code'][device_id].keys() for key in keys: if key != 'name': - self.modules['by_code'][code].__delitem__(key) + self.modules['by_code'][device_id].__delitem__(key) return None # cH = (hash) # delete _->{READINGS} foreach (@cH) @@ -753,8 +753,8 @@ def set(self, code, cmd, *args): elif cmd == "writeConfig": for x in range(0, 8): # for(my x=0; x<8; x++) { - regV = "00000000000000000000" if not ".reg{}".format(x) in self.modules['by_code'][code] else \ - self.modules['by_code'][code][".reg{}".format(x)] + regV = "00000000000000000000" if not ".reg{}".format(x) in self.modules['by_code'][device_id] else \ + self.modules['by_code'][device_id][".reg{}".format(x)] reg = "%02x" % (x + 0x81) buf = duoWeatherWriteConfig buf = buf.replace("yyyyyy", code) @@ -763,8 +763,8 @@ def set(self, code, cmd, *args): yield from self.send(buf) self.send(buf) - if "configModified" in self.modules['by_code'][code]: - self.modules['by_code'][code].__delitem__("configModified") + if "configModified" in self.modules['by_code'][device_id]: + self.modules['by_code'][device_id].__delitem__("configModified") # delete hash->{READINGS}{configModified} return None @@ -935,8 +935,8 @@ def set(self, code, cmd, *args): buf = duoCommand command = None - if 'chanNo' in self.modules['by_code'][code]: - chanNo = self.modules['by_code'][code]['chanNo'] + if 'chanNo' in self.modules['by_code'][device_id]: + chanNo = self.modules['by_code'][device_id]['chanNo'] # chanNo = hash->{chanNo} if (hash->{chanNo}) if 'noArg' in commands[cmd]: @@ -1017,19 +1017,19 @@ def set(self, code, cmd, *args): if subCmd not in commands[cmd]: raise Exception("Wrong argument {}, {}".format(arg, subCmd)) - position = -1 if not "position" in self.modules['by_code'][code] else self.modules['by_code'][code][ + position = -1 if not "position" in self.modules['by_code'][device_id] else self.modules['by_code'][device_id][ "position"] # toggleUpDown = AttrVal(name, "toggleUpDown", "0") - toggleUpDown = self.modules['by_code'][code]['toggleUpDown'] if 'toggleUpDown' in self.modules['by_code'][ - code] else 0 - moving = "stop" if not "moving" in self.modules['by_code'][code] else self.modules['by_code'][code][ + toggleUpDown = self.modules['by_code'][device_id]['toggleUpDown'] if 'toggleUpDown' in self.modules['by_code'][ + device_id] else 0 + moving = "stop" if not "moving" in self.modules['by_code'][device_id] else self.modules['by_code'][device_id][ "moving"] - timeAutomatic = "on" if not "timeAutomatic" in self.modules['by_code'][code] else \ - self.modules['by_code'][code]["timeAutomatic"] - dawnAutomatic = "on" if not "dawnAutomatic" in self.modules['by_code'][code] else \ - self.modules['by_code'][code]["dawnAutomatic"] - duskAutomatic = "on" if not "duskAutomatic" in self.modules['by_code'][code] else \ - self.modules['by_code'][code]["duskAutomatic"] + timeAutomatic = "on" if not "timeAutomatic" in self.modules['by_code'][device_id] else \ + self.modules['by_code'][device_id]["timeAutomatic"] + dawnAutomatic = "on" if not "dawnAutomatic" in self.modules['by_code'][device_id] else \ + self.modules['by_code'][device_id]["dawnAutomatic"] + duskAutomatic = "on" if not "duskAutomatic" in self.modules['by_code'][device_id] else \ + self.modules['by_code'][device_id]["duskAutomatic"] if moving != "stop": if cmd in ('up', 'down', 'toggle'): From 6ce649fff40f0ad04b28dfb611a8ebbc5ecf8fa0 Mon Sep 17 00:00:00 2001 From: Florian Date: Sun, 22 Dec 2019 23:39:23 +0100 Subject: [PATCH 06/31] only add channel specific devices for dual actors --- examples/homeassistant/custom_components/duofern/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/homeassistant/custom_components/duofern/light.py b/examples/homeassistant/custom_components/duofern/light.py index 693553b..1cfc6cb 100644 --- a/examples/homeassistant/custom_components/duofern/light.py +++ b/examples/homeassistant/custom_components/duofern/light.py @@ -32,7 +32,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): # Add devices to_add = [DuofernLight(device['id'], device['name'], stick, hass) for device in stick.config['devices'] if - (device['id'].startswith('46') or device['id'].startswith('43')) and not device['id'] in hass.data[DOMAIN]['devices'].keys()] + (device['id'].startswith('46') or (device['id'].startswith('43') and len(device['id']) == 8)) and not device['id'] in hass.data[DOMAIN]['devices'].keys()] add_devices(to_add) From fd561d17c752c2fa12c50254012021ceee63938e Mon Sep 17 00:00:00 2001 From: paulg Date: Wed, 1 Jan 2020 22:53:12 +0100 Subject: [PATCH 07/31] make "channel" explicit for multichannel actors (caveat: homeassistant code not yet adapted) --- pyduofern/duofern.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyduofern/duofern.py b/pyduofern/duofern.py index 83f4f22..47c4118 100644 --- a/pyduofern/duofern.py +++ b/pyduofern/duofern.py @@ -145,6 +145,7 @@ def parse(self, msg): hash = module_definition name = hash['name'] + channel = None if name in self.ignore_devices: return name @@ -201,6 +202,7 @@ def parse(self, msg): if module_definition01: hash = module_definition01 + channel = "01" # RolloTron if format == "21": @@ -267,6 +269,7 @@ def parse(self, msg): if module_definition02: hash = module_definition02 + channel = "02" level = int(msg[20:20 + 2], 16) & 0x7F modeChange = "on" if int(msg[20:20 + 2], 16) & 0x80 else "off" sunMode = "on" if int(msg[12:12 + 2], 16) & 0x10 else "off" @@ -547,6 +550,7 @@ def parse(self, msg): if (module_definition01): hash = module_definition01 + channel = "01" for chan in chans: if id[2:4] in ("1a", "18", "19", "01", "02", "03"): @@ -575,6 +579,7 @@ def parse(self, msg): module_definition01 = self.modules['by_code'][code + "00"] hash = module_definition01 + channel = "01" brightnessExp = 1000 if int(msg[8:8 + 4], 16) & 0x0400 else 1 brightness = (int(msg[8:8 + 4], 16) & 0x01FF) * brightnessExp @@ -607,6 +612,7 @@ def parse(self, msg): module_definition01 = self.modules['by_code'][code + "00"] hash = module_definition01 + channel = "01" year = msg[12:12 + 2] month = msg[14:14 + 2] @@ -631,6 +637,7 @@ def parse(self, msg): module_definition01 = self.modules['by_code'][code + "00"] hash = module_definition01 + channel = "01" del hash['READINGS']['configModified'] self.update_state(hash, ".regreg", "regVal", "1") @@ -761,7 +768,6 @@ def set(self, device_id, cmd, *args): buf = buf.replace("rr", reg) buf = buf.replace("nnnnnnnnnnnnnnnnnnnn", regV) yield from self.send(buf) - self.send(buf) if "configModified" in self.modules['by_code'][device_id]: self.modules['by_code'][device_id].__delitem__("configModified") From 9c9be2a663e6ce88b3636747838806e282e9e288 Mon Sep 17 00:00:00 2001 From: paulg Date: Wed, 1 Jan 2020 23:13:21 +0100 Subject: [PATCH 08/31] hash -> code --- pyduofern/duofern.py | 410 ++++++++++++++++++++++--------------------- 1 file changed, 206 insertions(+), 204 deletions(-) diff --git a/pyduofern/duofern.py b/pyduofern/duofern.py index 47c4118..ef32191 100644 --- a/pyduofern/duofern.py +++ b/pyduofern/duofern.py @@ -96,11 +96,9 @@ def __init__(self, send_hook=None, asyncio=False, changes_callback=None): def add_device(self, code, name=None): if name is None: - name = code + name = len(self.modules['by_code']) logger.debug("adding {}".format(code)) self.modules['by_code'][code] = {'name': name} - if len(code) == 8: - self.modules['by_code'][code]['chanNo'] = code[6:] def del_device(self, code, name=None): if name is None: @@ -109,14 +107,26 @@ def del_device(self, code, name=None): if code in self.modules['by_code']: del self.modules['by_code'][code] - def update_state(self, hash, key, value, trigger=None): - hash[key] = value - if self.changes_callback: + def update_state(self, code, key, value, trigger=None, channel=None): + """ + + :param code: duofern system code + :param key: some arbitrary key that should be set in the state dict + :param value: the corresponding value + :param trigger: whether or not to trigger + :param channel: if this is a multichannel actor: The channel the key should be set for + :return: + """ + if channel is None: + self.modules['by_code'][code][key] = value + else: + self.modules['by_code'][code][key+"_"+channel] = value + if self.changes_callback and trigger: self.changes_callback() - def delete_state(self, hash, key): - if key in hash: - del hash[key] + def delete_state(self, code, key): + if key in self.modules['by_code'][code]: + del self.modules['by_code'][code][key] def parse(self, msg): code = msg[30:36] @@ -152,15 +162,15 @@ def parse(self, msg): # Device paired if msg[0:4] == "0602": - self.update_state(hash, "state", "paired", "1") + self.update_state(code, "state", "paired", "1", channel=channel) # del hash['READINGS']['unpaired'] logger.info("DUOFERN device paired, ID {}".format(code)) # Device unpaired elif (msg[0:4] == "0603"): readingsBeginUpdate(hash) - self.update_state(hash, "unpaired", 1, "1") - self.update_state(hash, "state", "unpaired", "1") + self.update_state(code, "unpaired", 1, "1", channel=channel) + self.update_state(code, "state", "unpaired", "1", channel=channel) self.del_device(code) # readingsEndUpdate(hash, 1) # Notify is done by Dispatch logger.warning("DUOFERN device unpaired, code {}".format(code)) @@ -170,14 +180,14 @@ def parse(self, msg): format = msg[6:6 + 2] ver = msg[24:24 + 1] + msg[25:25 + 1] - self.update_state(hash, "version", ver, "0") + self.update_state(code, "version", ver, "0", channel=channel) # RemoveInternalTimer(hash) # del hash['helper']['timeout'] # Bewegungsmelder, Wettersensor, Mehrfachwandtaster not tested yet if code[0:2] in ("65", "69", "74"): # pragma: no cover - self.update_state(hash, "state", "OK", "1") + self.update_state(code, "state", "OK", "1", channel=channel) module_definition01 = self.modules['by_code'][code + "01"] if not module_definition01: DoTrigger("global", "UNDEFINED DUOFERN_code_actor DUOFERN code01") @@ -185,20 +195,12 @@ def parse(self, msg): # Universalaktor -- not tested yet elif code[0:2] == "43": # pragma: no cover - self.update_state(hash, "state", "OK", "1") - try: - module_definition01 = self.modules['by_code'][code + "01"] - except KeyError: - self.add_device(code + "01") - logger.info("detected unknown device, ID={}".format(code + "01")) - module_definition01 = self.modules['by_code'][code + "01"] + self.update_state(code, "state", "OK", "1", channel=channel) + module_definition01 = self.modules['by_code'][code] + if not module_definition01: + DoTrigger("global", "UNDEFINED DUOFERN_code+_01 DUOFERN code+01") - try: - module_definition02 = self.modules['by_code'][code + "02"] - except KeyError: - self.add_device(code + "02") - logger.info("detected unknown device, ID={}".format(code + "02")) - module_definition02 = self.modules['by_code'][code + "02"] + module_definition02 = None if module_definition01: hash = module_definition01 @@ -222,18 +224,18 @@ def parse(self, msg): state = "closed" if (pos == 100) else pos readingsBeginUpdate(hash) - self.update_state(hash, "ventilatingPosition", ventPos, "1") - self.update_state(hash, "ventilatingMode", ventMode, "1") - self.update_state(hash, "sunPosition", sunPos, "1") - self.update_state(hash, "sunMode", sunMode, "1") - self.update_state(hash, "timeAutomatic", timerAuto, "1") - self.update_state(hash, "sunAutomatic", sunAuto, "1") - self.update_state(hash, "dawnAutomatic", dawnAuto, "1") - self.update_state(hash, "duskAutomatic", duskAuto, "1") - self.update_state(hash, "manualMode", manualMode, "1") - self.update_state(hash, "position", pos, "1") - self.update_state(hash, "state", state, "1") - self.update_state(hash, "moving", "stop", "1") + self.update_state(code, "ventilatingPosition", ventPos, "1", channel=channel) + self.update_state(code, "ventilatingMode", ventMode, "1", channel=channel) + self.update_state(code, "sunPosition", sunPos, "1", channel=channel) + self.update_state(code, "sunMode", sunMode, "1", channel=channel) + self.update_state(code, "timeAutomatic", timerAuto, "1", channel=channel) + self.update_state(code, "sunAutomatic", sunAuto, "1", channel=channel) + self.update_state(code, "dawnAutomatic", dawnAuto, "1", channel=channel) + self.update_state(code, "duskAutomatic", duskAuto, "1", channel=channel) + self.update_state(code, "manualMode", manualMode, "1", channel=channel) + self.update_state(code, "position", pos, "1", channel=channel) + self.update_state(code, "state", state, "1", channel=channel) + self.update_state(code, "moving", "stop", "1", channel=channel) readingsEndUpdate(hash, 1) # Notify is done by Dispatch # Universal Aktor, Steckdosenaktor, Troll Comfort DuoFern (Lichtmodus) not tested yet @@ -254,17 +256,17 @@ def parse(self, msg): state = "on" if (level == 100) else level readingsBeginUpdate(hash) - self.update_state(hash, "sunMode", sunMode, "1") - self.update_state(hash, "timeAutomatic", timerAuto, "1") - self.update_state(hash, "sunAutomatic", sunAuto, "1") - self.update_state(hash, "dawnAutomatic", dawnAuto, "1") - self.update_state(hash, "duskAutomatic", duskAuto, "1") - self.update_state(hash, "manualMode", manualMode, "1") - self.update_state(hash, "modeChange", modeChange, "1") - self.update_state(hash, "stairwellFunction", stairwellFunction, "1") - self.update_state(hash, "stairwellTime", stairwellTime, "1") - self.update_state(hash, "level", level, "1") - self.update_state(hash, "state", state, "1") + self.update_state(code, "sunMode", sunMode, "1", channel=channel) + self.update_state(code, "timeAutomatic", timerAuto, "1", channel=channel) + self.update_state(code, "sunAutomatic", sunAuto, "1", channel=channel) + self.update_state(code, "dawnAutomatic", dawnAuto, "1", channel=channel) + self.update_state(code, "duskAutomatic", duskAuto, "1", channel=channel) + self.update_state(code, "manualMode", manualMode, "1", channel=channel) + self.update_state(code, "modeChange", modeChange, "1", channel=channel) + self.update_state(code, "stairwellFunction", stairwellFunction, "1", channel=channel) + self.update_state(code, "stairwellTime", stairwellTime, "1", channel=channel) + self.update_state(code, "level", level, "1", channel=channel) + self.update_state(code, "state", state, "1", channel=channel) readingsEndUpdate(hash, 1) if module_definition02: @@ -286,17 +288,17 @@ def parse(self, msg): state = "on" if (level == 100) else level readingsBeginUpdate(hash) - self.update_state(hash, "sunMode", sunMode, "1") - self.update_state(hash, "timeAutomatic", timerAuto, "1") - self.update_state(hash, "sunAutomatic", sunAuto, "1") - self.update_state(hash, "dawnAutomatic", dawnAuto, "1") - self.update_state(hash, "duskAutomatic", duskAuto, "1") - self.update_state(hash, "manualMode", manualMode, "1") - self.update_state(hash, "modeChange", modeChange, "1") - self.update_state(hash, "stairwellFunction", stairwellFunction, "1") - self.update_state(hash, "stairwellTime", stairwellTime, "1") - self.update_state(hash, "level", level, "1") - self.update_state(hash, "state", state, "1") + self.update_state(code, "sunMode", sunMode, "1", channel=channel) + self.update_state(code, "timeAutomatic", timerAuto, "1", channel=channel) + self.update_state(code, "sunAutomatic", sunAuto, "1", channel=channel) + self.update_state(code, "dawnAutomatic", dawnAuto, "1", channel=channel) + self.update_state(code, "duskAutomatic", duskAuto, "1", channel=channel) + self.update_state(code, "manualMode", manualMode, "1", channel=channel) + self.update_state(code, "modeChange", modeChange, "1", channel=channel) + self.update_state(code, "stairwellFunction", stairwellFunction, "1", channel=channel) + self.update_state(code, "stairwellTime", stairwellTime, "1", channel=channel) + self.update_state(code, "level", level, "1", channel=channel) + self.update_state(code, "state", state, "1", channel=channel) readingsEndUpdate(hash, 1) # Notify is done by Dispatch elif format == "23": pos = int(msg[22:22 + 2], 16) & 0x7F @@ -331,47 +333,47 @@ def parse(self, msg): state = "closed" if (pos == 100) else state readingsBeginUpdate(hash) - self.update_state(hash, "ventilatingPosition", ventPos, "1") - self.update_state(hash, "ventilatingMode", ventMode, "1") - self.update_state(hash, "sunPosition", sunPos, "1") - self.update_state(hash, "sunMode", sunMode, "1") - self.update_state(hash, "timeAutomatic", timerAuto, "1") - self.update_state(hash, "sunAutomatic", sunAuto, "1") - self.update_state(hash, "dawnAutomatic", dawnAuto, "1") - self.update_state(hash, "duskAutomatic", duskAuto, "1") - self.update_state(hash, "manualMode", manualMode, "1") - self.update_state(hash, "windAutomatic", windAuto, "1") - self.update_state(hash, "windMode", windMode, "1") - self.update_state(hash, "windDirection", windDir, "1") - self.update_state(hash, "rainAutomatic", rainAuto, "1") - self.update_state(hash, "rainMode", rainMode, "1") - self.update_state(hash, "rainDirection", rainDir, "1") - self.update_state(hash, "runningTime", runningTime, "1") - self.update_state(hash, "motorDeadTime", deadTimes[deadTime], "1") - self.update_state(hash, "position", pos, "1") - self.update_state(hash, "reversal", reversal, "1") - self.update_state(hash, "blindsMode", blindsMode, "1") + self.update_state(code, "ventilatingPosition", ventPos, "1", channel=channel) + self.update_state(code, "ventilatingMode", ventMode, "1", channel=channel) + self.update_state(code, "sunPosition", sunPos, "1", channel=channel) + self.update_state(code, "sunMode", sunMode, "1", channel=channel) + self.update_state(code, "timeAutomatic", timerAuto, "1", channel=channel) + self.update_state(code, "sunAutomatic", sunAuto, "1", channel=channel) + self.update_state(code, "dawnAutomatic", dawnAuto, "1", channel=channel) + self.update_state(code, "duskAutomatic", duskAuto, "1", channel=channel) + self.update_state(code, "manualMode", manualMode, "1", channel=channel) + self.update_state(code, "windAutomatic", windAuto, "1", channel=channel) + self.update_state(code, "windMode", windMode, "1", channel=channel) + self.update_state(code, "windDirection", windDir, "1", channel=channel) + self.update_state(code, "rainAutomatic", rainAuto, "1", channel=channel) + self.update_state(code, "rainMode", rainMode, "1", channel=channel) + self.update_state(code, "rainDirection", rainDir, "1", channel=channel) + self.update_state(code, "runningTime", runningTime, "1", channel=channel) + self.update_state(code, "motorDeadTime", deadTimes[deadTime], "1", channel=channel) + self.update_state(code, "position", pos, "1", channel=channel) + self.update_state(code, "reversal", reversal, "1", channel=channel) + self.update_state(code, "blindsMode", blindsMode, "1", channel=channel) # not tested yet if blindsMode == "on": # pragma: no cover - self.update_state(hash, "tiltInSunPos", tiltInSunPos, "1") - self.update_state(hash, "tiltInVentPos", tiltInVentPos, "1") - self.update_state(hash, "tiltAfterMoveLevel", tiltAfterMoveLevel, "1") - self.update_state(hash, "tiltAfterStopDown", tiltAfterStopDown, "1") - self.update_state(hash, "module_definitionaultSlatPos", module_definitionaultSlatPos, "1") - self.update_state(hash, "slatRunTime", slatRunTime, "1") - self.update_state(hash, "slatPosition", slatPosition, "1") + self.update_state(code, "tiltInSunPos", tiltInSunPos, "1", channel=channel) + self.update_state(code, "tiltInVentPos", tiltInVentPos, "1", channel=channel) + self.update_state(code, "tiltAfterMoveLevel", tiltAfterMoveLevel, "1", channel=channel) + self.update_state(code, "tiltAfterStopDown", tiltAfterStopDown, "1", channel=channel) + self.update_state(code, "module_definitionaultSlatPos", module_definitionaultSlatPos, "1", channel=channel) + self.update_state(code, "slatRunTime", slatRunTime, "1", channel=channel) + self.update_state(code, "slatPosition", slatPosition, "1", channel=channel) else: - self.delete_state(hash, 'tiltInSunPos') - self.delete_state(hash, 'tiltInVentPos') - self.delete_state(hash, 'tiltAfterMoveLevel') - self.delete_state(hash, 'tiltAfterStopDown') - self.delete_state(hash, 'module_definitionaultSlatPos') - self.delete_state(hash, 'slatRunTime') - self.delete_state(hash, 'slatPosition') - - self.update_state(hash, "moving", "stop", "1") - self.update_state(hash, "state", state, "1") + self.delete_state(code, 'tiltInSunPos') + self.delete_state(code, 'tiltInVentPos') + self.delete_state(code, 'tiltAfterMoveLevel') + self.delete_state(code, 'tiltAfterStopDown') + self.delete_state(code, 'module_definitionaultSlatPos') + self.delete_state(code, 'slatRunTime') + self.delete_state(code, 'slatPosition') + + self.update_state(code, "moving", "stop", "1", channel=channel) + self.update_state(code, "state", state, "1", channel=channel) readingsEndUpdate(hash, 1) # Notify is done by Dispatch # Rohrmotor, SX5 -- not tested yet elif format == "24": # pragma: no cover @@ -410,36 +412,36 @@ def parse(self, msg): state = "block" if (block == "1") else pos readingsBeginUpdate(hash) - self.update_state(hash, "manualMode", manualMode, "1") - self.update_state(hash, "timeAutomatic", timerAuto, "1") - self.update_state(hash, "ventilatingPosition", ventPos, "1") - self.update_state(hash, "ventilatingMode", ventMode, "1") - self.update_state(hash, "position", pos, "1") - self.update_state(hash, "state", state, "1") - self.update_state(hash, "obstacle", obstacle, "1") - self.update_state(hash, "block", block, "1") - self.update_state(hash, "moving", "stop", "1") + self.update_state(code, "manualMode", manualMode, "1", channel=channel) + self.update_state(code, "timeAutomatic", timerAuto, "1", channel=channel) + self.update_state(code, "ventilatingPosition", ventPos, "1", channel=channel) + self.update_state(code, "ventilatingMode", ventMode, "1", channel=channel) + self.update_state(code, "position", pos, "1", channel=channel) + self.update_state(code, "state", state, "1", channel=channel) + self.update_state(code, "obstacle", obstacle, "1", channel=channel) + self.update_state(code, "block", block, "1", channel=channel) + self.update_state(code, "moving", "stop", "1", channel=channel) if code[0:2] == "4E": # SX5 - self.update_state(hash, "10minuteAlarm", alert10, "1") - self.update_state(hash, "automaticClosing", closingTimes['autoClose'], "1") - self.update_state(hash, "2000cycleAlarm", alert2000, "1") - self.update_state(hash, "openSpeed", openSpeeds['openSpeed'], "1") - self.update_state(hash, "backJump", backJump, "1") - self.update_state(hash, "lightCurtain", lightCurtain, "1") + self.update_state(code, "10minuteAlarm", alert10, "1", channel=channel) + self.update_state(code, "automaticClosing", closingTimes['autoClose'], "1", channel=channel) + self.update_state(code, "2000cycleAlarm", alert2000, "1", channel=channel) + self.update_state(code, "openSpeed", openSpeeds['openSpeed'], "1", channel=channel) + self.update_state(code, "backJump", backJump, "1", channel=channel) + self.update_state(code, "lightCurtain", lightCurtain, "1", channel=channel) else: - self.update_state(hash, "sunPosition", sunPos, "1") - self.update_state(hash, "sunMode", sunMode, "1") - self.update_state(hash, "sunAutomatic", sunAuto, "1") - self.update_state(hash, "dawnAutomatic", dawnAuto, "1") - self.update_state(hash, "duskAutomatic", duskAuto, "1") - self.update_state(hash, "windAutomatic", windAuto, "1") - self.update_state(hash, "windMode", windMode, "1") - self.update_state(hash, "windDirection", windDir, "1") - self.update_state(hash, "rainAutomatic", rainAuto, "1") - self.update_state(hash, "rainMode", rainMode, "1") - self.update_state(hash, "rainDirection", rainDir, "1") - self.update_state(hash, "reversal", reversal, "1") + self.update_state(code, "sunPosition", sunPos, "1", channel=channel) + self.update_state(code, "sunMode", sunMode, "1", channel=channel) + self.update_state(code, "sunAutomatic", sunAuto, "1", channel=channel) + self.update_state(code, "dawnAutomatic", dawnAuto, "1", channel=channel) + self.update_state(code, "duskAutomatic", duskAuto, "1", channel=channel) + self.update_state(code, "windAutomatic", windAuto, "1", channel=channel) + self.update_state(code, "windMode", windMode, "1", channel=channel) + self.update_state(code, "windDirection", windDir, "1", channel=channel) + self.update_state(code, "rainAutomatic", rainAuto, "1", channel=channel) + self.update_state(code, "rainMode", rainMode, "1", channel=channel) + self.update_state(code, "rainDirection", rainDir, "1", channel=channel) + self.update_state(code, "reversal", reversal, "1", channel=channel) readingsEndUpdate(hash, 1) @@ -465,21 +467,21 @@ def parse(self, msg): state = "on" if (level == 100) else level readingsBeginUpdate(hash) - self.update_state(hash, "stairwellFunction", stairwellFunction, "1") - self.update_state(hash, "stairwellTime", stairwellTime, "1") - self.update_state(hash, "timeAutomatic", timerAuto, "1") - self.update_state(hash, "duskAutomatic", duskAuto, "1") - self.update_state(hash, "sunAutomatic", sunAuto, "1") - self.update_state(hash, "sunMode", sunMode, "1") - self.update_state(hash, "manualMode", manualMode, "1") - self.update_state(hash, "dawnAutomatic", dawnAuto, "1") - self.update_state(hash, "saveIntermediateOnStop", intemedSave, "1") - self.update_state(hash, "runningTime", runningTime, "1") - self.update_state(hash, "intermediateValue", intemedVal, "1") - self.update_state(hash, "intermediateMode", intermedMode, "1") - self.update_state(hash, "level", level, "1") - self.update_state(hash, "modeChange", modeChange, "1") - self.update_state(hash, "state", state, "1") + self.update_state(code, "stairwellFunction", stairwellFunction, "1", channel=channel) + self.update_state(code, "stairwellTime", stairwellTime, "1", channel=channel) + self.update_state(code, "timeAutomatic", timerAuto, "1", channel=channel) + self.update_state(code, "duskAutomatic", duskAuto, "1", channel=channel) + self.update_state(code, "sunAutomatic", sunAuto, "1", channel=channel) + self.update_state(code, "sunMode", sunMode, "1", channel=channel) + self.update_state(code, "manualMode", manualMode, "1", channel=channel) + self.update_state(code, "dawnAutomatic", dawnAuto, "1", channel=channel) + self.update_state(code, "saveIntermediateOnStop", intemedSave, "1", channel=channel) + self.update_state(code, "runningTime", runningTime, "1", channel=channel) + self.update_state(code, "intermediateValue", intemedVal, "1", channel=channel) + self.update_state(code, "intermediateMode", intermedMode, "1", channel=channel) + self.update_state(code, "level", level, "1", channel=channel) + self.update_state(code, "modeChange", modeChange, "1", channel=channel) + self.update_state(code, "state", state, "1", channel=channel) readingsEndUpdate(hash, 1) # Notify is done by Dispatch # Thermostat -- not tested yet @@ -500,20 +502,20 @@ def parse(self, msg): state = "T: temperature1 desired: desiredTemp" readingsBeginUpdate(hash) - self.update_state(hash, "measured-temp", temperature1, "1") - self.update_state(hash, "measured-temp2", temperature2, "1") - self.update_state(hash, "temperatureThreshold1", tempThreshold1, "1") - self.update_state(hash, "temperatureThreshold2", tempThreshold2, "1") - self.update_state(hash, "temperatureThreshold3", tempThreshold3, "1") - self.update_state(hash, "temperatureThreshold4", tempThreshold4, "1") - self.update_state(hash, "desired-temp", desiredTemp, "1") - self.update_state(hash, "output", output, "1") - self.update_state(hash, "manualOverride", manualOverride, "1") - self.update_state(hash, "actTempLimit", actTempLimit, "1") - self.update_state(hash, "timeAutomatic", timerAuto, "1") - self.update_state(hash, "manualMode", manualMode, "1") - - self.update_state(hash, "state", state, "1") + self.update_state(code, "measured-temp", temperature1, "1", channel=channel) + self.update_state(code, "measured-temp2", temperature2, "1", channel=channel) + self.update_state(code, "temperatureThreshold1", tempThreshold1, "1", channel=channel) + self.update_state(code, "temperatureThreshold2", tempThreshold2, "1", channel=channel) + self.update_state(code, "temperatureThreshold3", tempThreshold3, "1", channel=channel) + self.update_state(code, "temperatureThreshold4", tempThreshold4, "1", channel=channel) + self.update_state(code, "desired-temp", desiredTemp, "1", channel=channel) + self.update_state(code, "output", output, "1", channel=channel) + self.update_state(code, "manualOverride", manualOverride, "1", channel=channel) + self.update_state(code, "actTempLimit", actTempLimit, "1", channel=channel) + self.update_state(code, "timeAutomatic", timerAuto, "1", channel=channel) + self.update_state(code, "manualMode", manualMode, "1", channel=channel) + + self.update_state(code, "state", state, "1", channel=channel) readingsEndUpdate(hash, 1) else: @@ -555,18 +557,18 @@ def parse(self, msg): for chan in chans: if id[2:4] in ("1a", "18", "19", "01", "02", "03"): if (id[2:4] == "1a") or (id[0:2] == "0e") or (code[0:2] in ("a0", "a2")): - self.update_state(hash, "state", sensorMsg[id]['state'] + "." + chan, "1") + self.update_state(code, "state", sensorMsg[id]['state'] + "." + chan, "1", channel=channel) else: - self.update_state(hash, "state", sensorMsg[id]['state'], "1") + self.update_state(code, "state", sensorMsg[id]['state'], "1", channel=channel) - self.update_state(hash, "channelchan", sensorMsg[id]['name'], "1") + self.update_state(code, "channelchan", sensorMsg[id]['name'], "1", channel=channel) else: if (code[0:2] not in ("69", "73")) or (id[2:4] in ("11", "12")): chan = "" if code[0:2] in ("65", "a5", "aa", "ab"): - self.update_state(hash, "state", sensorMsg[id]['state'], "1") + self.update_state(code, "state", sensorMsg[id]['state'], "1", channel=channel) - self.update_state(hash, "event", sensorMsg[id]['name'] + "." + chan, "1") + self.update_state(code, "event", sensorMsg[id]['name'] + "." + chan, "1", channel=channel) DoTrigger(hash["name"], sensorMsg[id][name] + "." + chan) @@ -595,13 +597,13 @@ def parse(self, msg): state += " B: ".format(brightness) readingsBeginUpdate(hash) - self.update_state(hash, "brightness", brightness, "1") - self.update_state(hash, "sunDirection", sunDirection, "1") - self.update_state(hash, "sunHeight", sunHeight, "1") - self.update_state(hash, "temperature", temperature, "1") - self.update_state(hash, "isRaining", isRaining, "1") - self.update_state(hash, "state", state, "1") - self.update_state(hash, "wind", wind, "1") + self.update_state(code, "brightness", brightness, "1", channel=channel) + self.update_state(code, "sunDirection", sunDirection, "1", channel=channel) + self.update_state(code, "sunHeight", sunHeight, "1", channel=channel) + self.update_state(code, "temperature", temperature, "1", channel=channel) + self.update_state(code, "isRaining", isRaining, "1", channel=channel) + self.update_state(code, "state", state, "1", channel=channel) + self.update_state(code, "wind", wind, "1", channel=channel) readingsEndUpdate(hash, 1) # Notify is done by Dispatch # Umweltsensor Zeit @@ -622,8 +624,8 @@ def parse(self, msg): second = msg[24:24 + 2] readingsBeginUpdate(hash) - self.update_state(hash, "date", "20" + str(year) + "-" + str(month) + "-" + str(day), "1") - self.update_state(hash, "time", str(hour) + ":" + str(minute) + ":" + str(second), "1") + self.update_state(code, "date", "20" + str(year) + "-" + str(month) + "-" + str(day), "1", channel=channel) + self.update_state(code, "time", str(hour) + ":" + str(minute) + ":" + str(second), "1", channel=channel) readingsEndUpdate(hash, 1) # Notify is done by Dispatch # Umweltsensor Konfiguration @@ -640,8 +642,8 @@ def parse(self, msg): channel = "01" del hash['READINGS']['configModified'] - self.update_state(hash, ".regreg", "regVal", "1") - # self.update_state(hash, "regreg", "regVal", "1") + self.update_state(code, ".regreg", "regVal", "1", channel=channel) + # self.update_state(code, "regreg", "regVal", "1", channel=channel) DUOFERN_DecodeWeatherSensorConfig(hash) # Rauchmelder Batterie @@ -650,8 +652,8 @@ def parse(self, msg): batteryLevel = int(msg[8:8 + 2], 16) readingsBeginUpdate(hash) - self.update_state(hash, "battery", battery, "1") - self.update_state(hash, "batteryLevel", batteryLevel, "1") + self.update_state(code, "battery", battery, "1", channel=channel) + self.update_state(code, "batteryLevel", batteryLevel, "1", channel=channel) readingsEndUpdate(hash, 1) # Notify is done by Dispatch # ACK, Befehl vom Aktor empfangen @@ -663,7 +665,7 @@ def parse(self, msg): # NACK, Befehl nicht vom Aktor empfangen elif msg[0:8] == "810108aa": logger.warning("missing ack for {}".format(hash)) - # self.update_state(hash, "state", "MISSING ACK", "1") + # self.update_state(code, "state", "MISSING ACK", "1", channel=channel) # foreach (grep (/^channel_/, keys%{hash})){ # chnHash = module_definitions{hash->{_}} # readingsSingleUpdate(chnHash, "state", "MISSING ACK", 1) @@ -685,7 +687,7 @@ def send(self, cmd): yield from self.send_hook(cmd) @asyncio.coroutine - def set(self, device_id, cmd, *args): + def set(self, code, cmd, *args, channel=None): # my (hash, @a) = @_ # b = @a @@ -695,8 +697,8 @@ def set(self, device_id, cmd, *args): # cmd = shift @a arg = args[0] if len(args) >= 1 else None arg2 = args[1] if len(args) > 1 else None - code = device_id[0:0 + 6] - name = self.modules['by_code'][device_id]['name'] + code = code[0:0 + 6] + name = self.modules['by_code'][code]['name'] # sets @@ -729,7 +731,7 @@ def set(self, device_id, cmd, *args): if code[0:2] in ("65", "74") and len(code) >= 8 and code[6:8] == "01": sets = merge_dicts(setsSwitchActor) # if (code =~ /^(65|74)....01/) - blindsMode = "off" if not "blindsMode" in self.modules['by_code'][device_id] else self.modules['by_code'][device_id] + blindsMode = "off" if not "blindsMode" in self.modules['by_code'][code] else self.modules['by_code'][code] if (blindsMode == "on"): sets = merge_dicts(sets, setsBlinds) @@ -742,10 +744,10 @@ def set(self, device_id, cmd, *args): return None elif cmd == "clear": - keys = self.modules['by_code'][device_id].keys() + keys = self.modules['by_code'][code].keys() for key in keys: if key != 'name': - self.modules['by_code'][device_id].__delitem__(key) + self.modules['by_code'][code].__delitem__(key) return None # cH = (hash) # delete _->{READINGS} foreach (@cH) @@ -760,8 +762,8 @@ def set(self, device_id, cmd, *args): elif cmd == "writeConfig": for x in range(0, 8): # for(my x=0; x<8; x++) { - regV = "00000000000000000000" if not ".reg{}".format(x) in self.modules['by_code'][device_id] else \ - self.modules['by_code'][device_id][".reg{}".format(x)] + regV = "00000000000000000000" if not ".reg{}".format(x) in self.modules['by_code'][code] else \ + self.modules['by_code'][code][".reg{}".format(x)] reg = "%02x" % (x + 0x81) buf = duoWeatherWriteConfig buf = buf.replace("yyyyyy", code) @@ -769,8 +771,8 @@ def set(self, device_id, cmd, *args): buf = buf.replace("nnnnnnnnnnnnnnnnnnnn", regV) yield from self.send(buf) - if "configModified" in self.modules['by_code'][device_id]: - self.modules['by_code'][device_id].__delitem__("configModified") + if "configModified" in self.modules['by_code'][code]: + self.modules['by_code'][code].__delitem__("configModified") # delete hash->{READINGS}{configModified} return None @@ -941,8 +943,8 @@ def set(self, device_id, cmd, *args): buf = duoCommand command = None - if 'chanNo' in self.modules['by_code'][device_id]: - chanNo = self.modules['by_code'][device_id]['chanNo'] + if 'chanNo' in self.modules['by_code'][code]: + chanNo = self.modules['by_code'][code]['chanNo'] # chanNo = hash->{chanNo} if (hash->{chanNo}) if 'noArg' in commands[cmd]: @@ -1023,46 +1025,46 @@ def set(self, device_id, cmd, *args): if subCmd not in commands[cmd]: raise Exception("Wrong argument {}, {}".format(arg, subCmd)) - position = -1 if not "position" in self.modules['by_code'][device_id] else self.modules['by_code'][device_id][ + position = -1 if not "position" in self.modules['by_code'][code] else self.modules['by_code'][code][ "position"] # toggleUpDown = AttrVal(name, "toggleUpDown", "0") - toggleUpDown = self.modules['by_code'][device_id]['toggleUpDown'] if 'toggleUpDown' in self.modules['by_code'][ - device_id] else 0 - moving = "stop" if not "moving" in self.modules['by_code'][device_id] else self.modules['by_code'][device_id][ + toggleUpDown = self.modules['by_code'][code]['toggleUpDown'] if 'toggleUpDown' in self.modules['by_code'][ + code] else 0 + moving = "stop" if not "moving" in self.modules['by_code'][code] else self.modules['by_code'][code][ "moving"] - timeAutomatic = "on" if not "timeAutomatic" in self.modules['by_code'][device_id] else \ - self.modules['by_code'][device_id]["timeAutomatic"] - dawnAutomatic = "on" if not "dawnAutomatic" in self.modules['by_code'][device_id] else \ - self.modules['by_code'][device_id]["dawnAutomatic"] - duskAutomatic = "on" if not "duskAutomatic" in self.modules['by_code'][device_id] else \ - self.modules['by_code'][device_id]["duskAutomatic"] + timeAutomatic = "on" if not "timeAutomatic" in self.modules['by_code'][code] else \ + self.modules['by_code'][code]["timeAutomatic"] + dawnAutomatic = "on" if not "dawnAutomatic" in self.modules['by_code'][code] else \ + self.modules['by_code'][code]["dawnAutomatic"] + duskAutomatic = "on" if not "duskAutomatic" in self.modules['by_code'][code] else \ + self.modules['by_code'][code]["duskAutomatic"] if moving != "stop": if cmd in ('up', 'down', 'toggle'): if toggleUpDown: cmd = "stop" - # self.update_state(hash,"moving","moving") + # self.update_state(code,"moving","moving", channel=channel) if ((cmd == "toggle") and (position > -1)): - self.update_state(hash, "moving", "moving", 1) + self.update_state(code, "moving", "moving", 1, channel=channel) if ((cmd == "dawn") and (dawnAutomatic == "on") and (position > 0)): - self.update_state(hash, "moving", "up", 1) + self.update_state(code, "moving", "up", 1, channel=channel) if ((cmd == "dusk") and (duskAutomatic == "on") and (position < 100) and (position > -1)): - self.update_state(hash, "moving", "down", 1) + self.update_state(code, "moving", "down", 1, channel=channel) if timer == "00" or timeAutomatic == "on": if ((cmd == "up") and (position > 0)): - self.update_state(hash, "moving", "up", 1) + self.update_state(code, "moving", "up", 1, channel=channel) if ((cmd == "down") and (position < 100) and (position > -1)): - self.update_state(hash, "moving", "down", 1) + self.update_state(code, "moving", "down", 1, channel=channel) if cmd == "position": if arg > position: - self.update_state(hash, "moving", "down", 1) + self.update_state(code, "moving", "down", 1, channel=channel) elif (arg < position): - self.update_state(hash, "moving", "up", 1) + self.update_state(code, "moving", "up", 1, channel=channel) else: - self.update_state(hash, "moving", "stop", 1) + self.update_state(code, "moving", "stop", 1, channel=channel) command = commands[cmd][subCmd] From 456b001be803741514d679972c1c9f9a0ee8a56c Mon Sep 17 00:00:00 2001 From: paulg Date: Sat, 4 Jan 2020 16:09:39 +0100 Subject: [PATCH 09/31] do not timeout unit test when running in debug mode --- pyduofern/tests/test_replay.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyduofern/tests/test_replay.py b/pyduofern/tests/test_replay.py index 2e3992f..0e0ce37 100644 --- a/pyduofern/tests/test_replay.py +++ b/pyduofern/tests/test_replay.py @@ -29,6 +29,7 @@ import re import tempfile import time +import sys import pytest @@ -160,8 +161,9 @@ def test_init_against_mocked_stick(event_loop, replayfile): def feedback_loop(): while proto.transport.replay: yield - if time.time() - start_time > 3: - raise TimeoutError("Mock test should not take longer than 30 seconds, asynchronous loop must be stuck") + if time.time() - start_time > 3 and sys.gettrace() is None: + raise TimeoutError("Mock test should not take longer than 3 seconds, asynchronous loop must be stuck" + "Be aware this is not raised in debug mode.") if proto.transport.next_is_action(): asyncio.ensure_future(proto.transport.actions()) From a19328710916950761b61d0aa545a72073e49f59 Mon Sep 17 00:00:00 2001 From: paulg Date: Sat, 4 Jan 2020 16:10:40 +0100 Subject: [PATCH 10/31] -> chanNo is set using channel parameter -> channel parameter is added to delete_state -> channel parameter type is modified to "int" and formatted as hex when sent to duofern. --- pyduofern/duofern.py | 57 +++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/pyduofern/duofern.py b/pyduofern/duofern.py index ef32191..b1be932 100644 --- a/pyduofern/duofern.py +++ b/pyduofern/duofern.py @@ -107,24 +107,30 @@ def del_device(self, code, name=None): if code in self.modules['by_code']: del self.modules['by_code'][code] - def update_state(self, code, key, value, trigger=None, channel=None): + def update_state(self, code, key, value, trigger=None, channel: int = None): """ :param code: duofern system code :param key: some arbitrary key that should be set in the state dict :param value: the corresponding value - :param trigger: whether or not to trigger + :param trigger: whether or not to call the callback :param channel: if this is a multichannel actor: The channel the key should be set for :return: """ - if channel is None: + if channel is not None: + channel_str = "{:02x}".format(channel) + key = key + "_" + channel_str + if 'channels' not in self.modules['by_code'][code]: + self.modules['by_code'][code]['channels'] = set() + self.modules['by_code'][code]['channels'].add(channel_str) self.modules['by_code'][code][key] = value - else: - self.modules['by_code'][code][key+"_"+channel] = value if self.changes_callback and trigger: self.changes_callback() - def delete_state(self, code, key): + def delete_state(self, code, key, channel: int = None): + if channel is not None: + channel_str = "{:02x}".format(channel) + key = key + "_" + channel_str if key in self.modules['by_code'][code]: del self.modules['by_code'][code][key] @@ -204,7 +210,7 @@ def parse(self, msg): if module_definition01: hash = module_definition01 - channel = "01" + channel = 1 # RolloTron if format == "21": @@ -271,7 +277,7 @@ def parse(self, msg): if module_definition02: hash = module_definition02 - channel = "02" + channel = 2 level = int(msg[20:20 + 2], 16) & 0x7F modeChange = "on" if int(msg[20:20 + 2], 16) & 0x80 else "off" sunMode = "on" if int(msg[12:12 + 2], 16) & 0x10 else "off" @@ -364,13 +370,13 @@ def parse(self, msg): self.update_state(code, "slatRunTime", slatRunTime, "1", channel=channel) self.update_state(code, "slatPosition", slatPosition, "1", channel=channel) else: - self.delete_state(code, 'tiltInSunPos') - self.delete_state(code, 'tiltInVentPos') - self.delete_state(code, 'tiltAfterMoveLevel') - self.delete_state(code, 'tiltAfterStopDown') - self.delete_state(code, 'module_definitionaultSlatPos') - self.delete_state(code, 'slatRunTime') - self.delete_state(code, 'slatPosition') + self.delete_state(code, 'tiltInSunPos',channel=channel) + self.delete_state(code, 'tiltInVentPos',channel=channel) + self.delete_state(code, 'tiltAfterMoveLevel',channel=channel) + self.delete_state(code, 'tiltAfterStopDown',channel=channel) + self.delete_state(code, 'module_definitionaultSlatPos',channel=channel) + self.delete_state(code, 'slatRunTime',channel=channel) + self.delete_state(code, 'slatPosition',channel=channel) self.update_state(code, "moving", "stop", "1", channel=channel) self.update_state(code, "state", state, "1", channel=channel) @@ -552,7 +558,7 @@ def parse(self, msg): if (module_definition01): hash = module_definition01 - channel = "01" + channel = 1 for chan in chans: if id[2:4] in ("1a", "18", "19", "01", "02", "03"): @@ -581,7 +587,7 @@ def parse(self, msg): module_definition01 = self.modules['by_code'][code + "00"] hash = module_definition01 - channel = "01" + channel = 1 brightnessExp = 1000 if int(msg[8:8 + 4], 16) & 0x0400 else 1 brightness = (int(msg[8:8 + 4], 16) & 0x01FF) * brightnessExp @@ -614,7 +620,7 @@ def parse(self, msg): module_definition01 = self.modules['by_code'][code + "00"] hash = module_definition01 - channel = "01" + channel = 1 year = msg[12:12 + 2] month = msg[14:14 + 2] @@ -639,7 +645,7 @@ def parse(self, msg): module_definition01 = self.modules['by_code'][code + "00"] hash = module_definition01 - channel = "01" + channel = 1 del hash['READINGS']['configModified'] self.update_state(code, ".regreg", "regVal", "1", channel=channel) @@ -687,7 +693,7 @@ def send(self, cmd): yield from self.send_hook(cmd) @asyncio.coroutine - def set(self, code, cmd, *args, channel=None): + def set(self, code, cmd, *args, channel: int = None): # my (hash, @a) = @_ # b = @a @@ -697,7 +703,8 @@ def set(self, code, cmd, *args, channel=None): # cmd = shift @a arg = args[0] if len(args) >= 1 else None arg2 = args[1] if len(args) > 1 else None - code = code[0:0 + 6] + assert len(code) == 6, "code should be 6 hex digits" + # code = code[0:0 + 6] name = self.modules['by_code'][code]['name'] # sets @@ -936,16 +943,16 @@ def set(self, code, cmd, *args, channel=None): elif cmd in commands: logger.info("command valid") subCmd = None - chanNo = "01" + if channel is None: + chanNo = "01" + else: + chanNo = "{:02x}".format(channel) argV = "00" argW = "0000" timer = "00" buf = duoCommand command = None - if 'chanNo' in self.modules['by_code'][code]: - chanNo = self.modules['by_code'][code]['chanNo'] - # chanNo = hash->{chanNo} if (hash->{chanNo}) if 'noArg' in commands[cmd]: if (arg and (arg == "timer")): From e8fadbb9a3ecb61153be266f7144cefd01ac285c Mon Sep 17 00:00:00 2001 From: paulg Date: Sat, 4 Jan 2020 16:13:26 +0100 Subject: [PATCH 11/31] fix search and replace error. --- pyduofern/duofern.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyduofern/duofern.py b/pyduofern/duofern.py index b1be932..cf205e3 100644 --- a/pyduofern/duofern.py +++ b/pyduofern/duofern.py @@ -331,7 +331,7 @@ def parse(self, msg): tiltInVentPos = "on" if int(msg[8:8 + 2], 16) & 0x80 else "off" tiltAfterMoveLevel = "on" if int(msg[8:8 + 2], 16) & 0x40 else "off" tiltAfterStopDown = "on" if int(msg[10:10 + 2], 16) & 0x80 else "off" - module_definitionaultSlatPos = int(msg[10:10 + 2], 16) & 0x7F + defaultSlatPos = int(msg[10:10 + 2], 16) & 0x7F slatRunTime = int(msg[8:8 + 2], 16) & 0x3F slatPosition = int(msg[26:26 + 2], 16) & 0x7F @@ -366,7 +366,7 @@ def parse(self, msg): self.update_state(code, "tiltInVentPos", tiltInVentPos, "1", channel=channel) self.update_state(code, "tiltAfterMoveLevel", tiltAfterMoveLevel, "1", channel=channel) self.update_state(code, "tiltAfterStopDown", tiltAfterStopDown, "1", channel=channel) - self.update_state(code, "module_definitionaultSlatPos", module_definitionaultSlatPos, "1", channel=channel) + self.update_state(code, "defaultSlatPos", defaultSlatPos, "1", channel=channel) self.update_state(code, "slatRunTime", slatRunTime, "1", channel=channel) self.update_state(code, "slatPosition", slatPosition, "1", channel=channel) else: @@ -374,7 +374,7 @@ def parse(self, msg): self.delete_state(code, 'tiltInVentPos',channel=channel) self.delete_state(code, 'tiltAfterMoveLevel',channel=channel) self.delete_state(code, 'tiltAfterStopDown',channel=channel) - self.delete_state(code, 'module_definitionaultSlatPos',channel=channel) + self.delete_state(code, 'defaultSlatPos',channel=channel) self.delete_state(code, 'slatRunTime',channel=channel) self.delete_state(code, 'slatPosition',channel=channel) From 5dd81f065ca3ba6ca54c8778bdd935c2fcba069c Mon Sep 17 00:00:00 2001 From: paulg Date: Sat, 4 Jan 2020 16:13:50 +0100 Subject: [PATCH 12/31] whitespace for pep8 --- pyduofern/duofern.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pyduofern/duofern.py b/pyduofern/duofern.py index cf205e3..c86cad1 100644 --- a/pyduofern/duofern.py +++ b/pyduofern/duofern.py @@ -370,13 +370,13 @@ def parse(self, msg): self.update_state(code, "slatRunTime", slatRunTime, "1", channel=channel) self.update_state(code, "slatPosition", slatPosition, "1", channel=channel) else: - self.delete_state(code, 'tiltInSunPos',channel=channel) - self.delete_state(code, 'tiltInVentPos',channel=channel) - self.delete_state(code, 'tiltAfterMoveLevel',channel=channel) - self.delete_state(code, 'tiltAfterStopDown',channel=channel) - self.delete_state(code, 'defaultSlatPos',channel=channel) - self.delete_state(code, 'slatRunTime',channel=channel) - self.delete_state(code, 'slatPosition',channel=channel) + self.delete_state(code, 'tiltInSunPos', channel=channel) + self.delete_state(code, 'tiltInVentPos', channel=channel) + self.delete_state(code, 'tiltAfterMoveLevel', channel=channel) + self.delete_state(code, 'tiltAfterStopDown', channel=channel) + self.delete_state(code, 'defaultSlatPos', channel=channel) + self.delete_state(code, 'slatRunTime', channel=channel) + self.delete_state(code, 'slatPosition', channel=channel) self.update_state(code, "moving", "stop", "1", channel=channel) self.update_state(code, "state", state, "1", channel=channel) @@ -953,7 +953,6 @@ def set(self, code, cmd, *args, channel: int = None): buf = duoCommand command = None - if 'noArg' in commands[cmd]: if (arg and (arg == "timer")): timer = "01" From 9827a8bda6f764e9b4286712aebe9bf86c7f1928 Mon Sep 17 00:00:00 2001 From: paulg Date: Sat, 4 Jan 2020 20:14:10 +0100 Subject: [PATCH 13/31] -> comment out unused functions -> replace module_definition_01 and module_definition02 consistently by channel --- pyduofern/duofern.py | 161 ++++++++++++++++++++++--------------------- 1 file changed, 84 insertions(+), 77 deletions(-) diff --git a/pyduofern/duofern.py b/pyduofern/duofern.py index c86cad1..11fefe3 100644 --- a/pyduofern/duofern.py +++ b/pyduofern/duofern.py @@ -60,20 +60,20 @@ def DoTrigger(*args): logger.debug("called DoTrigger({})".format(args)) -def readingsBulkUpdate(*args): - pass +#def readingsBulkUpdate(*args): +# pass -def readingsSingleUpdate(*args): - pass +#def readingsSingleUpdate(*args): +# pass -def readingsEndUpdate(*args): - pass +#def readingsEndUpdate(*args): +# pass -def readingsBeginUpdate(*args): - pass +#def readingsBeginUpdate(*args): +# pass def RemoveInternalTimer(*args): @@ -150,8 +150,9 @@ def parse(self, msg): logger.info("detected unknown device, ID={}".format(code)) module_definition = self.modules['by_code'][code] - module_definition01 = None - module_definition02 = None + # module_definition01 = None + # module_definition02 = None + channel2 = None # if not module_definition: # DoTrigger("global", "Undefined code {}".format(code)) @@ -174,11 +175,11 @@ def parse(self, msg): # Device unpaired elif (msg[0:4] == "0603"): - readingsBeginUpdate(hash) + # readingsBeginUpdate(hash) self.update_state(code, "unpaired", 1, "1", channel=channel) self.update_state(code, "state", "unpaired", "1", channel=channel) self.del_device(code) - # readingsEndUpdate(hash, 1) # Notify is done by Dispatch + # # readingsEndUpdate(hash, 1) # Notify is done by Dispatch logger.warning("DUOFERN device unpaired, code {}".format(code)) # Status Nachricht Aktor @@ -195,6 +196,7 @@ def parse(self, msg): if code[0:2] in ("65", "69", "74"): # pragma: no cover self.update_state(code, "state", "OK", "1", channel=channel) module_definition01 = self.modules['by_code'][code + "01"] + channel = 1 if not module_definition01: DoTrigger("global", "UNDEFINED DUOFERN_code_actor DUOFERN code01") module_definition01 = self.modules['by_code'][code + "01"] @@ -202,15 +204,19 @@ def parse(self, msg): # Universalaktor -- not tested yet elif code[0:2] == "43": # pragma: no cover self.update_state(code, "state", "OK", "1", channel=channel) - module_definition01 = self.modules['by_code'][code] - if not module_definition01: - DoTrigger("global", "UNDEFINED DUOFERN_code+_01 DUOFERN code+01") + #module_definition01 = self.modules['by_code'][code] + channel = 1 + #if not module_definition01: + # DoTrigger("global", "UNDEFINED DUOFERN_code+_01 DUOFERN code+01") - module_definition02 = None + #module_definition02 = None + channel2 = 2 - if module_definition01: - hash = module_definition01 - channel = 1 + #if module_definition01: + # it seems that sometimes "module_definition01" corresponts to channel "01", at other times + # channel="00". I am trying to stick with what module_definition was set to. + #hash = module_definition01 + #channel = 1 # RolloTron if format == "21": @@ -229,7 +235,7 @@ def parse(self, msg): state = "opened" if (pos == 0) else pos state = "closed" if (pos == 100) else pos - readingsBeginUpdate(hash) + # readingsBeginUpdate(hash) self.update_state(code, "ventilatingPosition", ventPos, "1", channel=channel) self.update_state(code, "ventilatingMode", ventMode, "1", channel=channel) self.update_state(code, "sunPosition", sunPos, "1", channel=channel) @@ -242,7 +248,7 @@ def parse(self, msg): self.update_state(code, "position", pos, "1", channel=channel) self.update_state(code, "state", state, "1", channel=channel) self.update_state(code, "moving", "stop", "1", channel=channel) - readingsEndUpdate(hash, 1) # Notify is done by Dispatch + # readingsEndUpdate(hash, 1) # Notify is done by Dispatch # Universal Aktor, Steckdosenaktor, Troll Comfort DuoFern (Lichtmodus) not tested yet elif format == "22": # pragma: no cover @@ -261,7 +267,7 @@ def parse(self, msg): state = "off" if (level == 0) else level state = "on" if (level == 100) else level - readingsBeginUpdate(hash) + # readingsBeginUpdate(hash) self.update_state(code, "sunMode", sunMode, "1", channel=channel) self.update_state(code, "timeAutomatic", timerAuto, "1", channel=channel) self.update_state(code, "sunAutomatic", sunAuto, "1", channel=channel) @@ -273,11 +279,9 @@ def parse(self, msg): self.update_state(code, "stairwellTime", stairwellTime, "1", channel=channel) self.update_state(code, "level", level, "1", channel=channel) self.update_state(code, "state", state, "1", channel=channel) - readingsEndUpdate(hash, 1) + # readingsEndUpdate(hash, 1) - if module_definition02: - hash = module_definition02 - channel = 2 + if channel2: level = int(msg[20:20 + 2], 16) & 0x7F modeChange = "on" if int(msg[20:20 + 2], 16) & 0x80 else "off" sunMode = "on" if int(msg[12:12 + 2], 16) & 0x10 else "off" @@ -290,10 +294,12 @@ def parse(self, msg): stairwellTime = (int(msg[8:8 + 4], 16) & 0x7FFF) / 10 state = level - state = "off" if (level == 0) else level - state = "on" if (level == 100) else level + if level == 0: + state = "off" + if level == 100: + state = "on" - readingsBeginUpdate(hash) + # readingsBeginUpdate(hash) self.update_state(code, "sunMode", sunMode, "1", channel=channel) self.update_state(code, "timeAutomatic", timerAuto, "1", channel=channel) self.update_state(code, "sunAutomatic", sunAuto, "1", channel=channel) @@ -305,7 +311,7 @@ def parse(self, msg): self.update_state(code, "stairwellTime", stairwellTime, "1", channel=channel) self.update_state(code, "level", level, "1", channel=channel) self.update_state(code, "state", state, "1", channel=channel) - readingsEndUpdate(hash, 1) # Notify is done by Dispatch + # readingsEndUpdate(hash, 1) # Notify is done by Dispatch elif format == "23": pos = int(msg[22:22 + 2], 16) & 0x7F reversal = "on" if int(msg[22:22 + 2], 16) & 0x80 else "off" @@ -338,7 +344,7 @@ def parse(self, msg): state = "opened" if (pos == 0) else pos state = "closed" if (pos == 100) else state - readingsBeginUpdate(hash) + # readingsBeginUpdate(hash) self.update_state(code, "ventilatingPosition", ventPos, "1", channel=channel) self.update_state(code, "ventilatingMode", ventMode, "1", channel=channel) self.update_state(code, "sunPosition", sunPos, "1", channel=channel) @@ -380,7 +386,7 @@ def parse(self, msg): self.update_state(code, "moving", "stop", "1", channel=channel) self.update_state(code, "state", state, "1", channel=channel) - readingsEndUpdate(hash, 1) # Notify is done by Dispatch + # readingsEndUpdate(hash, 1) # Notify is done by Dispatch # Rohrmotor, SX5 -- not tested yet elif format == "24": # pragma: no cover @@ -417,7 +423,7 @@ def parse(self, msg): state = "obstacle" if (obstacle == "1") else pos state = "block" if (block == "1") else pos - readingsBeginUpdate(hash) + # readingsBeginUpdate(hash) self.update_state(code, "manualMode", manualMode, "1", channel=channel) self.update_state(code, "timeAutomatic", timerAuto, "1", channel=channel) self.update_state(code, "ventilatingPosition", ventPos, "1", channel=channel) @@ -449,7 +455,7 @@ def parse(self, msg): self.update_state(code, "rainDirection", rainDir, "1", channel=channel) self.update_state(code, "reversal", reversal, "1", channel=channel) - readingsEndUpdate(hash, 1) + # readingsEndUpdate(hash, 1) # Dimmaktor -- not tested yet elif format == "25": # pragma: no cover @@ -472,7 +478,7 @@ def parse(self, msg): state = "off" if (level == 0) else level state = "on" if (level == 100) else level - readingsBeginUpdate(hash) + # readingsBeginUpdate(hash) self.update_state(code, "stairwellFunction", stairwellFunction, "1", channel=channel) self.update_state(code, "stairwellTime", stairwellTime, "1", channel=channel) self.update_state(code, "timeAutomatic", timerAuto, "1", channel=channel) @@ -488,7 +494,7 @@ def parse(self, msg): self.update_state(code, "level", level, "1", channel=channel) self.update_state(code, "modeChange", modeChange, "1", channel=channel) self.update_state(code, "state", state, "1", channel=channel) - readingsEndUpdate(hash, 1) # Notify is done by Dispatch + # readingsEndUpdate(hash, 1) # Notify is done by Dispatch # Thermostat -- not tested yet elif format == "27": # pragma: no cover @@ -507,7 +513,7 @@ def parse(self, msg): state = "T: temperature1 desired: desiredTemp" - readingsBeginUpdate(hash) + # readingsBeginUpdate(hash) self.update_state(code, "measured-temp", temperature1, "1", channel=channel) self.update_state(code, "measured-temp2", temperature2, "1", channel=channel) self.update_state(code, "temperatureThreshold1", tempThreshold1, "1", channel=channel) @@ -522,7 +528,7 @@ def parse(self, msg): self.update_state(code, "manualMode", manualMode, "1", channel=channel) self.update_state(code, "state", state, "1", channel=channel) - readingsEndUpdate(hash, 1) + # readingsEndUpdate(hash, 1) else: logger.warning("DUOFERN unknown msg: {}".format(msg)) @@ -551,14 +557,15 @@ def parse(self, msg): chans.append(chan) if code[0:2] in ("65", "69", "74"): - module_definition01 = self.modules['by_code'][code + "00"] - if not module_definition01: - DoTrigger("global", "UNDEFINED DUOFERN_code_sensor DUOFERN code00") - module_definition01 = self.modules['by_code'][code + "00"] + # module_definition01 = self.modules['by_code'][code + "00"] + channel = 0 + #if not module_definition01: + #DoTrigger("global", "UNDEFINED DUOFERN_code_sensor DUOFERN code00") + #module_definition01 = self.modules['by_code'][code + "00"] - if (module_definition01): - hash = module_definition01 - channel = 1 + #if (module_definition01): + # hash = module_definition01 + # channel = 0 for chan in chans: if id[2:4] in ("1a", "18", "19", "01", "02", "03"): @@ -581,13 +588,13 @@ def parse(self, msg): # Umweltsensor Wetter -- not tested yet elif msg[0:8] == "0f011322": # pragma: no cover - module_definition01 = self.modules['by_code'][code + "00"] - if not module_definition01: - DoTrigger("global", "UNDEFINED DUOFERN_code_sensor DUOFERN code00") - module_definition01 = self.modules['by_code'][code + "00"] - - hash = module_definition01 - channel = 1 + # module_definition01 = self.modules['by_code'][code + "00"] + # if not module_definition01: + # DoTrigger("global", "UNDEFINED DUOFERN_code_sensor DUOFERN code00") + # module_definition01 = self.modules['by_code'][code + "00"] + # + # hash = module_definition01 + channel = 0 brightnessExp = 1000 if int(msg[8:8 + 4], 16) & 0x0400 else 1 brightness = (int(msg[8:8 + 4], 16) & 0x01FF) * brightnessExp @@ -602,7 +609,7 @@ def parse(self, msg): state += " IR: ".format(isRaining) state += " B: ".format(brightness) - readingsBeginUpdate(hash) + # readingsBeginUpdate(hash) self.update_state(code, "brightness", brightness, "1", channel=channel) self.update_state(code, "sunDirection", sunDirection, "1", channel=channel) self.update_state(code, "sunHeight", sunHeight, "1", channel=channel) @@ -610,17 +617,17 @@ def parse(self, msg): self.update_state(code, "isRaining", isRaining, "1", channel=channel) self.update_state(code, "state", state, "1", channel=channel) self.update_state(code, "wind", wind, "1", channel=channel) - readingsEndUpdate(hash, 1) # Notify is done by Dispatch + # readingsEndUpdate(hash, 1) # Notify is done by Dispatch # Umweltsensor Zeit elif msg[0:8] == "0fff1020": # pragma: no cover - module_definition01 = self.modules['by_code'][code + "00"] - if (not module_definition01): - DoTrigger("global", "UNDEFINED DUOFERN_code_sensor DUOFERN code00") - module_definition01 = self.modules['by_code'][code + "00"] - - hash = module_definition01 - channel = 1 + # module_definition01 = self.modules['by_code'][code + "00"] + # if (not module_definition01): + # DoTrigger("global", "UNDEFINED DUOFERN_code_sensor DUOFERN code00") + # module_definition01 = self.modules['by_code'][code + "00"] + # + # hash = module_definition01 + channel = 0 year = msg[12:12 + 2] month = msg[14:14 + 2] @@ -629,23 +636,23 @@ def parse(self, msg): minute = msg[22:22 + 2] second = msg[24:24 + 2] - readingsBeginUpdate(hash) + # readingsBeginUpdate(hash) self.update_state(code, "date", "20" + str(year) + "-" + str(month) + "-" + str(day), "1", channel=channel) self.update_state(code, "time", str(hour) + ":" + str(minute) + ":" + str(second), "1", channel=channel) - readingsEndUpdate(hash, 1) # Notify is done by Dispatch + # readingsEndUpdate(hash, 1) # Notify is done by Dispatch # Umweltsensor Konfiguration elif msg[0:7] == "0fff1b2" and msg[7] in ["0", "1", "2", "3", "4", "5", "6", "7", "8"]: # pragma: no cover reg = msg[6:6 + 2] - 21 regVal = msg[8:8 + 20] - module_definition01 = self.modules['by_code'][code + "00"] - if not module_definition01: - DoTrigger("global", "UNDEFINED DUOFERN_code_sensor DUOFERN {}00".format(code)) - module_definition01 = self.modules['by_code'][code + "00"] + # module_definition01 = self.modules['by_code'][code + "00"] + # if not module_definition01: + # DoTrigger("global", "UNDEFINED DUOFERN_code_sensor DUOFERN {}00".format(code)) + # module_definition01 = self.modules['by_code'][code + "00"] - hash = module_definition01 - channel = 1 + # hash = module_definition01 + channel = 0 del hash['READINGS']['configModified'] self.update_state(code, ".regreg", "regVal", "1", channel=channel) @@ -657,10 +664,10 @@ def parse(self, msg): battery = "low" if int(msg[8:8 + 2], 16) <= 10 else "ok" batteryLevel = int(msg[8:8 + 2], 16) - readingsBeginUpdate(hash) + # readingsBeginUpdate(hash) self.update_state(code, "battery", battery, "1", channel=channel) self.update_state(code, "batteryLevel", batteryLevel, "1", channel=channel) - readingsEndUpdate(hash, 1) # Notify is done by Dispatch + # readingsEndUpdate(hash, 1) # Notify is done by Dispatch # ACK, Befehl vom Aktor empfangen elif msg[0:8] == "810003cc": @@ -681,10 +688,10 @@ def parse(self, msg): else: logger.warning("Unknown msg: {}".format(msg)) - if module_definition01: - DoTrigger(module_definition01['name'], None) - if module_definition02: - DoTrigger(module_definition02['name'], None) +# if module_definition01: +# DoTrigger(module_definition01['name'], None) +# if module_definition02: +# DoTrigger(module_definition02['name'], None) return name @@ -929,13 +936,13 @@ def set(self, code, cmd, *args, channel: int = None): # # @regsA = unpack('(A20)*', regs) # - # readingsBeginUpdate(hash) + # # readingsBeginUpdate(hash) # for(my x=0; x<8; x++) { # readingsBulkUpdate(hash, ".regx", regsA[x], 0) # #readingsBulkUpdate(hash, "regx", regsA[x], 0) # } # readingsBulkUpdate(hash, "configModified", 1, 0) - # readingsEndUpdate(hash, 1) + # # readingsEndUpdate(hash, 1) # # DUOFERN_DecodeWeatherSensorConfig(hash) # return undef From b3ec1d599b4f264a9a7454aad64f92f3d9c99a26 Mon Sep 17 00:00:00 2001 From: paulg Date: Sat, 4 Jan 2020 20:15:04 +0100 Subject: [PATCH 14/31] check if not Nome to be more precise --- pyduofern/duofern.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyduofern/duofern.py b/pyduofern/duofern.py index 11fefe3..91bee21 100644 --- a/pyduofern/duofern.py +++ b/pyduofern/duofern.py @@ -281,7 +281,7 @@ def parse(self, msg): self.update_state(code, "state", state, "1", channel=channel) # readingsEndUpdate(hash, 1) - if channel2: + if channel2 is not None: level = int(msg[20:20 + 2], 16) & 0x7F modeChange = "on" if int(msg[20:20 + 2], 16) & 0x80 else "off" sunMode = "on" if int(msg[12:12 + 2], 16) & 0x10 else "off" From 54476c6ee964ecfdb4432bed1643b82f63db9a90 Mon Sep 17 00:00:00 2001 From: paulg Date: Sat, 4 Jan 2020 20:19:21 +0100 Subject: [PATCH 15/31] fix obvious bug when interpreting level --- pyduofern/duofern.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyduofern/duofern.py b/pyduofern/duofern.py index 91bee21..f4514f2 100644 --- a/pyduofern/duofern.py +++ b/pyduofern/duofern.py @@ -475,8 +475,11 @@ def parse(self, msg): modeChange = "on" if int(msg[22:22 + 2], 16) & 0x80 else "off" state = level - state = "off" if (level == 0) else level - state = "on" if (level == 100) else level + + if level == 0: + state = "off" + if level == 100: + state = "on" # readingsBeginUpdate(hash) self.update_state(code, "stairwellFunction", stairwellFunction, "1", channel=channel) From b10c7d40ed126423faab257659a54bb3b0b29921 Mon Sep 17 00:00:00 2001 From: paulg Date: Sat, 4 Jan 2020 20:28:29 +0100 Subject: [PATCH 16/31] get rid of hash --- pyduofern/duofern.py | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/pyduofern/duofern.py b/pyduofern/duofern.py index f4514f2..d3cccbb 100644 --- a/pyduofern/duofern.py +++ b/pyduofern/duofern.py @@ -144,12 +144,15 @@ def parse(self, msg): # return hash->{NAME} if (code == "FFFFFF") try: - module_definition = self.modules['by_code'][code] + # module_definition = self.modules['by_code'][code] + name = self.modules['by_code'][code]['name'] + except KeyError: self.add_device(code) logger.info("detected unknown device, ID={}".format(code)) - module_definition = self.modules['by_code'][code] + name = self.modules['by_code'][code]['name'] + #hash="asdf" # module_definition01 = None # module_definition02 = None channel2 = None @@ -160,8 +163,8 @@ def parse(self, msg): # logger.warning("Undefined code {}".format(code)) # raise DuofernException("Undefined code {}".format(code)) - hash = module_definition - name = hash['name'] + # hash = module_definition + # name = hash['name'] channel = None if name in self.ignore_devices: @@ -194,12 +197,12 @@ def parse(self, msg): # Bewegungsmelder, Wettersensor, Mehrfachwandtaster not tested yet if code[0:2] in ("65", "69", "74"): # pragma: no cover - self.update_state(code, "state", "OK", "1", channel=channel) - module_definition01 = self.modules['by_code'][code + "01"] + #self.update_state(code, "state", "OK", "1", channel=channel) + #module_definition01 = self.modules['by_code'][code + "01"] channel = 1 - if not module_definition01: - DoTrigger("global", "UNDEFINED DUOFERN_code_actor DUOFERN code01") - module_definition01 = self.modules['by_code'][code + "01"] + #if not module_definition01: + #DoTrigger("global", "UNDEFINED DUOFERN_code_actor DUOFERN code01") + #module_definition01 = self.modules['by_code'][code + "01"] # Universalaktor -- not tested yet elif code[0:2] == "43": # pragma: no cover @@ -585,7 +588,7 @@ def parse(self, msg): self.update_state(code, "state", sensorMsg[id]['state'], "1", channel=channel) self.update_state(code, "event", sensorMsg[id]['name'] + "." + chan, "1", channel=channel) - DoTrigger(hash["name"], sensorMsg[id][name] + "." + chan) + # DoTrigger(hash["name"], sensorMsg[id][name] + "." + chan) @@ -657,10 +660,12 @@ def parse(self, msg): # hash = module_definition01 channel = 0 - del hash['READINGS']['configModified'] + logger.warning("Weather sensor not supported yet") + #del hash['READINGS']['configModified'] self.update_state(code, ".regreg", "regVal", "1", channel=channel) # self.update_state(code, "regreg", "regVal", "1", channel=channel) - DUOFERN_DecodeWeatherSensorConfig(hash) + + #DUOFERN_DecodeWeatherSensorConfig(hash) # Rauchmelder Batterie elif msg[0:8] == "0fff1323": # pragma: no cover @@ -674,13 +679,14 @@ def parse(self, msg): # ACK, Befehl vom Aktor empfangen elif msg[0:8] == "810003cc": - hash['helper']['timeout']['t'] = hash['name']["timeout"]["60"] + logger.debug("ack received {}".format(self.modules['by_code'][code])) + #hash['helper']['timeout']['t'] = hash['name']["timeout"]["60"] ##InternalTimer(gettimeofday()+hash['helper']['timeout']{t}, "DUOFERN_StatusTimeout", hash, 0) - hash['helper']['timeout']['count'] = 4 + #hash['helper']['timeout']['count'] = 4 # NACK, Befehl nicht vom Aktor empfangen elif msg[0:8] == "810108aa": - logger.warning("missing ack for {}".format(hash)) + logger.warning("missing ack for {}".format(self.modules['by_code'][code])) # self.update_state(code, "state", "MISSING ACK", "1", channel=channel) # foreach (grep (/^channel_/, keys%{hash})){ # chnHash = module_definitions{hash->{_}} From 541ad3f39fe47e90fbec49c26e18d334fe8f661d Mon Sep 17 00:00:00 2001 From: paulg Date: Sat, 4 Jan 2020 20:33:31 +0100 Subject: [PATCH 17/31] actually use channel2 fix one more of those if-bugs --- pyduofern/duofern.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/pyduofern/duofern.py b/pyduofern/duofern.py index d3cccbb..4ffc5ae 100644 --- a/pyduofern/duofern.py +++ b/pyduofern/duofern.py @@ -267,8 +267,10 @@ def parse(self, msg): stairwellTime = (int(msg[16:16 + 4], 16) & 0x7FFF) / 10 state = level - state = "off" if (level == 0) else level - state = "on" if (level == 100) else level + if level == 0: + state = "off" + if level == 100: + state = "on" # readingsBeginUpdate(hash) self.update_state(code, "sunMode", sunMode, "1", channel=channel) @@ -303,17 +305,17 @@ def parse(self, msg): state = "on" # readingsBeginUpdate(hash) - self.update_state(code, "sunMode", sunMode, "1", channel=channel) - self.update_state(code, "timeAutomatic", timerAuto, "1", channel=channel) - self.update_state(code, "sunAutomatic", sunAuto, "1", channel=channel) - self.update_state(code, "dawnAutomatic", dawnAuto, "1", channel=channel) - self.update_state(code, "duskAutomatic", duskAuto, "1", channel=channel) - self.update_state(code, "manualMode", manualMode, "1", channel=channel) - self.update_state(code, "modeChange", modeChange, "1", channel=channel) - self.update_state(code, "stairwellFunction", stairwellFunction, "1", channel=channel) - self.update_state(code, "stairwellTime", stairwellTime, "1", channel=channel) - self.update_state(code, "level", level, "1", channel=channel) - self.update_state(code, "state", state, "1", channel=channel) + self.update_state(code, "sunMode", sunMode, "1", channel=channel2) + self.update_state(code, "timeAutomatic", timerAuto, "1", channel=channel2) + self.update_state(code, "sunAutomatic", sunAuto, "1", channel=channel2) + self.update_state(code, "dawnAutomatic", dawnAuto, "1", channel=channel2) + self.update_state(code, "duskAutomatic", duskAuto, "1", channel=channel2) + self.update_state(code, "manualMode", manualMode, "1", channel=channel2) + self.update_state(code, "modeChange", modeChange, "1", channel=channel2) + self.update_state(code, "stairwellFunction", stairwellFunction, "1", channel=channel2) + self.update_state(code, "stairwellTime", stairwellTime, "1", channel=channel2) + self.update_state(code, "level", level, "1", channel=channel2) + self.update_state(code, "state", state, "1", channel=channel2) # readingsEndUpdate(hash, 1) # Notify is done by Dispatch elif format == "23": pos = int(msg[22:22 + 2], 16) & 0x7F From 041e3c87c80e8bfd558a9cce90d68552dd680de9 Mon Sep 17 00:00:00 2001 From: paulg Date: Sat, 4 Jan 2020 20:50:59 +0100 Subject: [PATCH 18/31] make these shutter related lines channel aware --- pyduofern/duofern.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/pyduofern/duofern.py b/pyduofern/duofern.py index 4ffc5ae..aad45b1 100644 --- a/pyduofern/duofern.py +++ b/pyduofern/duofern.py @@ -1049,19 +1049,23 @@ def set(self, code, cmd, *args, channel: int = None): if subCmd not in commands[cmd]: raise Exception("Wrong argument {}, {}".format(arg, subCmd)) - position = -1 if not "position" in self.modules['by_code'][code] else self.modules['by_code'][code][ - "position"] + channel_suffix = "" + if channel is not None: + channel_suffix = "_" + chanNo + + position = -1 if "position"+channel_suffix not in self.modules['by_code'][code] else \ + self.modules['by_code'][code]["position"+channel_suffix] # toggleUpDown = AttrVal(name, "toggleUpDown", "0") - toggleUpDown = self.modules['by_code'][code]['toggleUpDown'] if 'toggleUpDown' in self.modules['by_code'][ - code] else 0 - moving = "stop" if not "moving" in self.modules['by_code'][code] else self.modules['by_code'][code][ - "moving"] - timeAutomatic = "on" if not "timeAutomatic" in self.modules['by_code'][code] else \ - self.modules['by_code'][code]["timeAutomatic"] - dawnAutomatic = "on" if not "dawnAutomatic" in self.modules['by_code'][code] else \ - self.modules['by_code'][code]["dawnAutomatic"] - duskAutomatic = "on" if not "duskAutomatic" in self.modules['by_code'][code] else \ - self.modules['by_code'][code]["duskAutomatic"] + toggleUpDown = self.modules['by_code'][code]['toggleUpDown'+channel_suffix] \ + if 'toggleUpDown'+channel_suffix in self.modules['by_code'][code] else 0 + moving = "stop" if "moving"+channel_suffix not in self.modules['by_code'][code] else \ + self.modules['by_code'][code]["moving"+channel_suffix] + timeAutomatic = "on" if "timeAutomatic"+channel_suffix not in self.modules['by_code'][code] else \ + self.modules['by_code'][code]["timeAutomatic"+channel_suffix] + dawnAutomatic = "on" if "dawnAutomatic"+channel_suffix not in self.modules['by_code'][code] else \ + self.modules['by_code'][code]["dawnAutomatic"+channel_suffix] + duskAutomatic = "on" if "duskAutomatic"+channel_suffix not in self.modules['by_code'][code] else \ + self.modules['by_code'][code]["duskAutomatic"+channel_suffix] if moving != "stop": if cmd in ('up', 'down', 'toggle'): From edf6db91901313db87e0277f35f33ebde88ae905 Mon Sep 17 00:00:00 2001 From: paulg Date: Sat, 4 Jan 2020 21:02:21 +0100 Subject: [PATCH 19/31] add documentation about breaking change and up version number. --- README.rst | 7 +++++++ pyduofern/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index aa710cf..a873618 100644 --- a/README.rst +++ b/README.rst @@ -167,6 +167,13 @@ commands instead of buying a weather station. Changelog ========= +**0.30.0** +- **breaking change**: instead of creating multiple devices for single physical devices + with multiple actor channels which was rather buggy add a ``channel`` parameter to the + respective functions in pyduofern.duofern.Duofern() which allows to handle channels in + a consistent manner. See discussion in https://github.com/gluap/pyduofern/pull/9 . + For multichannel devices each extra channel is listed in in Duofern().modules['by_code'][code]['channels'] + **0.25.2** - try to fix https://github.com/gluap/pyduofern/issues/2 diff --git a/pyduofern/__init__.py b/pyduofern/__init__.py index d1e09e6..1c03aae 100644 --- a/pyduofern/__init__.py +++ b/pyduofern/__init__.py @@ -22,7 +22,7 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -__version__ = "0.25.2" +__version__ = "0.30.0" __all__ = ['DuofernException', 'DuofernStick', 'DuofernStickAsync', 'duoACK'] From 7ef9eb124662253a2f6bae705253077ac29976c9 Mon Sep 17 00:00:00 2001 From: paulg Date: Sat, 4 Jan 2020 21:05:59 +0100 Subject: [PATCH 20/31] all devices shall have the "channels" set --- pyduofern/duofern.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyduofern/duofern.py b/pyduofern/duofern.py index aad45b1..fa1970e 100644 --- a/pyduofern/duofern.py +++ b/pyduofern/duofern.py @@ -98,7 +98,7 @@ def add_device(self, code, name=None): if name is None: name = len(self.modules['by_code']) logger.debug("adding {}".format(code)) - self.modules['by_code'][code] = {'name': name} + self.modules['by_code'][code] = {'name': name, 'channels': set()} def del_device(self, code, name=None): if name is None: @@ -120,8 +120,6 @@ def update_state(self, code, key, value, trigger=None, channel: int = None): if channel is not None: channel_str = "{:02x}".format(channel) key = key + "_" + channel_str - if 'channels' not in self.modules['by_code'][code]: - self.modules['by_code'][code]['channels'] = set() self.modules['by_code'][code]['channels'].add(channel_str) self.modules['by_code'][code][key] = value if self.changes_callback and trigger: From 3c5b92714b1db9b9b824201f58a132c9b6ac263c Mon Sep 17 00:00:00 2001 From: paulg Date: Sat, 4 Jan 2020 21:08:09 +0100 Subject: [PATCH 21/31] make sure the default channel "None" is contained in the channels set for consistency --- pyduofern/duofern.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyduofern/duofern.py b/pyduofern/duofern.py index fa1970e..929edb6 100644 --- a/pyduofern/duofern.py +++ b/pyduofern/duofern.py @@ -98,7 +98,7 @@ def add_device(self, code, name=None): if name is None: name = len(self.modules['by_code']) logger.debug("adding {}".format(code)) - self.modules['by_code'][code] = {'name': name, 'channels': set()} + self.modules['by_code'][code] = {'name': name, 'channels': {None}} def del_device(self, code, name=None): if name is None: From 0923ab6a25f15e699ccb997e352efd4e9c5e9b2e Mon Sep 17 00:00:00 2001 From: paulg Date: Sat, 4 Jan 2020 21:08:59 +0100 Subject: [PATCH 22/31] make sure the default channel "None" is contained in the channels set for consistency --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index a873618..d031485 100644 --- a/README.rst +++ b/README.rst @@ -172,7 +172,8 @@ Changelog with multiple actor channels which was rather buggy add a ``channel`` parameter to the respective functions in pyduofern.duofern.Duofern() which allows to handle channels in a consistent manner. See discussion in https://github.com/gluap/pyduofern/pull/9 . - For multichannel devices each extra channel is listed in in Duofern().modules['by_code'][code]['channels'] + For each device available channels are listed in in Duofern().modules['by_code'][code]['channels']. + The default channel available for all devices is "None". **0.25.2** - try to fix https://github.com/gluap/pyduofern/issues/2 From b517fbec64c56e5c13b8f6b09eb31cbb6dfb82fe Mon Sep 17 00:00:00 2001 From: paulg Date: Sat, 4 Jan 2020 21:43:18 +0100 Subject: [PATCH 23/31] fix rendering of changelog --- README.rst | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index d031485..d366a02 100644 --- a/README.rst +++ b/README.rst @@ -168,47 +168,54 @@ commands instead of buying a weather station. Changelog ========= **0.30.0** -- **breaking change**: instead of creating multiple devices for single physical devices - with multiple actor channels which was rather buggy add a ``channel`` parameter to the - respective functions in pyduofern.duofern.Duofern() which allows to handle channels in - a consistent manner. See discussion in https://github.com/gluap/pyduofern/pull/9 . - For each device available channels are listed in in Duofern().modules['by_code'][code]['channels']. - The default channel available for all devices is "None". + +- **breaking change**: instead of creating multiple devices for single physical devices with multiple actor channels which was rather buggy add a ``channel`` parameter to the respective functions inpyduofern.duofern.Duofern() which allows to handle channels in a consistent manner. See discussion in https://github.com/gluap/pyduofern/pull/9 . For each device available channels are listed in in Duofern().modules['by_code'][code]['channels']. The default channel available for all devices is ``None``, otherwise an ``int`` is expected. + **0.25.2** + - try to fix https://github.com/gluap/pyduofern/issues/2 **0.25.1** -- changed custom component to fix bug in switch implementation accidentally introduced - recently. + +- changed custom component to fix bug in switch implementation accidentally introduced recently. **0.25** + - restarted from 0.23 to get somewhat working auto detection **0.24** + - somewhat broken changes for auto detection **0.23.5** + - python 3.7 support should enable current hassio version **0.23.3** + - added ``--position`` to CLI **0.23.2** + - renamed README.rst and moved version number from `setup.py` to `__init__.py` **0.23.1** + - fixed references to repository url - upped version for pypi release **0.23** + - added recordings and increased coverage of unit tests (no result-based tests yet though -- just checking if every replay runs until the end without hanging) **0.22** -- Added recording of actions for replay in integration tests -- Improved unit tests -- Enable travis -- Enable coveralls + +* Added recording of actions for replay in integration tests +* Improved unit tests +* Enable travis +* Enable coveralls **0.21.1** + - fixed bug where device IDs containing `cc` would be be messed up when inserting channel number. From c7e157428d2599a175510df0c9e81b66ef1930ed Mon Sep 17 00:00:00 2001 From: Florian Kauer Date: Sat, 4 Jan 2020 22:07:54 +0000 Subject: [PATCH 24/31] ignore 8 digit codes --- pyduofern/duofern_stick.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/pyduofern/duofern_stick.py b/pyduofern/duofern_stick.py index 3da0a77..9435c7d 100644 --- a/pyduofern/duofern_stick.py +++ b/pyduofern/duofern_stick.py @@ -416,13 +416,14 @@ def handshake(self): counter = 0 for device in self.config['devices']: # devices with id other than 6 characters - # are sub-devices of another device with 6 characters + # were previously sub-devices of another device with 6 characters # (i.e. devices representing a single channel) - # and thus do not require SetPairs - if len(device['id']) == 6: - hex_to_write = duoSetPairs.replace('nn', '{:02X}'.format(counter)).replace('yyyyyy', device['id']) - yield from send_and_await_reply(self, hex_to_write, "SetPairs") - yield from self.send(duoACK) + # but are no longer relevant + if len(device['id']) != 6: + continue + hex_to_write = duoSetPairs.replace('nn', '{:02X}'.format(counter)).replace('yyyyyy', device['id']) + yield from send_and_await_reply(self, hex_to_write, "SetPairs") + yield from self.send(duoACK) counter += 1 self.duofern_parser.add_device(device['id'], device['name']) @@ -501,17 +502,19 @@ def _initialize(self): # DoInit counter = 0 for device in self.config['devices']: # devices with id other than 6 characters - # are sub-devices of another device with 6 characters + # were previously sub-devices of another device with 6 characters # (i.e. devices representing a single channel) - # and thus do not require SetPairs - if len(device['id']) == 6: - hex_to_write = duoSetPairs.replace('nn', '{:02X}'.format(counter)).replace('yyyyyy', device['id']) - self._simple_write(hex_to_write) - try: - self._read_answer("SetPairs") - except DuofernTimeoutException: # pragma: no cover - continue - self._simple_write(duoACK) + # but are no longer relevant + if len(device['id']) != 6: + continue + + hex_to_write = duoSetPairs.replace('nn', '{:02X}'.format(counter)).replace('yyyyyy', device['id']) + self._simple_write(hex_to_write) + try: + self._read_answer("SetPairs") + except DuofernTimeoutException: # pragma: no cover + continue + self._simple_write(duoACK) counter += 1 self.duofern_parser.add_device(device['id'], device['name']) From b42ce311f4a27aaea9e91e35cbe1b95d6c1062da Mon Sep 17 00:00:00 2001 From: Florian Kauer Date: Sat, 4 Jan 2020 22:50:39 +0000 Subject: [PATCH 25/31] pass on channel for command --- pyduofern/duofern_stick.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyduofern/duofern_stick.py b/pyduofern/duofern_stick.py index 9435c7d..95e947f 100644 --- a/pyduofern/duofern_stick.py +++ b/pyduofern/duofern_stick.py @@ -325,11 +325,11 @@ def __init__(self, loop=None, *args, **kwargs): # self.running = False @asyncio.coroutine - def command(self, *args): + def command(self, *args, **kwargs): if self.recording: - self.recorder.write("sending_command {}\n".format(args)) + self.recorder.write("sending_command {} {}\n".format(args,kwargs)) self.recorder.flush() - yield from self.duofern_parser.set(*args) + yield from self.duofern_parser.set(*args, **kwargs) def add_serial_and_send(self, msg): message = msg.replace("zzzzzz", "6f" + self.system_code) @@ -564,11 +564,11 @@ def _simple_write(self, string_to_write): # SimpleWrite self.serial_connection.open() self.serial_connection.write(data_to_write) - def command(self, *args): + def command(self, *args, **kwargs): if self.recording: - self.recorder.write("sending_command {}\n".format(args)) + self.recorder.write("sending_command {} {}\n".format(args,kwargs)) self.recorder.flush() - list(self.duofern_parser.set(*args)) + list(self.duofern_parser.set(*args, **kwargs)) @asyncio.coroutine def add_serial_and_send(self, msg): From 4da0675cb37758ab02b4abf53fb00c122f635917 Mon Sep 17 00:00:00 2001 From: Florian Kauer Date: Sat, 4 Jan 2020 23:27:49 +0000 Subject: [PATCH 26/31] update state if channel is None --- pyduofern/duofern.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyduofern/duofern.py b/pyduofern/duofern.py index 929edb6..fee7943 100644 --- a/pyduofern/duofern.py +++ b/pyduofern/duofern.py @@ -121,7 +121,9 @@ def update_state(self, code, key, value, trigger=None, channel: int = None): channel_str = "{:02x}".format(channel) key = key + "_" + channel_str self.modules['by_code'][code]['channels'].add(channel_str) - self.modules['by_code'][code][key] = value + + self.modules['by_code'][code][key] = value + if self.changes_callback and trigger: self.changes_callback() From caa1dc0a7f8dbe0d4271b58314bd0e9280b302af Mon Sep 17 00:00:00 2001 From: Florian Kauer Date: Sat, 4 Jan 2020 23:28:16 +0000 Subject: [PATCH 27/31] get state --- pyduofern/duofern.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyduofern/duofern.py b/pyduofern/duofern.py index fee7943..609b7d8 100644 --- a/pyduofern/duofern.py +++ b/pyduofern/duofern.py @@ -134,6 +134,16 @@ def delete_state(self, code, key, channel: int = None): if key in self.modules['by_code'][code]: del self.modules['by_code'][code][key] + def get_state(self, code, key, channel=None, default=None): + if channel is not None: + channel_str = "{:02x}".format(channel) + key = key + "_" + channel_str + + if not key in self.modules['by_code'][code]: + return default + + return self.modules['by_code'][code][key] + def parse(self, msg): code = msg[30:36] if msg[0:2] == '81': From 1281f018e875b8e2b3e5d24bc29ec5e293700883 Mon Sep 17 00:00:00 2001 From: Florian Kauer Date: Sun, 5 Jan 2020 00:14:46 +0000 Subject: [PATCH 28/31] use whitelist instead of blacklist for cover --- examples/homeassistant/custom_components/duofern/cover.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/homeassistant/custom_components/duofern/cover.py b/examples/homeassistant/custom_components/duofern/cover.py index 7c1a37d..19f1233 100644 --- a/examples/homeassistant/custom_components/duofern/cover.py +++ b/examples/homeassistant/custom_components/duofern/cover.py @@ -19,8 +19,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): stick = hass.data[DOMAIN]['stick'] # Add devices - to_add = [DuofernShutter(device['id'], device['name'], stick, hass) for device in stick.config['devices'] if - not (device['id'].startswith('46') or device['id'].startswith('43')) and not device['id'] in hass.data[DOMAIN]['devices'].keys()] + to_add = [DuofernShutter(device['id'], device['name'], stick, hass) for device in stick.config['devices'] if (device['id'].startswith('40') or device['id'].startswith('41') or device['id'].startswith('42') or device['id'].startswith('47') or device['id'].startswith('49') or device['id'].startswith('61')) and not device['id'] in hass.data[DOMAIN]['devices'].keys()] add_devices(to_add) From 437d600f053b7000876a37cbb4fec0425f74abc1 Mon Sep 17 00:00:00 2001 From: Florian Kauer Date: Sun, 5 Jan 2020 00:16:25 +0000 Subject: [PATCH 29/31] add dual channel support for light component --- .../custom_components/duofern/__init__.py | 2 +- .../custom_components/duofern/light.py | 49 +++++++++++++------ .../custom_components/duofern/manifest.json | 4 +- 3 files changed, 36 insertions(+), 19 deletions(-) diff --git a/examples/homeassistant/custom_components/duofern/__init__.py b/examples/homeassistant/custom_components/duofern/__init__.py index f25304c..f11c88f 100644 --- a/examples/homeassistant/custom_components/duofern/__init__.py +++ b/examples/homeassistant/custom_components/duofern/__init__.py @@ -11,7 +11,7 @@ # Import the device class from the component that you want to support # Home Assistant depends on 3rd party packages for API specific code. -REQUIREMENTS = ['pyduofern==0.25.2'] +REQUIREMENTS = ['pyduofern==0.30.0'] _LOGGER = logging.getLogger(__name__) diff --git a/examples/homeassistant/custom_components/duofern/light.py b/examples/homeassistant/custom_components/duofern/light.py index 1cfc6cb..c0e1da8 100644 --- a/examples/homeassistant/custom_components/duofern/light.py +++ b/examples/homeassistant/custom_components/duofern/light.py @@ -31,20 +31,36 @@ def setup_platform(hass, config, add_devices, discovery_info=None): stick = hass.data["duofern"]['stick'] # Add devices - to_add = [DuofernLight(device['id'], device['name'], stick, hass) for device in stick.config['devices'] if - (device['id'].startswith('46') or (device['id'].startswith('43') and len(device['id']) == 8)) and not device['id'] in hass.data[DOMAIN]['devices'].keys()] - add_devices(to_add) - + for device in stick.config['devices']: + if device['id'].startswith('46') or device['id'].startswith('48'): + if device['id'] in hass.data[DOMAIN]['devices'].keys(): + continue + add_devices([DuofernLight(device['id'], device['name'], stick, hass)]) + + if device['id'].startswith('43'): + for channel in [1,2]: + chanNo = "{:02x}".format(channel) + if device['id']+chanNo in hass.data[DOMAIN]['devices'].keys(): + continue + add_devices([DuofernLight(device['id'], device['name'], stick, hass, channel=channel)]) class DuofernLight(Light): - def __init__(self, id, desc, stick, hass): - """Initialize the shutter.""" - self._id = id + def __init__(self, code, desc, stick, hass, channel=None): + """Initialize the light.""" + self._code = code + self._id = code self._name = desc + + if channel: + chanNo = "{:02x}".format(channel) + self._id += chanNo + self._name += chanNo + self._state = None self._brightness = None self._stick = stick - hass.data[DOMAIN]['devices'][id] = self + self._channel = channel + hass.data[DOMAIN]['devices'][self._id] = self @property def name(self): @@ -53,8 +69,9 @@ def name(self): @property def is_on(self): try: - _LOGGER.info(self._stick.duofern_parser.modules['by_code'][self._id]) - return self._stick.duofern_parser.modules['by_code'][self._id]['state'] == "on" + _LOGGER.info(self._stick.duofern_parser.modules['by_code'][self._code]) + state = self._stick.duofern_parser.get_state(self._code, 'state', channel=self._channel) + return state == "on" except KeyError: return None @@ -63,14 +80,14 @@ def unique_id(self): return self._id def turn_on(self): - self._stick.command(self._id, "on") - # this is a hotfix because currently the state is not correctly detected from duofern - self._stick.duofern_parser.modules['by_code'][self._id]['state'] = "on" + self._stick.command(self._code, "on", channel=self._channel) + # this is a hotfix because currently the state is detected with delay from duofern + self._stick.duofern_parser.update_state(self._code, 'state', "on", channel=self._channel) def turn_off(self): - self._stick.command(self._id, "off") - # this is a hotfix because currently the state is not correctly detected from duofern - self._stick.duofern_parser.modules['by_code'][self._id]['state'] = 0 + self._stick.command(self._code, "off", channel=self._channel) + # this is a hotfix because currently the state is detected with delay from duofern + self._stick.duofern_parser.update_state(self._code, 'state', "off", channel=self._channel) def update(self): pass diff --git a/examples/homeassistant/custom_components/duofern/manifest.json b/examples/homeassistant/custom_components/duofern/manifest.json index f56d021..a44c87c 100644 --- a/examples/homeassistant/custom_components/duofern/manifest.json +++ b/examples/homeassistant/custom_components/duofern/manifest.json @@ -5,5 +5,5 @@ "dependencies": [], "config_flow": true, "codeowners": ["@gluap"], - "requirements": ["pyduofern==0.25.2"] -} \ No newline at end of file + "requirements": ["pyduofern==0.30.0"] +} From 083aaa9d82c0360ffa3d50fd97cd49168d426937 Mon Sep 17 00:00:00 2001 From: Florian Kauer Date: Mon, 6 Jan 2020 09:27:50 +0100 Subject: [PATCH 30/31] dual actor record --- .../duofern_record_1578298202.5071442 | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 pyduofern/tests/replaydata/duofern_record_1578298202.5071442 diff --git a/pyduofern/tests/replaydata/duofern_record_1578298202.5071442 b/pyduofern/tests/replaydata/duofern_record_1578298202.5071442 new file mode 100644 index 0000000..c0c2c5e --- /dev/null +++ b/pyduofern/tests/replaydata/duofern_record_1578298202.5071442 @@ -0,0 +1,48 @@ +sent 01000000000000000000000000000000000000000000 +received 81000000000000000000000000000000000000000000 +sent 0E000000000000000000000000000000000000000000 +received 81000000000000000000000000000000000000000000 +sent 0A6fffff000100000000000000000000000000000000 +received 816fffff010000000000000000000000000000000000 +sent 81000000000000000000000000000000000000000000 +sent 14140000000000000000000000000000000000000000 +received 81140000000000000000000000000000000000000000 +sent 81000000000000000000000000000000000000000000 +sent 03004376c90000000000000000000000000000000000 +received 81004376c90000000000000000000000000000000000 +sent 81000000000000000000000000000000000000000000 +sent 10010000000000000000000000000000000000000000 +received 81010000000000000000000000000000000000000000 +sent 81000000000000000000000000000000000000000000 +sent 0DFF0F400000000000000000000000000000FFFFFF01 +received 810000000000000000000000000000000000ffffff01 +sent 81000000000000000000000000000000000000000000 +sent 81000000000000000000000000000000000000000000 +received 0fff0f2207080101070864643300004376c96fffff01 +sending_command ('4376c9', 'off') {'channel': 1} +sent 0D010E0200000000000000000000006fffff4376c900 +sent 81000000000000000000000000000000000000000000 +received 0fff0f2207080101070864003300004376c9ffffff01 +sent 81000000000000000000000000000000000000000000 +received 810100bb00000000000000000000006fffff4376c900 +sending_command ('4376c9', 'off') {'channel': 2} +sent 0D020E0200000000000000000000006fffff4376c900 +sent 81000000000000000000000000000000000000000000 +received 0fff0f2207080101070800003300004376c9ffffff01 +sent 81000000000000000000000000000000000000000000 +received 810100bb00000000000000000000006fffff4376c900 +sending_command ('4376c9', 'on') {'channel': 1} +sent 0D010E0300000000000000000000006fffff4376c900 +sent 81000000000000000000000000000000000000000000 +received 810003cc00000000000000000000006fffff4376c900 +sent 81000000000000000000000000000000000000000000 +received 0fff0f2207080101070800643300004376c9ffffff01 +sending_command ('4376c9', 'on') {'channel': 2} +sent 0D020E0300000000000000000000006fffff4376c900 +sent 81000000000000000000000000000000000000000000 +received 0fff0f2207080101070864643300004376c9ffffff01 +sent 81000000000000000000000000000000000000000000 +received 0fff0f2207080101070800643300004376c9ffffff01 +sent 81000000000000000000000000000000000000000000 +received 0fff0f2207080101070800003300004376c9ffffff01 + From a1a7161a33da6d3fcbc6bf254577c56754197842 Mon Sep 17 00:00:00 2001 From: paulg Date: Mon, 6 Jan 2020 21:46:18 +0100 Subject: [PATCH 31/31] enable replays with kwargs and reorder/fiddle with replay of switches to make it work (the replay mechanism gets confused if an "ack" is sent or received out of order as it expects the order to exactly match the written record) --- .../duofern_record_1528034711.6005006 | 6 +++--- .../duofern_record_1578298202.5071442 | 17 +++++++---------- pyduofern/tests/test_replay.py | 9 +++++---- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/pyduofern/tests/replaydata/duofern_record_1528034711.6005006 b/pyduofern/tests/replaydata/duofern_record_1528034711.6005006 index 4cb2c35..03ad986 100644 --- a/pyduofern/tests/replaydata/duofern_record_1528034711.6005006 +++ b/pyduofern/tests/replaydata/duofern_record_1528034711.6005006 @@ -33,7 +33,7 @@ received 0fff0f230f000020501e199e1300ff700fecffffff01 sent 81000000000000000000000000000000000000000000 received 0fff0f230f000020501e198a1300ff700fecffffff01 sent 81000000000000000000000000000000000000000000 -sending_command ('409882', 'position', 63) +sending_command ('409882', 'position', 63) {'channel': None} sent 0D010707003f0000000000000000006fffff40988200 received 810003cc00000000000000000000006fffff40988200 sent 81000000000000000000000000000000000000000000 @@ -41,11 +41,11 @@ received 0fff0f210d0864000000413f110000409882ffffff01 sent 81000000000000000000000000000000000000000000 received 0fff0f210d08640000004100110003408ea2ffffff01 sent 81000000000000000000000000000000000000000000 -sending_command ('409b21', 'position', 85) +sending_command ('409b21', 'position', 85) {'channel': None} sent 0D01070700550000000000000000006fffff409b2100 received 810003cc00000000000000000000006fffff409b2100 sent 81000000000000000000000000000000000000000000 -sending_command ('408e94', 'position', 85) +sending_command ('408e94', 'position', 85) {'channel': None} sent 0D01070700550000000000000000006fffff408e9400 received 810003cc00000000000000000000006fffff408e9400 sent 81000000000000000000000000000000000000000000 diff --git a/pyduofern/tests/replaydata/duofern_record_1578298202.5071442 b/pyduofern/tests/replaydata/duofern_record_1578298202.5071442 index c0c2c5e..9882068 100644 --- a/pyduofern/tests/replaydata/duofern_record_1578298202.5071442 +++ b/pyduofern/tests/replaydata/duofern_record_1578298202.5071442 @@ -3,43 +3,40 @@ received 81000000000000000000000000000000000000000000 sent 0E000000000000000000000000000000000000000000 received 81000000000000000000000000000000000000000000 sent 0A6fffff000100000000000000000000000000000000 -received 816fffff010000000000000000000000000000000000 +received 81000000000000000000000000000000000000000000 sent 81000000000000000000000000000000000000000000 sent 14140000000000000000000000000000000000000000 -received 81140000000000000000000000000000000000000000 -sent 81000000000000000000000000000000000000000000 -sent 03004376c90000000000000000000000000000000000 -received 81004376c90000000000000000000000000000000000 +received 81000000000000000000000000000000000000000000 sent 81000000000000000000000000000000000000000000 sent 10010000000000000000000000000000000000000000 -received 81010000000000000000000000000000000000000000 +received 81000000000000000000000000000000000000000000 sent 81000000000000000000000000000000000000000000 sent 0DFF0F400000000000000000000000000000FFFFFF01 received 810000000000000000000000000000000000ffffff01 sent 81000000000000000000000000000000000000000000 sent 81000000000000000000000000000000000000000000 received 0fff0f2207080101070864643300004376c96fffff01 +sent 81000000000000000000000000000000000000000000 sending_command ('4376c9', 'off') {'channel': 1} sent 0D010E0200000000000000000000006fffff4376c900 -sent 81000000000000000000000000000000000000000000 received 0fff0f2207080101070864003300004376c9ffffff01 sent 81000000000000000000000000000000000000000000 received 810100bb00000000000000000000006fffff4376c900 +sent 81000000000000000000000000000000000000000000 sending_command ('4376c9', 'off') {'channel': 2} sent 0D020E0200000000000000000000006fffff4376c900 -sent 81000000000000000000000000000000000000000000 received 0fff0f2207080101070800003300004376c9ffffff01 sent 81000000000000000000000000000000000000000000 received 810100bb00000000000000000000006fffff4376c900 +sent 81000000000000000000000000000000000000000000 sending_command ('4376c9', 'on') {'channel': 1} sent 0D010E0300000000000000000000006fffff4376c900 -sent 81000000000000000000000000000000000000000000 received 810003cc00000000000000000000006fffff4376c900 sent 81000000000000000000000000000000000000000000 received 0fff0f2207080101070800643300004376c9ffffff01 +sent 81000000000000000000000000000000000000000000 sending_command ('4376c9', 'on') {'channel': 2} sent 0D020E0300000000000000000000006fffff4376c900 -sent 81000000000000000000000000000000000000000000 received 0fff0f2207080101070864643300004376c9ffffff01 sent 81000000000000000000000000000000000000000000 received 0fff0f2207080101070800643300004376c9ffffff01 diff --git a/pyduofern/tests/test_replay.py b/pyduofern/tests/test_replay.py index 0e0ce37..e8c31a3 100644 --- a/pyduofern/tests/test_replay.py +++ b/pyduofern/tests/test_replay.py @@ -30,6 +30,7 @@ import tempfile import time import sys +from ast import literal_eval import pytest @@ -117,10 +118,10 @@ def receive_loop(self): @asyncio.coroutine def actions(self): while self.next_is_action(): - command_args = self.next_line()[1:] - command_args_ = [re.sub("[,()']?", "", arg) for arg in command_args] - command_args_[1:] = [int(arg) if re.match("^[0-9]+$", arg) else arg for arg in command_args_[1:]] - yield from self.proto.command(*command_args_) + command_args = " ".join(self.next_line()[1:]) + args_and_kwargs = re.match(r"(\([^\)]+\)).*({[^}]+})", command_args) + args, kwargs = args_and_kwargs.groups() + yield from self.proto.command(*literal_eval(args),**literal_eval(kwargs)) def check_if_next_matches(self, data): logger.warning("writing {} detected by mock writer".format(data))