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
This commit is contained in:
Andrey 2018-05-08 22:30:28 +03:00 committed by Paulus Schoutsen
parent e12994a0cd
commit 10505d542a
4 changed files with 92 additions and 30 deletions

View File

@ -11,7 +11,7 @@ from pprint import pprint
import voluptuous as vol import voluptuous as vol
from homeassistant.core import CoreState from homeassistant.core import callback, CoreState
from homeassistant.loader import get_platform from homeassistant.loader import get_platform
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
from homeassistant.helpers.entity import generate_entity_id 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 .node_entity import ZWaveBaseEntity, ZWaveNodeEntity
from . import workaround from . import workaround
from .discovery_schemas import DISCOVERY_SCHEMAS 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'] REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.3']
@ -313,30 +314,22 @@ def setup(hass, config):
_add_node_to_component() _add_node_to_component()
return return
async def _check_node_ready(): @callback
"""Wait for node to be parsed.""" def _on_ready(sec):
start_time = dt_util.utcnow() _LOGGER.info("Z-Wave node %d ready after %d seconds",
while True: entity.node_id, sec)
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)
hass.async_add_job(_add_node_to_component) 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(): def network_ready():
"""Handle the query of all awake nodes.""" """Handle the query of all awake nodes."""
@ -839,13 +832,35 @@ class ZWaveDeviceEntityValues():
dict_id = id(self) 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): async def discover_device(component, device, dict_id):
"""Put device in a dictionary and call discovery on it.""" """Put device in a dictionary and call discovery on it."""
self._hass.data[DATA_DEVICES][dict_id] = device self._hass.data[DATA_DEVICES][dict_id] = device
await discovery.async_load_platform( await discovery.async_load_platform(
self._hass, component, DOMAIN, self._hass, component, DOMAIN,
{const.DISCOVERY_DEVICE: dict_id}, self._zwave_config) {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): class ZWaveDeviceEntity(ZWaveBaseEntity):
@ -862,8 +877,7 @@ class ZWaveDeviceEntity(ZWaveBaseEntity):
self.values.primary.set_change_verified(False) self.values.primary.set_change_verified(False)
self._name = _value_name(self.values.primary) self._name = _value_name(self.values.primary)
self._unique_id = "{}-{}".format(self.node.node_id, self._unique_id = self._compute_unique_id()
self.values.primary.object_id)
self._update_attributes() self._update_attributes()
dispatcher.connect( dispatcher.connect(
@ -894,6 +908,11 @@ class ZWaveDeviceEntity(ZWaveBaseEntity):
def _update_attributes(self): def _update_attributes(self):
"""Update the node attributes. May only be used inside callback.""" """Update the node attributes. May only be used inside callback."""
self.node_id = self.node.node_id 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: if self.values.power:
self.power_consumption = round( self.power_consumption = round(
@ -940,3 +959,11 @@ class ZWaveDeviceEntity(ZWaveBaseEntity):
for value in self.values: for value in self.values:
if value is not None: if value is not None:
self.node.refresh_value(value.value_id) 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

View File

@ -9,7 +9,7 @@ from .const import (
ATTR_NODE_ID, COMMAND_CLASS_WAKE_UP, ATTR_SCENE_ID, ATTR_SCENE_DATA, ATTR_NODE_ID, COMMAND_CLASS_WAKE_UP, ATTR_SCENE_ID, ATTR_SCENE_DATA,
ATTR_BASIC_LEVEL, EVENT_NODE_EVENT, EVENT_SCENE_ACTIVATED, ATTR_BASIC_LEVEL, EVENT_NODE_EVENT, EVENT_SCENE_ACTIVATED,
COMMAND_CLASS_CENTRAL_SCENE) COMMAND_CLASS_CENTRAL_SCENE)
from .util import node_name from .util import node_name, is_node_parsed
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -65,6 +65,15 @@ class ZWaveBaseEntity(Entity):
self._update_scheduled = True self._update_scheduled = True
self.hass.loop.call_later(0.1, do_update) 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): class ZWaveNodeEntity(ZWaveBaseEntity):
"""Representation of a Z-Wave node.""" """Representation of a Z-Wave node."""
@ -151,6 +160,9 @@ class ZWaveNodeEntity(ZWaveBaseEntity):
if not self._unique_id: if not self._unique_id:
self._unique_id = self._compute_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() self.maybe_schedule_update()
@ -243,6 +255,6 @@ class ZWaveNodeEntity(ZWaveBaseEntity):
return attrs return attrs
def _compute_unique_id(self): 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 'node-{}'.format(self.node_id)
return None return None

View File

@ -1,6 +1,9 @@
"""Zwave util methods.""" """Zwave util methods."""
import asyncio
import logging import logging
import homeassistant.util.dt as dt_util
from . import const from . import const
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -67,3 +70,23 @@ def node_name(node):
"""Return the name of the node.""" """Return the name of the node."""
return node.name or '{} {}'.format( return node.name or '{} {}'.format(
node.manufacturer_name, node.product_name) 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

View File

@ -237,7 +237,7 @@ async def test_unparsed_node_discovery(hass, mock_openzwave):
assert len(mock_receivers) == 1 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 = [] sleeps = []