Convert pytuya to asyncio

This commit is contained in:
Pierre Ståhl
2020-10-01 09:40:12 +02:00
committed by rospogrigio
parent 084b3a741a
commit cad31f1ffe
8 changed files with 457 additions and 308 deletions

View File

@@ -1,8 +1,9 @@
"""Code shared between all platforms."""
import asyncio
import logging
from time import time, sleep
from threading import Lock
from random import randrange
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
@@ -23,6 +24,8 @@ from .const import CONF_LOCAL_KEY, CONF_PROTOCOL_VERSION, DOMAIN, TUYA_DEVICE
_LOGGER = logging.getLogger(__name__)
BACKOFF_TIME_UPPER_LIMIT = 300 # Five minutes
def prepare_setup_entities(hass, config_entry, platform):
"""Prepare ro setup entities for a platform."""
@@ -60,7 +63,7 @@ async def async_setup_entry(
# Add DPS used by this platform to the request list
for dp_conf in dps_config_fields:
if dp_conf in device_config:
tuyainterface._interface.add_dps_to_request(device_config[dp_conf])
tuyainterface._dps_to_request[device_config[dp_conf]] = None
entities.append(
entity_class(
@@ -88,92 +91,98 @@ def get_entity_config(config_entry, dps_id):
raise Exception(f"missing entity config for id {dps_id}")
class TuyaDevice:
class TuyaDevice(pytuya.TuyaListener):
"""Cache wrapper for pytuya.TuyaInterface."""
def __init__(self, hass, config_entry):
"""Initialize the cache."""
self._cached_status = ""
self._cached_status_time = 0
self._interface = pytuya.TuyaInterface(
config_entry[CONF_DEVICE_ID],
config_entry[CONF_HOST],
config_entry[CONF_LOCAL_KEY],
float(config_entry[CONF_PROTOCOL_VERSION]),
)
for entity in config_entry[CONF_ENTITIES]:
# this has to be done in case the device type is type_0d
self._interface.add_dps_to_request(entity[CONF_ID])
self._friendly_name = config_entry[CONF_FRIENDLY_NAME]
self._hass = hass
self._lock = Lock()
self._config_entry = config_entry
self._interface = None
self._status = {}
self._dps_to_request = {}
self._connect_task = None
self._connection_attempts = 0
@property
def unique_id(self):
"""Return unique device identifier."""
return self._interface.id
# This has to be done in case the device type is type_0d
for entity in config_entry[CONF_ENTITIES]:
self._dps_to_request[entity[CONF_ID]] = None
def __get_status(self):
_LOGGER.debug("running def __get_status from TuyaDevice")
for i in range(5):
try:
status = self._interface.status()
return status
except Exception as e:
print(
"Failed to update status of device [{}]: [{}]".format(
self._interface.address, e
)
)
sleep(1.0)
if i + 1 == 3:
_LOGGER.error(
"Failed to update status of device %s", self._interface.address
)
# return None
raise ConnectionError("Failed to update status .")
def connect(self, delay=None):
"""Connet to device if not already connected."""
if self._connect_task is None:
self._connect_task = asyncio.ensure_future(self._make_connection())
def set_dps(self, state, dps_index):
"""Change value of a DP of the Tuya device and update the cached status."""
# _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:
result = self._interface.set_dps(state, dps_index)
self._cached_status["dps"].update(result["dps"])
signal = f"localtuya_{self._interface.id}"
async_dispatcher_send(self._hass, signal, self._cached_status)
return
except Exception as e:
print(
"Failed to set status of device [{}]: [{}]".format(
self._interface.address, e
)
)
if i + 1 == 3:
_LOGGER.error(
"Failed to set status of device %s", self._interface.address
)
return
async def _make_connection(self):
# Do nothing if already connected
if self._interface:
return
# raise ConnectionError("Failed to set status.")
# The sleep gives another task the possibility to sweep in and
# connect, so we block that here
self._interface = True
backoff = min(
randrange(2 ** self._connection_attempts), BACKOFF_TIME_UPPER_LIMIT
)
_LOGGER.debug("Waiting %d seconds before connecting", backoff)
await asyncio.sleep(backoff)
def status(self):
"""Get the state of the Tuya device and cache the results."""
_LOGGER.debug("running def status(self) from TuyaDevice")
self._lock.acquire()
try:
now = time()
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()
return self._cached_status
finally:
self._lock.release()
_LOGGER.debug("Connecting to %s", self._config_entry[CONF_HOST])
self._interface = await pytuya.connect(
self._config_entry[CONF_HOST],
self._config_entry[CONF_DEVICE_ID],
self._config_entry[CONF_LOCAL_KEY],
float(self._config_entry[CONF_PROTOCOL_VERSION]),
self,
)
self._interface.add_dps_to_request(self._dps_to_request)
_LOGGER.debug("Retrieving initial state")
status = await self._interface.status()
if status is None:
raise Exception("failed to retrieve status")
self.status_updated(status)
self._connection_attempts = 0
except Exception:
_LOGGER.exception("connect failed")
self._connection_attempts += 1
self._interface.close()
self._interface = None
self._hass.loop.call_soon(self.connect)
self._connect_task = None
async def set_dps(self, state, dps_index):
"""Change value of a DP of the Tuya device and update the cached status."""
if self._interface is not None:
try:
await self._interface.set_dps(state, dps_index)
except Exception:
_LOGGER.exception("Failed to set DP {dps_index} to state")
else:
_LOGGER.error(
"Not connected to device %s", self._config_entry[CONF_FRIENDLY_NAME]
)
@callback
def status_updated(self, status):
"""Device updated status."""
self._status.update(status["dps"])
signal = f"localtuya_{self._config_entry[CONF_DEVICE_ID]}"
async_dispatcher_send(self._hass, signal, self._status)
@callback
def disconnected(self, exc):
"""Device disconnected."""
signal = f"localtuya_{self._config_entry[CONF_DEVICE_ID]}"
async_dispatcher_send(self._hass, signal, None)
self._interface = None
self.connect()
class LocalTuyaEntity(Entity):
@@ -212,7 +221,7 @@ class LocalTuyaEntity(Entity):
return {
"identifiers": {
# Serial numbers are unique identifiers within a specific domain
(DOMAIN, f"local_{self._device.unique_id}")
(DOMAIN, f"local_{self._config_entry.data[CONF_DEVICE_ID]}")
},
"name": self._config_entry.data[CONF_FRIENDLY_NAME],
"manufacturer": "Unknown",
@@ -233,7 +242,7 @@ class LocalTuyaEntity(Entity):
@property
def unique_id(self):
"""Return unique device identifier."""
return f"local_{self._device.unique_id}_{self._dps_id}"
return f"local_{self._config_entry.data[CONF_DEVICE_ID]}_{self._dps_id}"
def has_config(self, attr):
"""Return if a config parameter has a valid value."""
@@ -243,14 +252,11 @@ class LocalTuyaEntity(Entity):
@property
def available(self):
"""Return if device is available or not."""
return bool(self._status)
return str(self._dps_id) in self._status
def dps(self, dps_index):
"""Return cached value for DPS index."""
if "dps" not in self._status:
return None
value = self._status["dps"].get(str(dps_index))
value = self._status.get(str(dps_index))
if value is None:
_LOGGER.warning(
"Entity %s is requesting unknown DPS index %s",