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/15] 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 789091c..2c613b4 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -61,6 +61,7 @@ TuyaMessage = namedtuple("TuyaMessage", "seqno cmd retcode payload crc") 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 @@ PAYLOAD_DICT = { 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 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): 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 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): """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 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): 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 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): 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/15] 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 88b434f..2573b1a 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -234,13 +234,13 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger): 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/15] 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 2c613b4..90ac8ed 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -61,7 +61,7 @@ TuyaMessage = namedtuple("TuyaMessage", "seqno cmd retcode payload crc") 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 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): 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 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): 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/15] 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 2573b1a..30028af 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 @@ from homeassistant.const import ( 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 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): 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 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): 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 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): 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 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): """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 731b60a..03bf80d 100644 --- a/custom_components/localtuya/config_flow.py +++ b/custom_components/localtuya/config_flow.py @@ -14,6 +14,7 @@ from homeassistant.const import ( CONF_HOST, CONF_ID, CONF_PLATFORM, + CONF_SCAN_INTERVAL, ) from homeassistant.core import callback @@ -44,6 +45,7 @@ BASIC_INFO_SCHEMA = vol.Schema( 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 @@ DEVICE_SCHEMA = vol.Schema( 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 90ac8ed..f033757 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -382,7 +382,6 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): 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 898b99d..8a2c03f 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 6ce233a..34826c4 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/15] 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 f033757..ccde438 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -91,13 +91,13 @@ PAYLOAD_DICT = { 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 @@ class AESCipher: 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 @@ class MessageDispatcher(ContextualLogger): 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 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): 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 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): 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/15] 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 8a2c03f..b8bedc8 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 34826c4..8a03279 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/15] 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 30028af..badfabc 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -173,7 +173,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger): 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 ccde438..24b0268 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -21,6 +21,7 @@ Functions 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 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): """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/15] 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 24b0268..ecd0b7d 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -509,14 +509,9 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): 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/15] 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 ecd0b7d..16ae271 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -92,13 +92,13 @@ PAYLOAD_DICT = { 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 @@ class AESCipher: 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 @@ class MessageDispatcher(ContextualLogger): 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 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): 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 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): 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/15] 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 16ae271..f9618e0 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -497,7 +497,7 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): 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/15] 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 f9618e0..b7645ec 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -78,6 +78,9 @@ SUFFIX_VALUE = 0x0000AA55 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 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger): 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 From 28554a1849746d9d210cff87b612acece2f47a7e Mon Sep 17 00:00:00 2001 From: jeremysherriff Date: Sun, 19 Dec 2021 17:38:30 +1300 Subject: [PATCH 12/15] Add scan interval information to README To accompany PR rospogrigio/localtuya#549 --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a4ccf04..20ff98f 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ localtuya: local_key: xxxxx friendly_name: Tuya Device protocol_version: "3.3" + scan_interval: # optional, only needed if energy monitoring values are not updating + seconds: 30 # Values less than 10 seconds may cause stability issues entities: - platform: binary_sensor friendly_name: Plug Status @@ -112,9 +114,13 @@ select one of these, or manually input all the parameters. ![discovery](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/1-discovery.png) If you have selected one entry, you only need to input the device's Friendly Name and the localKey. + +Setting the scan interval is optional, only needed if energy/power values are not updating frequently enough by default. Values less than 10 seconds may cause stability issues. + Once you press "Submit", the connection is tested to check that everything works. -![device](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/2-device.png) +![image](https://user-images.githubusercontent.com/1082213/146663895-41e1902b-4f09-4b21-b9d7-067d9cd67069.png) + Then, it's time to add the entities: this step will take place several times. First, select the entity type from the drop-down menu to set it up. After you have defined all the needed entities, leave the "Do not add more entities" checkbox checked: this will complete the procedure. @@ -140,6 +146,7 @@ You can obtain Energy monitoring (voltage, current) in two different ways: Note: Voltage and Consumption usually include the first decimal. You will need to scale the parament by 0.1 to get the correct values. 1) Access the voltage/current/current_consumption attributes of a switch, and define template sensors Note: these values are already divided by 10 for Voltage and Consumption +1) On some devices, you may find that the energy values are not updating frequently enough by default. If so, set the scan interval (see above) to an appropriate value. Settings below 10 seconds may cause stability issues, 30 seconds is recommended. ``` sensor: From 0d14df976d8cfa28f3b934bc5e0fbca6aa015486 Mon Sep 17 00:00:00 2001 From: jeremysherriff Date: Sun, 19 Dec 2021 17:41:02 +1300 Subject: [PATCH 13/15] Tweak image sizing --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 20ff98f..7e4d690 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ Setting the scan interval is optional, only needed if energy/power values are no Once you press "Submit", the connection is tested to check that everything works. -![image](https://user-images.githubusercontent.com/1082213/146663895-41e1902b-4f09-4b21-b9d7-067d9cd67069.png) +![image](https://user-images.githubusercontent.com/1082213/146664103-ac40319e-f934-4933-90cf-2beaff1e6bac.png) Then, it's time to add the entities: this step will take place several times. First, select the entity type from the drop-down menu to set it up. From 5542d83e31d1e13234264517843b02b60c685de5 Mon Sep 17 00:00:00 2001 From: jeremysherriff Date: Sun, 19 Dec 2021 17:46:34 +1300 Subject: [PATCH 14/15] Add scan interval information to info.md To accompany PR rospogrigio/localtuya#549 --- info.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/info.md b/info.md index 3c69026..b59ba35 100644 --- a/info.md +++ b/info.md @@ -43,6 +43,8 @@ localtuya: local_key: xxxxx friendly_name: Tuya Device protocol_version: "3.3" + scan_interval: # optional, only needed if energy monitoring values are not updating + seconds: 30 # Values less than 10 seconds may cause stability issues entities: - platform: binary_sensor friendly_name: Plug Status @@ -98,9 +100,12 @@ select one of these, or manually input all the parameters. ![discovery](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/1-discovery.png) If you have selected one entry, you just have to input the Friendly Name of the Device, and the localKey. -Once you press "Submit", the connection will be tested to check that everything works, in order to proceed. -![device](https://github.com/rospogrigio/localtuya-homeassistant/blob/master/img/2-device.png) +Setting the scan interval is optional, only needed if energy/power values are not updating frequently enough by default. Values less than 10 seconds may cause stability issues. + +Once you press "Submit", the connection is tested to check that everything works. + +![image](https://user-images.githubusercontent.com/1082213/146663895-41e1902b-4f09-4b21-b9d7-067d9cd67069.png) Then, it's time to add the entities: this step will take place several times. Select the entity type from the drop-down menu to set it up. After you have defined all the needed entities leave the "Do not add more entities" checkbox checked: this will complete the procedure. @@ -122,7 +127,8 @@ After all the entities have been configured, the procedure is complete, and the Energy monitoring (voltage, current...) values can be obtained in two different ways: 1) creating individual sensors, each one with the desired name. Note: Voltage and Consumption usually include the first decimal, so 0.1 as "scaling" parameter shall be used in order to get the correct values. -2) accessing the voltage/current/current_consumption attributes of a switch, and then defining template sensors like this (please note that in this case the values are already divided by 10 for Voltage and Consumption): +2) accessing the voltage/current/current_consumption attributes of a switch, and then defining template sensors like this (please note that in this case the values are already divided by 10 for Voltage and Consumption) +3) On some devices, you may find that the energy values are not updating frequently enough by default. If so, set the scan interval (see above) to an appropriate value. Settings below 10 seconds may cause stability issues, 30 seconds is recommended. ``` sensor: From 38b9c675441fa4daaa1315307af7291b21310363 Mon Sep 17 00:00:00 2001 From: jeremysherriff Date: Sun, 19 Dec 2021 17:48:28 +1300 Subject: [PATCH 15/15] Tweak image sizing --- info.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/info.md b/info.md index b59ba35..ca0b487 100644 --- a/info.md +++ b/info.md @@ -105,7 +105,7 @@ Setting the scan interval is optional, only needed if energy/power values are no Once you press "Submit", the connection is tested to check that everything works. -![image](https://user-images.githubusercontent.com/1082213/146663895-41e1902b-4f09-4b21-b9d7-067d9cd67069.png) +![image](https://user-images.githubusercontent.com/1082213/146664103-ac40319e-f934-4933-90cf-2beaff1e6bac.png) Then, it's time to add the entities: this step will take place several times. Select the entity type from the drop-down menu to set it up. After you have defined all the needed entities leave the "Do not add more entities" checkbox checked: this will complete the procedure.