Add support for passive devices (#171)

* Add support for passive devices

* Fix a few issues

* Fix broken handling of closing a connection

This fixes the following error:

  AttributeError: 'NoneType' object has no attribute 'write'

* Always use discovery broadcasts to trigger connects

From now on there's no connect loop but all attempts are initiated by
received discovery messages.

* Fix cancelling of heartbeat task

* Verify entry has been loaded before connecting

* Don't reset seqno when switching to type_d device
This commit is contained in:
Pierre Ståhl
2020-12-15 09:08:46 +01:00
committed by GitHub
parent 1d58abd0f2
commit 10e55e67b4
3 changed files with 45 additions and 54 deletions

View File

@@ -71,7 +71,12 @@ from homeassistant.helpers.reload import async_integration_yaml_config
from .common import TuyaDevice
from .config_flow import config_schema
from .const import CONF_PRODUCT_KEY, DATA_DISCOVERY, DOMAIN, TUYA_DEVICE
from .const import (
CONF_PRODUCT_KEY,
DATA_DISCOVERY,
DOMAIN,
TUYA_DEVICE,
)
from .discovery import TuyaDiscovery
_LOGGER = logging.getLogger(__name__)
@@ -155,11 +160,19 @@ async def async_setup(hass: HomeAssistant, config: dict):
if entry.data.get(CONF_PRODUCT_KEY) != product_key:
updates[CONF_PRODUCT_KEY] = product_key
# Update settings if something changed, otherwise try to connect. Updating
# settings triggers a reload of the config entry, which tears down the device
# so no need to connect in that case.
if updates:
_LOGGER.debug("Update keys for device %s: %s", device_id, updates)
hass.config_entries.async_update_entry(
entry, data={**entry.data, **updates}
)
elif entry.entry_id in hass.data[DOMAIN]:
_LOGGER.debug("Device %s found with IP %s", device_id, device_ip)
device = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE]
device.connect()
discovery = TuyaDiscovery(_device_discovered)
@@ -209,7 +222,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
for platform in platforms
]
)
device.connect()
hass.async_create_task(setup_entities())
@@ -230,7 +242,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
)
hass.data[DOMAIN][entry.entry_id][UNSUB_LISTENER]()
hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE].close()
await hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE].close()
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)

View File

@@ -1,7 +1,6 @@
"""Code shared between all platforms."""
import asyncio
import logging
from random import randrange
from homeassistant.const import (
CONF_DEVICE_ID,
@@ -29,8 +28,6 @@ from .const import (
_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."""
@@ -108,7 +105,6 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
self._dps_to_request = {}
self._is_closing = False
self._connect_task = None
self._connection_attempts = 0
self.set_logger(_LOGGER, config_entry[CONF_DEVICE_ID])
# This has to be done in case the device type is type_0d
@@ -116,33 +112,14 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
self._dps_to_request[entity[CONF_ID]] = None
def connect(self):
"""Connet to device if not already connected."""
"""Connect to device if not already connected."""
if not self._is_closing and self._connect_task is None and not self._interface:
self.debug(
"Connecting to %s",
self._config_entry[CONF_HOST],
)
self._connect_task = asyncio.ensure_future(self._make_connection())
else:
self.debug(
"Already connecting to %s (%s) - %s, %s, %s",
self._config_entry[CONF_HOST],
self._config_entry[CONF_DEVICE_ID],
self._is_closing,
self._connect_task,
self._interface,
)
self._connect_task = asyncio.create_task(self._make_connection())
async def _make_connection(self):
backoff = min(
randrange(2 ** self._connection_attempts), BACKOFF_TIME_UPPER_LIMIT
)
self.debug("Waiting %d seconds before connecting", backoff)
await asyncio.sleep(backoff)
self.debug("Connecting to %s", self._config_entry[CONF_HOST])
try:
self.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],
@@ -158,23 +135,21 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
raise Exception("Failed to retrieve status")
self.status_updated(status)
self._connection_attempts = 0
except Exception:
self.exception(f"Connect to {self._config_entry[CONF_HOST]} failed")
self._connection_attempts += 1
if self._interface is not None:
self._interface.close()
await self._interface.close()
self._interface = None
self._hass.loop.call_soon(self.connect)
self._connect_task = None
def close(self):
async def close(self):
"""Close connection and stop re-connect loop."""
self._is_closing = True
if self._connect_task:
self._connect_task.cancel()
await self._connect_task
if self._interface:
self._interface.close()
await self._interface.close()
async def set_dp(self, state, dp_index):
"""Change value of a DP of the Tuya device."""
@@ -209,15 +184,13 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
async_dispatcher_send(self._hass, signal, self._status)
@callback
def disconnected(self, exc):
def disconnected(self):
"""Device disconnected."""
self.debug("Disconnected: %s", exc)
signal = f"localtuya_{self._config_entry[CONF_DEVICE_ID]}"
async_dispatcher_send(self._hass, signal, None)
self._interface = None
self.connect()
self.debug("Disconnected - waiting for discovery broadcast")
class LocalTuyaEntity(Entity, pytuya.ContextualLogger):

View File

@@ -362,7 +362,7 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
if "dps" in decoded_message:
self.dps_cache.update(decoded_message["dps"])
listener = self.listener()
listener = self.listener and self.listener()
if listener is not None:
listener.status_updated(self.dps_cache)
@@ -377,12 +377,17 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
while True:
try:
await self.heartbeat()
await asyncio.sleep(HEARTBEAT_INTERVAL)
except asyncio.CancelledError:
self.debug("Stopped heartbeat loop")
raise
except Exception as ex:
self.exception("Heartbeat failed (%s), disconnecting", ex)
break
await asyncio.sleep(HEARTBEAT_INTERVAL)
self.debug("Stopped heartbeat loop")
self.close()
transport = self.transport
self.transport = None
transport.close()
self.transport = transport
self.on_connected.set_result(True)
@@ -396,24 +401,25 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
"""Disconnected from device."""
self.debug("Connection lost: %s", exc)
try:
self.close()
except Exception:
self.exception("Failed to close connection")
finally:
try:
listener = self.listener()
listener = self.listener and self.listener()
if listener is not None:
listener.disconnected(exc)
listener.disconnected()
except Exception:
self.exception("Failed to call disconnected callback")
def close(self):
async def close(self):
"""Close connection and abort all outstanding listeners."""
self.debug("Closing connection")
if self.heartbeater is not None:
self.heartbeater.cancel()
try:
await self.heartbeater
except asyncio.CancelledError:
pass
self.heartbeater = None
if self.dispatcher is not None:
self.dispatcher.abort()
self.dispatcher = None
if self.transport is not None:
transport = self.transport
self.transport = None
@@ -572,7 +578,7 @@ class TuyaProtocol(asyncio.Protocol, ContextualLogger):
if data is not None:
json_data["dps"] = data
if command_hb == 0x0D:
elif command_hb == 0x0D:
json_data["dps"] = self.dps_to_request
payload = json.dumps(json_data).replace(" ", "").encode("utf-8")