From 3fc19da2594bf2dff0c685e6316b7ed861f4a6de Mon Sep 17 00:00:00 2001 From: rospogrigio Date: Mon, 28 Sep 2020 14:56:55 +0200 Subject: [PATCH] Updating the status of TuyaDevice using the response of the set_dps() call --- custom_components/localtuya/common.py | 17 ++++-- .../localtuya/pytuya/__init__.py | 56 +++++++++++++++++++ 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/custom_components/localtuya/common.py b/custom_components/localtuya/common.py index 86d156a..4c8e259 100644 --- a/custom_components/localtuya/common.py +++ b/custom_components/localtuya/common.py @@ -89,13 +89,18 @@ class TuyaDevice: raise ConnectionError("Failed to update status .") def set_dps(self, state, dps_index): - # _LOGGER.info("running def set_dps from cover") - """Change the Tuya switch status and clear the cache.""" - self._cached_status = "" - self._cached_status_time = 0 + # _LOGGER.info("running def set_dps from TuyaDevice") + # No need to clear the cache here: let's just update the status of the changed dps as returned by the interface (see 5 lines below) +# self._cached_status = "" +# self._cached_status_time = 0 for i in range(5): try: - return self._interface.set_dps(state, dps_index) + result = self._interface.set_dps(state, dps_index) + self._cached_status["dps"].update(result["dps"]) + # NOW WE SHOULD TRIGGER status_updated FOR ALL ENTITIES INVOLVED IN result["dps"] : + #for dp in result["dps"]: + # have status_updated() called.... + return except Exception: print( "Failed to set status of device [{}]".format( @@ -116,7 +121,7 @@ class TuyaDevice: self._lock.acquire() try: now = time() - if not self._cached_status or now - self._cached_status_time > 15: + if not self._cached_status or now - self._cached_status_time > 10: sleep(0.5) self._cached_status = self.__get_status() self._cached_status_time = time() diff --git a/custom_components/localtuya/pytuya/__init__.py b/custom_components/localtuya/pytuya/__init__.py index 98bcc3d..2507075 100644 --- a/custom_components/localtuya/pytuya/__init__.py +++ b/custom_components/localtuya/pytuya/__init__.py @@ -253,6 +253,62 @@ class TuyaInterface: """ return self.exchange(SET, {str(dps_index): value}) + + def _decode_received_data(self, data, is_status): + """Return device status.""" + """is_status may be True (result of a status request) or False (result of a set_dps request)""" + result = data[20:-8] # hard coded offsets + if self.dev_type != "type_0a": + result = result[15:] + elif not is_status: + result = result[15:] + + log.debug("Decrypting %r :", result) + # result = data[data.find('{'):data.rfind('}')+1] # naive marker search, + # hope neither { nor } occur in header/footer + # print('result %r' % result) + if result.startswith(b"{"): + # this is the regular expected code path + if not isinstance(result, str): + result = result.decode() + result = json.loads(result) + elif result.startswith(PROTOCOL_VERSION_BYTES_31): + # got an encrypted payload, happens occasionally + # expect resulting json to look similar to: + # {"devId":"ID","dps":{"1":true,"2":0},"t":EPOCH_SECS,"s":3_DIGIT_NUM} + # NOTE dps.2 may or may not be present + result = result[len(PROTOCOL_VERSION_BYTES_31) :] # remove version header + # remove (what I'm guessing, but not confirmed is) 16-bytes of MD5 + # hexdigest of payload + result = result[16:] + cipher = AESCipher(self.local_key) + result = cipher.decrypt(result) + #print("decrypted result=[{}]".format(result)) + log.info("decrypted result=%r", result) + if not isinstance(result, str): + result = result.decode() + result = json.loads(result) + elif self.version == 3.3: + # results of a set_dps request must have a further offset + cipher = AESCipher(self.local_key) + result = cipher.decrypt(result, False) + log.debug("decrypted result=%r", result) + if "data unvalid" in result: + self.dev_type = "type_0d" + log.info( + "'data unvalid' error detected: switching to dev_type %r", + self.dev_type, + ) + return self.status() + if not isinstance(result, str): + result = result.decode() + result = json.loads(result) + else: + log.error("Unexpected status() payload=%r", result) + + return result + + def detect_available_dps(self): """Return which datapoints are supported by the device.""" # type_0d devices need a sort of bruteforce querying in order to detect the