diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index c2d4a13a934..01b17023c12 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -297,15 +297,46 @@ def setup(hass, config): def node_added(node): """Handle a new node on the network.""" entity = ZWaveNodeEntity(node, network) - name = node_name(node) - generated_id = generate_entity_id(DOMAIN + '.{}', name, []) - node_config = device_config.get(generated_id) - if node_config.get(CONF_IGNORED): - _LOGGER.info( - "Ignoring node entity %s due to device settings", - generated_id) + + def _add_node_to_component(): + name = node_name(node) + generated_id = generate_entity_id(DOMAIN + '.{}', name, []) + node_config = device_config.get(generated_id) + if node_config.get(CONF_IGNORED): + _LOGGER.info( + "Ignoring node entity %s due to device settings", + generated_id) + return + component.add_entities([entity]) + + if entity.unique_id: + _add_node_to_component() return - component.add_entities([entity]) + + 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) + + hass.async_add_job(_add_node_to_component) + + hass.add_job(_check_node_ready) def network_ready(): """Handle the query of all awake nodes.""" diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index b42b6d0fce7..3e503e4d9a4 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -20,6 +20,7 @@ ATTR_POLL_INTENSITY = "poll_intensity" ATTR_VALUE_INDEX = "value_index" ATTR_VALUE_INSTANCE = "value_instance" NETWORK_READY_WAIT_SECS = 300 +NODE_READY_WAIT_SECS = 30 DISCOVERY_DEVICE = 'device' diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index 5a4b1b02504..bcddcb0b800 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -81,6 +81,7 @@ class ZWaveNodeEntity(ZWaveBaseEntity): self._name = node_name(self.node) self._product_name = node.product_name self._manufacturer_name = node.manufacturer_name + self._unique_id = self._compute_unique_id() self._attributes = {} self.wakeup_interval = None self.location = None @@ -95,6 +96,11 @@ class ZWaveNodeEntity(ZWaveBaseEntity): dispatcher.connect( self.network_scene_activated, ZWaveNetwork.SIGNAL_SCENE_EVENT) + @property + def unique_id(self): + """Unique ID of Z-wave node.""" + return self._unique_id + def network_node_changed(self, node=None, value=None, args=None): """Handle a changed node on the network.""" if node and node.node_id != self.node_id: @@ -138,8 +144,14 @@ class ZWaveNodeEntity(ZWaveBaseEntity): self.wakeup_interval = None self.battery_level = self.node.get_battery_level() + self._product_name = self.node.product_name + self._manufacturer_name = self.node.manufacturer_name + self._name = node_name(self.node) self._attributes = attributes + if not self._unique_id: + self._unique_id = self._compute_unique_id() + self.maybe_schedule_update() def network_node_event(self, node, value): @@ -229,3 +241,8 @@ class ZWaveNodeEntity(ZWaveBaseEntity): attrs[ATTR_WAKEUP] = self.wakeup_interval return attrs + + def _compute_unique_id(self): + if self._manufacturer_name and self._product_name: + return 'node-{}'.format(self.node_id) + return None diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 004e5e95ca0..faa7357bd8a 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -224,6 +224,47 @@ def test_node_discovery(hass, mock_openzwave): assert hass.states.get('zwave.mock_node').state is 'unknown' +async def test_unparsed_node_discovery(hass, mock_openzwave): + """Test discovery of a node.""" + mock_receivers = [] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal == MockNetwork.SIGNAL_NODE_ADDED: + mock_receivers.append(receiver) + + with patch('pydispatch.dispatcher.connect', new=mock_connect): + await async_setup_component(hass, 'zwave', {'zwave': {}}) + + assert len(mock_receivers) == 1 + + node = MockNode(node_id=14, manufacturer_name=None) + + sleeps = [] + + def utcnow(): + return datetime.fromtimestamp(len(sleeps)) + + asyncio_sleep = asyncio.sleep + + async def sleep(duration, loop): + if duration > 0: + sleeps.append(duration) + await asyncio_sleep(0, loop=loop) + + with patch('homeassistant.components.zwave.dt_util.utcnow', new=utcnow): + with patch('asyncio.sleep', new=sleep): + with patch.object(zwave, '_LOGGER') as mock_logger: + hass.async_add_job(mock_receivers[0], node) + await hass.async_block_till_done() + + assert len(sleeps) == const.NODE_READY_WAIT_SECS + assert mock_logger.warning.called + assert len(mock_logger.warning.mock_calls) == 1 + assert mock_logger.warning.mock_calls[0][1][1:] == \ + (14, const.NODE_READY_WAIT_SECS) + assert hass.states.get('zwave.mock_node').state is 'unknown' + + @asyncio.coroutine def test_node_ignored(hass, mock_openzwave): """Test discovery of a node.""" diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py index 299821d3685..f4d9b3ef0e8 100644 --- a/tests/components/zwave/test_node_entity.py +++ b/tests/components/zwave/test_node_entity.py @@ -182,8 +182,6 @@ class TestZWaveNodeEntity(unittest.TestCase): query_stage='Dynamic', is_awake=True, is_ready=False, is_failed=False, is_info_received=True, max_baud_rate=40000, is_zwave_plus=False, capabilities=[], neighbors=[], location=None) - self.node.manufacturer_name = 'Test Manufacturer' - self.node.product_name = 'Test Product' self.entity = node_entity.ZWaveNodeEntity(self.node, self.zwave_network) @@ -357,3 +355,14 @@ class TestZWaveNodeEntity(unittest.TestCase): def test_not_polled(self): """Test should_poll property.""" self.assertFalse(self.entity.should_poll) + + def test_unique_id(self): + """Test unique_id.""" + self.assertEqual('node-567', self.entity.unique_id) + + def test_unique_id_missing_data(self): + """Test unique_id.""" + self.node.manufacturer_name = None + entity = node_entity.ZWaveNodeEntity(self.node, self.zwave_network) + + self.assertIsNone(entity.unique_id) diff --git a/tests/mock/zwave.py b/tests/mock/zwave.py index 672cc884904..67bfb590c3f 100644 --- a/tests/mock/zwave.py +++ b/tests/mock/zwave.py @@ -119,6 +119,8 @@ class MockNode(MagicMock): product_type='678', command_classes=None, can_wake_up_value=True, + manufacturer_name='Test Manufacturer', + product_name='Test Product', network=None, **kwargs): """Initialize a Z-Wave mock node.""" @@ -128,6 +130,8 @@ class MockNode(MagicMock): self.manufacturer_id = manufacturer_id self.product_id = product_id self.product_type = product_type + self.manufacturer_name = manufacturer_name + self.product_name = product_name self.can_wake_up_value = can_wake_up_value self._command_classes = command_classes or [] if network is not None: