From 10505d542ad49fbb4643d4f0cb3ae8b13477f2e2 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 8 May 2018 22:30:28 +0300 Subject: [PATCH] Make sure zwave nodes/entities enter the registry is proper state. (#14251) * When zwave node's info is parsed remove it and re-add back. * Delay value entity if not ready * If node is ready consider it parsed even if manufacturer/product are missing. * Add annotations --- homeassistant/components/zwave/__init__.py | 81 ++++++++++++------- homeassistant/components/zwave/node_entity.py | 16 +++- homeassistant/components/zwave/util.py | 23 ++++++ tests/components/zwave/test_init.py | 2 +- 4 files changed, 92 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 01b17023c12..7562ac0ff14 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -11,7 +11,7 @@ from pprint import pprint import voluptuous as vol -from homeassistant.core import CoreState +from homeassistant.core import callback, CoreState from homeassistant.loader import get_platform from homeassistant.helpers import discovery from homeassistant.helpers.entity import generate_entity_id @@ -31,7 +31,8 @@ from .const import DOMAIN, DATA_DEVICES, DATA_NETWORK, DATA_ENTITY_VALUES from .node_entity import ZWaveBaseEntity, ZWaveNodeEntity from . import workaround from .discovery_schemas import DISCOVERY_SCHEMAS -from .util import check_node_schema, check_value_schema, node_name +from .util import (check_node_schema, check_value_schema, node_name, + check_has_unique_id, is_node_parsed) REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.3'] @@ -313,30 +314,22 @@ def setup(hass, config): _add_node_to_component() return - async def _check_node_ready(): - """Wait for node to be parsed.""" - start_time = dt_util.utcnow() - while True: - waited = int((dt_util.utcnow()-start_time).total_seconds()) - - if entity.unique_id: - _LOGGER.info("Z-Wave node %d ready after %d seconds", - entity.node_id, waited) - break - elif waited >= const.NODE_READY_WAIT_SECS: - # Wait up to NODE_READY_WAIT_SECS seconds for the Z-Wave - # node to be ready. - _LOGGER.warning( - "Z-Wave node %d not ready after %d seconds, " - "continuing anyway", - entity.node_id, waited) - break - else: - await asyncio.sleep(1, loop=hass.loop) - + @callback + def _on_ready(sec): + _LOGGER.info("Z-Wave node %d ready after %d seconds", + entity.node_id, sec) hass.async_add_job(_add_node_to_component) - hass.add_job(_check_node_ready) + @callback + def _on_timeout(sec): + _LOGGER.warning( + "Z-Wave node %d not ready after %d seconds, " + "continuing anyway", + entity.node_id, sec) + hass.async_add_job(_add_node_to_component) + + hass.add_job(check_has_unique_id, entity, _on_ready, _on_timeout, + hass.loop) def network_ready(): """Handle the query of all awake nodes.""" @@ -839,13 +832,35 @@ class ZWaveDeviceEntityValues(): dict_id = id(self) + @callback + def _on_ready(sec): + _LOGGER.info( + "Z-Wave entity %s (node_id: %d) ready after %d seconds", + device.name, self._node.node_id, sec) + self._hass.async_add_job(discover_device, component, device, + dict_id) + + @callback + def _on_timeout(sec): + _LOGGER.warning( + "Z-Wave entity %s (node_id: %d) not ready after %d seconds, " + "continuing anyway", + device.name, self._node.node_id, sec) + self._hass.async_add_job(discover_device, component, device, + dict_id) + async def discover_device(component, device, dict_id): """Put device in a dictionary and call discovery on it.""" self._hass.data[DATA_DEVICES][dict_id] = device await discovery.async_load_platform( self._hass, component, DOMAIN, {const.DISCOVERY_DEVICE: dict_id}, self._zwave_config) - self._hass.add_job(discover_device, component, device, dict_id) + + if device.unique_id: + self._hass.add_job(discover_device, component, device, dict_id) + else: + self._hass.add_job(check_has_unique_id, device, _on_ready, + _on_timeout, self._hass.loop) class ZWaveDeviceEntity(ZWaveBaseEntity): @@ -862,8 +877,7 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): self.values.primary.set_change_verified(False) self._name = _value_name(self.values.primary) - self._unique_id = "{}-{}".format(self.node.node_id, - self.values.primary.object_id) + self._unique_id = self._compute_unique_id() self._update_attributes() dispatcher.connect( @@ -894,6 +908,11 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): def _update_attributes(self): """Update the node attributes. May only be used inside callback.""" self.node_id = self.node.node_id + self._name = _value_name(self.values.primary) + if not self._unique_id: + self._unique_id = self._compute_unique_id() + if self._unique_id: + self.try_remove_and_add() if self.values.power: self.power_consumption = round( @@ -940,3 +959,11 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): for value in self.values: if value is not None: self.node.refresh_value(value.value_id) + + def _compute_unique_id(self): + if (is_node_parsed(self.node) and + self.values.primary.label != "Unknown") or \ + self.node.is_ready: + return "{}-{}".format(self.node.node_id, + self.values.primary.object_id) + return None diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index bcddcb0b800..2c6d26802bd 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -9,7 +9,7 @@ from .const import ( ATTR_NODE_ID, COMMAND_CLASS_WAKE_UP, ATTR_SCENE_ID, ATTR_SCENE_DATA, ATTR_BASIC_LEVEL, EVENT_NODE_EVENT, EVENT_SCENE_ACTIVATED, COMMAND_CLASS_CENTRAL_SCENE) -from .util import node_name +from .util import node_name, is_node_parsed _LOGGER = logging.getLogger(__name__) @@ -65,6 +65,15 @@ class ZWaveBaseEntity(Entity): self._update_scheduled = True self.hass.loop.call_later(0.1, do_update) + def try_remove_and_add(self): + """Remove this entity and add it back.""" + async def _async_remove_and_add(): + await self.async_remove() + self.entity_id = None + await self.platform.async_add_entities([self]) + if self.hass and self.platform: + self.hass.add_job(_async_remove_and_add) + class ZWaveNodeEntity(ZWaveBaseEntity): """Representation of a Z-Wave node.""" @@ -151,6 +160,9 @@ class ZWaveNodeEntity(ZWaveBaseEntity): if not self._unique_id: self._unique_id = self._compute_unique_id() + if self._unique_id: + # Node info parsed. Remove and re-add + self.try_remove_and_add() self.maybe_schedule_update() @@ -243,6 +255,6 @@ class ZWaveNodeEntity(ZWaveBaseEntity): return attrs def _compute_unique_id(self): - if self._manufacturer_name and self._product_name: + if is_node_parsed(self.node) or self.node.is_ready: return 'node-{}'.format(self.node_id) return None diff --git a/homeassistant/components/zwave/util.py b/homeassistant/components/zwave/util.py index 8c74b731ad6..1c0bb14f7e5 100644 --- a/homeassistant/components/zwave/util.py +++ b/homeassistant/components/zwave/util.py @@ -1,6 +1,9 @@ """Zwave util methods.""" +import asyncio import logging +import homeassistant.util.dt as dt_util + from . import const _LOGGER = logging.getLogger(__name__) @@ -67,3 +70,23 @@ def node_name(node): """Return the name of the node.""" return node.name or '{} {}'.format( node.manufacturer_name, node.product_name) + + +async def check_has_unique_id(entity, ready_callback, timeout_callback, loop): + """Wait for entity to have unique_id.""" + start_time = dt_util.utcnow() + while True: + waited = int((dt_util.utcnow()-start_time).total_seconds()) + if entity.unique_id: + ready_callback(waited) + return + elif waited >= const.NODE_READY_WAIT_SECS: + # Wait up to NODE_READY_WAIT_SECS seconds for unique_id to appear. + timeout_callback(waited) + return + await asyncio.sleep(1, loop=loop) + + +def is_node_parsed(node): + """Check whether the node has been parsed or still waiting to be parsed.""" + return node.manufacturer_name and node.product_name diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index faa7357bd8a..0eba19f03a4 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -237,7 +237,7 @@ async def test_unparsed_node_discovery(hass, mock_openzwave): assert len(mock_receivers) == 1 - node = MockNode(node_id=14, manufacturer_name=None) + node = MockNode(node_id=14, manufacturer_name=None, is_ready=False) sleeps = []