From e13f02a88119ce8025e1561f95ca3b6aa063678e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Fri, 27 Aug 2021 02:04:48 +0100 Subject: [PATCH 01/11] send update dps command with heartbeat --- .../localtuya/pytuya/__init__.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index 789091c7b..2c613b426 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -61,6 +61,7 @@ SET = "set" STATUS = "status" HEARTBEAT = "heartbeat" +UPDATEDPS = "updatedps" # Request refresh of DPS PROTOCOL_VERSION_BYTES_31 = b"3.1" PROTOCOL_VERSION_BYTES_33 = b"3.3" @@ -90,11 +91,13 @@ STATUS: {"hexByte": 0x0A, "command": {"gwId": "", "devId": ""}}, SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}}, HEARTBEAT: {"hexByte": 0x09, "command": {}}, + UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [18, 19, 20]}}, }, "type_0d": { STATUS: {"hexByte": 0x0D, "command": {"devId": "", "uid": "", "t": ""}}, SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}}, HEARTBEAT: {"hexByte": 0x09, "command": {}}, + UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [18, 19, 20]}}, }, } @@ -379,6 +382,7 @@ async def heartbeat_loop(): while True: try: await self.heartbeat() + await self.updatedps() await asyncio.sleep(HEARTBEAT_INTERVAL) except asyncio.CancelledError: self.debug("Stopped heartbeat loop") @@ -478,6 +482,16 @@ async def heartbeat(self): """Send a heartbeat message.""" return await self.exchange(HEARTBEAT) + async def updatedps(self): + """ + Request device to update index. + Args: + index(array): list of dps to update (ex. [4, 5, 6, 18, 19, 20]) + """ + self.debug('updatedps() entry (dev_type is %s)', self.dev_type) + payload = self._generate_payload(UPDATEDPS) + self.transport.write(payload) + async def set_dp(self, value, dp_index): """ Set value (may be any type: bool, int or string) of any dps index. @@ -582,7 +596,10 @@ def _generate_payload(self, command, data=None): json_data["t"] = str(int(time.time())) if data is not None: - json_data["dps"] = data + if "dpId" in json_data: + json_data["dpId"] = data + else: + json_data["dps"] = data elif command_hb == 0x0D: json_data["dps"] = self.dps_to_request @@ -591,7 +608,7 @@ def _generate_payload(self, command, data=None): if self.version == 3.3: payload = self.cipher.encrypt(payload, False) - if command_hb != 0x0A: + if command_hb != 0x0A and command_hb != 0x12: # add the 3.3 header payload = PROTOCOL_33_HEADER + payload elif command == SET: From 24152569e7393a70403052df82d1566244866d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Tue, 31 Aug 2021 13:49:39 +0100 Subject: [PATCH 02/11] update state only on changes --- custom_components/localtuya/common.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index 88b434f06..2573b1afb 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -234,13 +234,13 @@ async def async_added_to_hass(self): def _update_handler(status): """Update entity state when status was updated.""" - if status is not None: - self._status = status - self.status_updated() - else: - self._status = {} - - self.schedule_update_ha_state() + if status is None: + status = {} + if self._status != status: + self._status = status.copy() + if status: + self.status_updated() + self.schedule_update_ha_state() signal = f"localtuya_{self._config_entry.data[CONF_DEVICE_ID]}" self.async_on_remove( From 47c4edc20d6e4ed6b2848b8df9becf83a6fc8b94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Tue, 23 Nov 2021 21:20:43 +0000 Subject: [PATCH 03/11] fix lint issues --- custom_components/localtuya/pytuya/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index 2c613b426..90ac8ed39 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -61,7 +61,7 @@ SET = "set" STATUS = "status" HEARTBEAT = "heartbeat" -UPDATEDPS = "updatedps" # Request refresh of DPS +UPDATEDPS = "updatedps" # Request refresh of DPS PROTOCOL_VERSION_BYTES_31 = b"3.1" PROTOCOL_VERSION_BYTES_33 = b"3.3" @@ -485,10 +485,11 @@ async def heartbeat(self): async def updatedps(self): """ Request device to update index. + Args: index(array): list of dps to update (ex. [4, 5, 6, 18, 19, 20]) """ - self.debug('updatedps() entry (dev_type is %s)', self.dev_type) + self.debug("updatedps() entry (dev_type is %s)", self.dev_type) payload = self._generate_payload(UPDATEDPS) self.transport.write(payload) @@ -608,7 +609,7 @@ def _generate_payload(self, command, data=None): if self.version == 3.3: payload = self.cipher.encrypt(payload, False) - if command_hb != 0x0A and command_hb != 0x12: + if command_hb not in [0x0A, 0x12]: # add the 3.3 header payload = PROTOCOL_33_HEADER + payload elif command == SET: From 515b3303e9042322fe185db11ae5cac85fe5dfae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Tue, 23 Nov 2021 22:16:30 +0000 Subject: [PATCH 04/11] add polling option --- custom_components/localtuya/common.py | 21 ++++++++++++++++++- custom_components/localtuya/config_flow.py | 4 ++++ .../localtuya/pytuya/__init__.py | 1 - custom_components/localtuya/strings.json | 3 ++- .../localtuya/translations/en.json | 4 +++- 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index 2573b1afb..30028af10 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -1,6 +1,7 @@ """Code shared between all platforms.""" import asyncio import logging +from datetime import timedelta from homeassistant.const import ( CONF_DEVICE_ID, @@ -9,8 +10,10 @@ CONF_HOST, CONF_ID, CONF_PLATFORM, + CONF_SCAN_INTERVAL, ) from homeassistant.core import callback +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -116,6 +119,7 @@ def __init__(self, hass, config_entry): self.dps_to_request = {} self._is_closing = False self._connect_task = None + self._unsub_interval = None self.set_logger(_LOGGER, config_entry[CONF_DEVICE_ID]) # This has to be done in case the device type is type_0d @@ -151,6 +155,15 @@ async def _make_connection(self): raise Exception("Failed to retrieve status") self.status_updated(status) + if ( + CONF_SCAN_INTERVAL in self._config_entry + and self._config_entry[CONF_SCAN_INTERVAL] > 0 + ): + self._unsub_interval = async_track_time_interval( + self._hass, + self._async_refresh, + timedelta(seconds=self._config_entry[CONF_SCAN_INTERVAL]), + ) except Exception: # pylint: disable=broad-except self.exception(f"Connect to {self._config_entry[CONF_HOST]} failed") if self._interface is not None: @@ -158,6 +171,10 @@ async def _make_connection(self): self._interface = None self._connect_task = None + async def _async_refresh(self, _now): + if self._interface is not None: + await self._interface.updatedps() + async def close(self): """Close connection and stop re-connect loop.""" self._is_closing = True @@ -204,7 +221,9 @@ def disconnected(self): """Device disconnected.""" signal = f"localtuya_{self._config_entry[CONF_DEVICE_ID]}" async_dispatcher_send(self._hass, signal, None) - + if self._unsub_interval is not None: + self._unsub_interval() + self._unsub_interval = None self._interface = None self.debug("Disconnected - waiting for discovery broadcast") diff --git a/custom_components/localtuya/config_flow.py b/custom_components/localtuya/config_flow.py index 731b60acf..03bf80d24 100644 --- a/custom_components/localtuya/config_flow.py +++ b/custom_components/localtuya/config_flow.py @@ -14,6 +14,7 @@ CONF_HOST, CONF_ID, CONF_PLATFORM, + CONF_SCAN_INTERVAL, ) from homeassistant.core import callback @@ -44,6 +45,7 @@ vol.Required(CONF_HOST): str, vol.Required(CONF_DEVICE_ID): str, vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]), + vol.Optional(CONF_SCAN_INTERVAL): int, } ) @@ -55,6 +57,7 @@ vol.Required(CONF_LOCAL_KEY): cv.string, vol.Required(CONF_FRIENDLY_NAME): cv.string, vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]), + vol.Optional(CONF_SCAN_INTERVAL): int, } ) @@ -90,6 +93,7 @@ def options_schema(entities): vol.Required(CONF_HOST): str, vol.Required(CONF_LOCAL_KEY): str, vol.Required(CONF_PROTOCOL_VERSION, default="3.3"): vol.In(["3.1", "3.3"]), + vol.Optional(CONF_SCAN_INTERVAL): int, vol.Required( CONF_ENTITIES, description={"suggested_value": entity_names} ): cv.multi_select(entity_names), diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index 90ac8ed39..f03375707 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -382,7 +382,6 @@ async def heartbeat_loop(): while True: try: await self.heartbeat() - await self.updatedps() await asyncio.sleep(HEARTBEAT_INTERVAL) except asyncio.CancelledError: self.debug("Stopped heartbeat loop") diff --git a/custom_components/localtuya/strings.json b/custom_components/localtuya/strings.json index 898b99d48..8a2c03fb7 100644 --- a/custom_components/localtuya/strings.json +++ b/custom_components/localtuya/strings.json @@ -20,6 +20,7 @@ "device_id": "Device ID", "local_key": "Local key", "protocol_version": "Protocol Version", + "scan_interval": "Scan interval in seconds (fill only if the device is not updating automatically)", "device_type": "Device type" } }, @@ -39,4 +40,4 @@ } }, "title": "LocalTuya" -} \ No newline at end of file +} diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 6ce233a41..34826c455 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -29,7 +29,8 @@ "host": "Host", "device_id": "Device ID", "local_key": "Local key", - "protocol_version": "Protocol Version" + "protocol_version": "Protocol Version", + "scan_interval": "Scan interval in seconds (fill only if the device is not updating automatically)" } }, "pick_entity_type": { @@ -89,6 +90,7 @@ "host": "Host", "local_key": "Local key", "protocol_version": "Protocol Version", + "scan_interval": "Scan interval in seconds (fill only if the device is not updating automatically)", "entities": "Entities (uncheck an entity to remove it)" } }, From 9c6de40731716baa2b51854b292095db5bf77887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Fri, 26 Nov 2021 23:47:00 +0000 Subject: [PATCH 05/11] handle updatedps response --- .../localtuya/pytuya/__init__.py | 59 ++++++++++++------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index f03375707..ccde438ea 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -91,13 +91,13 @@ STATUS: {"hexByte": 0x0A, "command": {"gwId": "", "devId": ""}}, SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}}, HEARTBEAT: {"hexByte": 0x09, "command": {}}, - UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [18, 19, 20]}}, + UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [4, 5, 6, 18, 19, 20]}}, }, "type_0d": { STATUS: {"hexByte": 0x0D, "command": {"devId": "", "uid": "", "t": ""}}, SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}}, HEARTBEAT: {"hexByte": 0x09, "command": {}}, - UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [18, 19, 20]}}, + UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [4, 5, 6, 18, 19, 20]}}, }, } @@ -210,9 +210,11 @@ def _unpad(data): class MessageDispatcher(ContextualLogger): """Buffer and dispatcher for Tuya messages.""" - # Heartbeats always respond with sequence number 0, so they can't be waited for like - # other messages. This is a hack to allow waiting for heartbeats. + # Heartbeats and updatedps always respond with sequence number 0, + # so they can't be waited for like other messages. + # This is a hack to allow waiting for them. HEARTBEAT_SEQNO = -100 + UPDATEDPS_SEQNO = -101 def __init__(self, dev_id, listener): """Initialize a new MessageBuffer.""" @@ -295,9 +297,28 @@ def _dispatch(self, msg): sem = self.listeners[self.HEARTBEAT_SEQNO] self.listeners[self.HEARTBEAT_SEQNO] = msg sem.release() + elif msg.cmd == 0x12: + self.debug("Got normal updatedps response") + if self.UPDATEDPS_SEQNO in self.listeners: + sem = self.listeners[self.UPDATEDPS_SEQNO] + self.listeners[self.UPDATEDPS_SEQNO] = msg + if isinstance(sem, asyncio.Semaphore): + sem.release() elif msg.cmd == 0x08: - self.debug("Got status update") - self.listener(msg) + # If we have an open updatedps call then this is for it. + # Some devices send 0x12 and 0x08 in response to a updatedps. + # Empty DPS responses here are always for updatedps + # but hey we haven't decoded yet to know + if self.UPDATEDPS_SEQNO in self.listeners and isinstance( + self.listeners[self.UPDATEDPS_SEQNO], asyncio.Semaphore + ): + self.debug("Got status type updatedps response") + sem = self.listeners[self.UPDATEDPS_SEQNO] + self.listeners[self.UPDATEDPS_SEQNO] = msg + sem.release() + else: + self.debug("Got status update") + self.listener(msg) else: self.debug( "Got message type %d for unknown listener %d: %s", @@ -443,12 +464,13 @@ async def exchange(self, command, dps=None): payload = self._generate_payload(command, dps) dev_type = self.dev_type - # Wait for special sequence number if heartbeat - seqno = ( - MessageDispatcher.HEARTBEAT_SEQNO - if command == HEARTBEAT - else (self.seqno - 1) - ) + # Wait for special sequence number if heartbeat or updatedps + if command == HEARTBEAT: + seqno = MessageDispatcher.HEARTBEAT_SEQNO + elif command == UPDATEDPS: + seqno = MessageDispatcher.UPDATEDPS_SEQNO + else: + seqno = self.seqno - 1 self.transport.write(payload) msg = await self.dispatcher.wait_for(seqno) @@ -482,15 +504,10 @@ async def heartbeat(self): return await self.exchange(HEARTBEAT) async def updatedps(self): - """ - Request device to update index. - - Args: - index(array): list of dps to update (ex. [4, 5, 6, 18, 19, 20]) - """ - self.debug("updatedps() entry (dev_type is %s)", self.dev_type) - payload = self._generate_payload(UPDATEDPS) - self.transport.write(payload) + """Request device to update index.""" + if self.version == 3.3: + return await self.exchange(UPDATEDPS) + return True async def set_dp(self, value, dp_index): """ From e292524793c10bb3d5f78dfc1b60ca32aa303def Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Sat, 27 Nov 2021 17:30:15 +0000 Subject: [PATCH 06/11] shorten label text --- custom_components/localtuya/strings.json | 2 +- custom_components/localtuya/translations/en.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/custom_components/localtuya/strings.json b/custom_components/localtuya/strings.json index 8a2c03fb7..b8bedc8be 100644 --- a/custom_components/localtuya/strings.json +++ b/custom_components/localtuya/strings.json @@ -20,7 +20,7 @@ "device_id": "Device ID", "local_key": "Local key", "protocol_version": "Protocol Version", - "scan_interval": "Scan interval in seconds (fill only if the device is not updating automatically)", + "scan_interval": "Scan interval (seconds, only when not updating automatically)", "device_type": "Device type" } }, diff --git a/custom_components/localtuya/translations/en.json b/custom_components/localtuya/translations/en.json index 34826c455..8a0327902 100644 --- a/custom_components/localtuya/translations/en.json +++ b/custom_components/localtuya/translations/en.json @@ -30,7 +30,7 @@ "device_id": "Device ID", "local_key": "Local key", "protocol_version": "Protocol Version", - "scan_interval": "Scan interval in seconds (fill only if the device is not updating automatically)" + "scan_interval": "Scan interval (seconds, only when not updating automatically)" } }, "pick_entity_type": { @@ -90,7 +90,7 @@ "host": "Host", "local_key": "Local key", "protocol_version": "Protocol Version", - "scan_interval": "Scan interval in seconds (fill only if the device is not updating automatically)", + "scan_interval": "Scan interval (seconds, only when not updating automatically)", "entities": "Entities (uncheck an entity to remove it)" } }, From d6e7c7dec433c1c89e33efe6cb100da8f399d35d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Sun, 28 Nov 2021 01:46:12 +0000 Subject: [PATCH 07/11] send updatedps with all detected dps by default --- custom_components/localtuya/common.py | 2 +- custom_components/localtuya/pytuya/__init__.py | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index 30028af10..badfabcbe 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -173,7 +173,7 @@ async def _make_connection(self): async def _async_refresh(self, _now): if self._interface is not None: - await self._interface.updatedps() + await self._interface.update_dps() async def close(self): """Close connection and stop re-connect loop.""" diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index ccde438ea..24b026816 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -21,6 +21,7 @@ json = status() # returns json payload set_version(version) # 3.1 [default] or 3.3 detect_available_dps() # returns a list of available dps provided by the device + update_dps(dps) # sends update dps command add_dps_to_request(dp_index) # adds dp_index to the list of dps used by the # device (to be queried in the payload) set_dp(on, dp_index) # Set value of any dps index. @@ -503,10 +504,20 @@ async def heartbeat(self): """Send a heartbeat message.""" return await self.exchange(HEARTBEAT) - async def updatedps(self): - """Request device to update index.""" + async def update_dps(self, dps=None): + """ + Request device to update index. + + Args: + dps([int]): list of dps to update, default=all detected + """ if self.version == 3.3: - return await self.exchange(UPDATEDPS) + if dps is None: + if not self.dps_cache: + await self.detect_available_dps() + if self.dps_cache: + dps = [int(dp) for dp in self.dps_cache][:255] + return await self.exchange(UPDATEDPS, dps) return True async def set_dp(self, value, dp_index): From 23e48b791a995f5757155c63953861074c0571e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Sun, 28 Nov 2021 10:46:28 +0000 Subject: [PATCH 08/11] revert defaulting to all dps --- custom_components/localtuya/pytuya/__init__.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index 24b026816..ecd0b7d39 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -509,14 +509,9 @@ async def update_dps(self, dps=None): Request device to update index. Args: - dps([int]): list of dps to update, default=all detected + dps([int]): list of dps to update """ if self.version == 3.3: - if dps is None: - if not self.dps_cache: - await self.detect_available_dps() - if self.dps_cache: - dps = [int(dp) for dp in self.dps_cache][:255] return await self.exchange(UPDATEDPS, dps) return True From 3cbe6751d3ae9f26136c82811cb2b7a5e23478e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Mon, 29 Nov 2021 03:39:22 +0000 Subject: [PATCH 09/11] revert changes, dont wait for response but use all detected dps --- .../localtuya/pytuya/__init__.py | 55 +++++++------------ 1 file changed, 21 insertions(+), 34 deletions(-) diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index ecd0b7d39..16ae2719c 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -92,13 +92,13 @@ STATUS: {"hexByte": 0x0A, "command": {"gwId": "", "devId": ""}}, SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}}, HEARTBEAT: {"hexByte": 0x09, "command": {}}, - UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [4, 5, 6, 18, 19, 20]}}, + UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [18, 19, 20]}}, }, "type_0d": { STATUS: {"hexByte": 0x0D, "command": {"devId": "", "uid": "", "t": ""}}, SET: {"hexByte": 0x07, "command": {"devId": "", "uid": "", "t": ""}}, HEARTBEAT: {"hexByte": 0x09, "command": {}}, - UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [4, 5, 6, 18, 19, 20]}}, + UPDATEDPS: {"hexByte": 0x12, "command": {"dpId": [18, 19, 20]}}, }, } @@ -211,11 +211,9 @@ def _unpad(data): class MessageDispatcher(ContextualLogger): """Buffer and dispatcher for Tuya messages.""" - # Heartbeats and updatedps always respond with sequence number 0, - # so they can't be waited for like other messages. - # This is a hack to allow waiting for them. + # Heartbeats always respond with sequence number 0, so they can't be waited for like + # other messages. This is a hack to allow waiting for heartbeats. HEARTBEAT_SEQNO = -100 - UPDATEDPS_SEQNO = -101 def __init__(self, dev_id, listener): """Initialize a new MessageBuffer.""" @@ -300,26 +298,9 @@ def _dispatch(self, msg): sem.release() elif msg.cmd == 0x12: self.debug("Got normal updatedps response") - if self.UPDATEDPS_SEQNO in self.listeners: - sem = self.listeners[self.UPDATEDPS_SEQNO] - self.listeners[self.UPDATEDPS_SEQNO] = msg - if isinstance(sem, asyncio.Semaphore): - sem.release() elif msg.cmd == 0x08: - # If we have an open updatedps call then this is for it. - # Some devices send 0x12 and 0x08 in response to a updatedps. - # Empty DPS responses here are always for updatedps - # but hey we haven't decoded yet to know - if self.UPDATEDPS_SEQNO in self.listeners and isinstance( - self.listeners[self.UPDATEDPS_SEQNO], asyncio.Semaphore - ): - self.debug("Got status type updatedps response") - sem = self.listeners[self.UPDATEDPS_SEQNO] - self.listeners[self.UPDATEDPS_SEQNO] = msg - sem.release() - else: - self.debug("Got status update") - self.listener(msg) + self.debug("Got status update") + self.listener(msg) else: self.debug( "Got message type %d for unknown listener %d: %s", @@ -465,13 +446,12 @@ async def exchange(self, command, dps=None): payload = self._generate_payload(command, dps) dev_type = self.dev_type - # Wait for special sequence number if heartbeat or updatedps - if command == HEARTBEAT: - seqno = MessageDispatcher.HEARTBEAT_SEQNO - elif command == UPDATEDPS: - seqno = MessageDispatcher.UPDATEDPS_SEQNO - else: - seqno = self.seqno - 1 + # Wait for special sequence number if heartbeat + seqno = ( + MessageDispatcher.HEARTBEAT_SEQNO + if command == HEARTBEAT + else (self.seqno - 1) + ) self.transport.write(payload) msg = await self.dispatcher.wait_for(seqno) @@ -509,10 +489,17 @@ async def update_dps(self, dps=None): Request device to update index. Args: - dps([int]): list of dps to update + dps([int]): list of dps to update, default=all detected """ if self.version == 3.3: - return await self.exchange(UPDATEDPS, dps) + if dps is None: + if not self.dps_cache: + await self.detect_available_dps() + if self.dps_cache: + dps = [int(dp) for dp in self.dps_cache][:255] + self.debug("updatedps() entry (dps %s)", dps) + payload = self._generate_payload(UPDATEDPS, dps) + self.transport.write(payload) return True async def set_dp(self, value, dp_index): From 943bfa532e1af6ac8c2b3d418fe944db772a77a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Tue, 30 Nov 2021 17:08:47 +0000 Subject: [PATCH 10/11] log dps_cache --- custom_components/localtuya/pytuya/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index 16ae2719c..f9618e01d 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -497,7 +497,7 @@ async def update_dps(self, dps=None): await self.detect_available_dps() if self.dps_cache: dps = [int(dp) for dp in self.dps_cache][:255] - self.debug("updatedps() entry (dps %s)", dps) + self.debug("updatedps() entry (dps_cache %s)", self.dps_cache) payload = self._generate_payload(UPDATEDPS, dps) self.transport.write(payload) return True From 0db320ee36e30ea5cc8724c393bb19c68fe66968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Villagra?= Date: Wed, 1 Dec 2021 15:36:03 +0000 Subject: [PATCH 11/11] add whitelist with 18,19,20 --- custom_components/localtuya/pytuya/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index f9618e01d..b7645ec25 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -78,6 +78,9 @@ HEARTBEAT_INTERVAL = 10 +# DPS that are known to be safe to use with update_dps (0x12) command +UPDATE_DPS_WHITELIST = [18, 19, 20] # Socket (Wi-Fi) + # This is intended to match requests.json payload at # https://github.com/codetheweb/tuyapi : # type_0a devices require the 0a command as the status request @@ -489,15 +492,17 @@ async def update_dps(self, dps=None): Request device to update index. Args: - dps([int]): list of dps to update, default=all detected + dps([int]): list of dps to update, default=detected&whitelisted """ if self.version == 3.3: if dps is None: if not self.dps_cache: await self.detect_available_dps() if self.dps_cache: - dps = [int(dp) for dp in self.dps_cache][:255] - self.debug("updatedps() entry (dps_cache %s)", self.dps_cache) + dps = [int(dp) for dp in self.dps_cache] + # filter non whitelisted dps + dps = list(set(dps).intersection(set(UPDATE_DPS_WHITELIST))) + self.debug("updatedps() entry (dps %s, dps_cache %s)", dps, self.dps_cache) payload = self._generate_payload(UPDATEDPS, dps) self.transport.write(payload) return True