diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index a5a460d129e29b62c4a69b7b7a277c505a4418be..b32a76be40b1a8f7a3e71b9fad32f682aa0531ed 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -68,12 +68,14 @@ SUPPORTED_PLATFORMS = ['binary_sensor', 'climate', 'cover', 'fan', RENAME_NODE_SCHEMA = vol.Schema({ vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), vol.Required(const.ATTR_NAME): cv.string, + vol.Optional(const.ATTR_UPDATE_IDS, default=False): cv.boolean, }) RENAME_VALUE_SCHEMA = vol.Schema({ vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), vol.Required(const.ATTR_VALUE_ID): vol.Coerce(int), vol.Required(const.ATTR_NAME): cv.string, + vol.Optional(const.ATTR_UPDATE_IDS, default=False): cv.boolean, }) SET_CONFIG_PARAMETER_SCHEMA = vol.Schema({ @@ -389,8 +391,7 @@ async def async_setup_entry(hass, config_entry): 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) + hass.add_job(check_has_unique_id, entity, _on_ready, _on_timeout) def node_removed(node): node_id = node.node_id @@ -491,6 +492,7 @@ async def async_setup_entry(hass, config_entry): if hass.state == CoreState.running: hass.bus.fire(const.EVENT_NETWORK_STOP) + @callback def rename_node(service): """Rename a node.""" node_id = service.data.get(const.ATTR_NODE_ID) @@ -499,7 +501,19 @@ async def async_setup_entry(hass, config_entry): node.name = name _LOGGER.info( "Renamed Z-Wave node %d to %s", node_id, name) + update_ids = service.data.get(const.ATTR_UPDATE_IDS) + # We want to rename the device, the node entity, + # and all the contained entities + node_key = 'node-{}'.format(node_id) + entity = hass.data[DATA_DEVICES][node_key] + hass.async_create_task(entity.node_renamed(update_ids)) + for key in list(hass.data[DATA_DEVICES]): + if not key.startswith('{}-'.format(node_id)): + continue + entity = hass.data[DATA_DEVICES][key] + hass.async_create_task(entity.value_renamed(update_ids)) + @callback def rename_value(service): """Rename a node value.""" node_id = service.data.get(const.ATTR_NODE_ID) @@ -511,6 +525,10 @@ async def async_setup_entry(hass, config_entry): _LOGGER.info( "Renamed Z-Wave value (Node %d Value %d) to %s", node_id, value_id, name) + update_ids = service.data.get(const.ATTR_UPDATE_IDS) + value_key = '{}-{}'.format(node_id, value_id) + entity = hass.data[DATA_DEVICES][value_key] + hass.async_create_task(entity.value_renamed(update_ids)) def set_poll_intensity(service): """Set the polling intensity of a node value.""" @@ -996,7 +1014,7 @@ class ZWaveDeviceEntityValues(): self._hass.add_job(discover_device, component, device) else: self._hass.add_job(check_has_unique_id, device, _on_ready, - _on_timeout, self._hass.loop) + _on_timeout) class ZWaveDeviceEntity(ZWaveBaseEntity): @@ -1034,6 +1052,25 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): self.update_properties() self.maybe_schedule_update() + async def value_renamed(self, update_ids=False): + """Rename the node and update any IDs.""" + self._name = _value_name(self.values.primary) + if update_ids: + # Update entity ID. + ent_reg = await async_get_registry(self.hass) + new_entity_id = ent_reg.async_generate_entity_id( + self.platform.domain, + self._name, + self.platform.entities.keys() - {self.entity_id}) + if new_entity_id != self.entity_id: + # Don't change the name attribute, it will be None unless + # customised and if it's been customised, keep the + # customisation. + ent_reg.async_update_entity( + self.entity_id, new_entity_id=new_entity_id) + return + self.async_schedule_update_ha_state() + async def async_added_to_hass(self): """Add device to dict.""" async_dispatcher_connect( diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index 67b5341a4e60a2f8c178f4a272e4d23e5aff21ee..5a09b54235de0835146c5e4b938ce9a6203897e6 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -19,6 +19,7 @@ ATTR_CONFIG_VALUE = "value" ATTR_POLL_INTENSITY = "poll_intensity" ATTR_VALUE_INDEX = "value_index" ATTR_VALUE_INSTANCE = "value_instance" +ATTR_UPDATE_IDS = 'update_ids' NETWORK_READY_WAIT_SECS = 300 NODE_READY_WAIT_SECS = 30 diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index 3bba18f5c020588cc80da353126c8a61b0eaaeef..9a721ecf2d7609ccb99ce6bf9fd1c19509adb2ce 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -1,9 +1,13 @@ """Entity class that represents Z-Wave node.""" import logging +from itertools import count from homeassistant.core import callback -from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_WAKEUP, ATTR_ENTITY_ID +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, ATTR_WAKEUP, ATTR_ENTITY_ID) from homeassistant.helpers.entity_registry import async_get_registry +from homeassistant.helpers.device_registry import ( + async_get_registry as get_dev_reg) from homeassistant.helpers.entity import Entity from .const import ( @@ -192,6 +196,42 @@ class ZWaveNodeEntity(ZWaveBaseEntity): self.maybe_schedule_update() + async def node_renamed(self, update_ids=False): + """Rename the node and update any IDs.""" + self._name = node_name(self.node) + # Set the name in the devices. If they're customised + # the customisation will not be stored as name and will stick. + dev_reg = await get_dev_reg(self.hass) + device = dev_reg.async_get_device( + identifiers={(DOMAIN, self.node_id), }, + connections=set()) + dev_reg.async_update_device(device.id, name=self._name) + # update sub-devices too + for i in count(2): + identifier = (DOMAIN, self.node_id, i) + device = dev_reg.async_get_device( + identifiers={identifier, }, + connections=set()) + if not device: + break + new_name = "{} ({})".format(self._name, i) + dev_reg.async_update_device(device.id, name=new_name) + + # Update entity ID. + if update_ids: + ent_reg = await async_get_registry(self.hass) + new_entity_id = ent_reg.async_generate_entity_id( + DOMAIN, self._name, + self.platform.entities.keys() - {self.entity_id}) + if new_entity_id != self.entity_id: + # Don't change the name attribute, it will be None unless + # customised and if it's been customised, keep the + # customisation. + ent_reg.async_update_entity( + self.entity_id, new_entity_id=new_entity_id) + return + self.async_schedule_update_ha_state() + def network_node_event(self, node, value): """Handle a node activated event on the network.""" if node.node_id == self.node.node_id: diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml index 83e6ea2533b18094445f2782fb86d61af5954bd9..37b1223275934ee93dca7a72e5a4da15bbffa666 100644 --- a/homeassistant/components/zwave/services.yaml +++ b/homeassistant/components/zwave/services.yaml @@ -168,6 +168,9 @@ rename_node: node_id: description: ID of the node to rename. example: 10 + update_ids: + description: (optional) Rename the entity IDs for entities of this node. + example: True name: description: New Name example: 'kitchen' @@ -181,6 +184,9 @@ rename_value: value_id: description: ID of the value to rename. example: 72037594255792737 + update_ids: + description: (optional) Update the entity ID for this value's entity. + example: True name: description: New Name example: 'Luminosity' diff --git a/homeassistant/components/zwave/util.py b/homeassistant/components/zwave/util.py index 312d72575a94aeed0e5b634155dabf9f3be28387..a3b6d9a956d2679e81fce09c200ed5977a028282 100644 --- a/homeassistant/components/zwave/util.py +++ b/homeassistant/components/zwave/util.py @@ -74,7 +74,7 @@ def node_name(node): return 'Unknown Node {}'.format(node.node_id) -async def check_has_unique_id(entity, ready_callback, timeout_callback, loop): +async def check_has_unique_id(entity, ready_callback, timeout_callback): """Wait for entity to have unique_id.""" start_time = dt_util.utcnow() while True: @@ -86,7 +86,7 @@ async def check_has_unique_id(entity, ready_callback, timeout_callback, loop): # Wait up to NODE_READY_WAIT_SECS seconds for unique_id to appear. timeout_callback(waited) return - await asyncio.sleep(1, loop=loop) + await asyncio.sleep(1) def is_node_parsed(node): diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 77c788035abb3f9b7ea07f567979edc8263631c5..8f53850b6191115411f9dba1d12ab723b6ff6537 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -136,11 +136,13 @@ class DeviceRegistry: @callback def async_update_device( - self, device_id, *, area_id=_UNDEF, name_by_user=_UNDEF, + self, device_id, *, area_id=_UNDEF, + name=_UNDEF, name_by_user=_UNDEF, new_identifiers=_UNDEF): """Update properties of a device.""" return self._async_update_device( - device_id, area_id=area_id, name_by_user=name_by_user, + device_id, area_id=area_id, + name=name, name_by_user=name_by_user, new_identifiers=new_identifiers) @callback diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 69ee7c45a9b50a592dbfe8a815e2bd860d2df777..19830b1343c93431250ea25c3cf3d7c9aafe1190 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -2,26 +2,27 @@ import asyncio from collections import OrderedDict from datetime import datetime +import unittest +from unittest.mock import MagicMock, patch + +import pytest from pytz import utc import voluptuous as vol -import unittest -from unittest.mock import patch, MagicMock - from homeassistant.bootstrap import async_setup_component -from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START from homeassistant.components import zwave -from homeassistant.components.zwave.binary_sensor import get_device from homeassistant.components.zwave import ( - const, CONFIG_SCHEMA, CONF_DEVICE_CONFIG_GLOB, DATA_NETWORK) + CONF_DEVICE_CONFIG_GLOB, CONFIG_SCHEMA, DATA_NETWORK, const) +from homeassistant.components.zwave.binary_sensor import get_device +from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START +from homeassistant.helpers.entity_registry import async_get_registry +from homeassistant.helpers.device_registry import ( + async_get_registry as get_dev_reg) from homeassistant.setup import setup_component -from tests.common import mock_registry - -import pytest from tests.common import ( - get_test_home_assistant, async_fire_time_changed, mock_coro) -from tests.mock.zwave import MockNetwork, MockNode, MockValue, MockEntityValues + async_fire_time_changed, get_test_home_assistant, mock_coro, mock_registry) +from tests.mock.zwave import MockEntityValues, MockNetwork, MockNode, MockValue async def test_valid_device_config(hass, mock_openzwave): @@ -382,6 +383,150 @@ async def test_value_discovery(hass, mock_openzwave): 'binary_sensor.mock_node_mock_value').state == 'off' +async def test_value_entities(hass, mock_openzwave): + """Test discovery of a node.""" + mock_receivers = {} + + def mock_connect(receiver, signal, *args, **kwargs): + mock_receivers[signal] = receiver + + with patch('pydispatch.dispatcher.connect', new=mock_connect): + await async_setup_component(hass, 'zwave', {'zwave': {}}) + await hass.async_block_till_done() + + zwave_network = hass.data[DATA_NETWORK] + zwave_network.state = MockNetwork.STATE_READY + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + assert mock_receivers + + hass.async_add_job( + mock_receivers[MockNetwork.SIGNAL_ALL_NODES_QUERIED]) + node = MockNode(node_id=11, generic=const.GENERIC_TYPE_SENSOR_BINARY) + zwave_network.nodes = {node.node_id: node} + value = MockValue( + data=False, node=node, index=12, instance=1, + command_class=const.COMMAND_CLASS_SENSOR_BINARY, + type=const.TYPE_BOOL, genre=const.GENRE_USER) + node.values = {'primary': value, value.value_id: value} + value2 = MockValue( + data=False, node=node, index=12, instance=2, + label="Mock Value B", + command_class=const.COMMAND_CLASS_SENSOR_BINARY, + type=const.TYPE_BOOL, genre=const.GENRE_USER) + node.values[value2.value_id] = value2 + + hass.async_add_job( + mock_receivers[MockNetwork.SIGNAL_NODE_ADDED], node) + hass.async_add_job( + mock_receivers[MockNetwork.SIGNAL_VALUE_ADDED], node, value) + hass.async_add_job( + mock_receivers[MockNetwork.SIGNAL_VALUE_ADDED], node, value2) + await hass.async_block_till_done() + + assert hass.states.get( + 'binary_sensor.mock_node_mock_value').state == 'off' + assert hass.states.get( + 'binary_sensor.mock_node_mock_value_b').state == 'off' + + ent_reg = await async_get_registry(hass) + dev_reg = await get_dev_reg(hass) + + entry = ent_reg.async_get('zwave.mock_node') + assert entry is not None + assert entry.unique_id == 'node-{}'.format(node.node_id) + node_dev_id = entry.device_id + + entry = ent_reg.async_get('binary_sensor.mock_node_mock_value') + assert entry is not None + assert entry.unique_id == '{}-{}'.format(node.node_id, value.object_id) + assert entry.name is None + assert entry.device_id == node_dev_id + + entry = ent_reg.async_get('binary_sensor.mock_node_mock_value_b') + assert entry is not None + assert entry.unique_id == '{}-{}'.format(node.node_id, value2.object_id) + assert entry.name is None + assert entry.device_id != node_dev_id + device_id_b = entry.device_id + + device = dev_reg.async_get(node_dev_id) + assert device is not None + assert device.name == node.name + old_device = device + + device = dev_reg.async_get(device_id_b) + assert device is not None + assert device.name == "{} ({})".format(node.name, value2.instance) + + # test renaming without updating + await hass.services.async_call('zwave', 'rename_node', { + const.ATTR_NODE_ID: node.node_id, + const.ATTR_NAME: "Demo Node", + }) + await hass.async_block_till_done() + + assert node.name == "Demo Node" + + entry = ent_reg.async_get('zwave.mock_node') + assert entry is not None + + entry = ent_reg.async_get('binary_sensor.mock_node_mock_value') + assert entry is not None + + entry = ent_reg.async_get('binary_sensor.mock_node_mock_value_b') + assert entry is not None + + device = dev_reg.async_get(node_dev_id) + assert device is not None + assert device.id == old_device.id + assert device.name == node.name + + device = dev_reg.async_get(device_id_b) + assert device is not None + assert device.name == "{} ({})".format(node.name, value2.instance) + + # test renaming + await hass.services.async_call('zwave', 'rename_node', { + const.ATTR_NODE_ID: node.node_id, + const.ATTR_UPDATE_IDS: True, + const.ATTR_NAME: "New Node", + }) + await hass.async_block_till_done() + + assert node.name == "New Node" + + entry = ent_reg.async_get('zwave.new_node') + assert entry is not None + assert entry.unique_id == 'node-{}'.format(node.node_id) + + entry = ent_reg.async_get('binary_sensor.new_node_mock_value') + assert entry is not None + assert entry.unique_id == '{}-{}'.format(node.node_id, value.object_id) + + device = dev_reg.async_get(node_dev_id) + assert device is not None + assert device.id == old_device.id + assert device.name == node.name + + device = dev_reg.async_get(device_id_b) + assert device is not None + assert device.name == "{} ({})".format(node.name, value2.instance) + + await hass.services.async_call('zwave', 'rename_value', { + const.ATTR_NODE_ID: node.node_id, + const.ATTR_VALUE_ID: value.object_id, + const.ATTR_UPDATE_IDS: True, + const.ATTR_NAME: "New Label", + }) + await hass.async_block_till_done() + + entry = ent_reg.async_get('binary_sensor.new_node_new_label') + assert entry is not None + assert entry.unique_id == '{}-{}'.format(node.node_id, value.object_id) + + async def test_value_discovery_existing_entity(hass, mock_openzwave): """Test discovery of a node.""" mock_receivers = []