Update IDs for rename node/value (#24646)

* Update IDs for rename node/value

* Rename devices and entities

* Improved coverage
pull/24905/head
Penny Wood 2019-07-02 06:54:19 +08:00 committed by Paulus Schoutsen
parent 7f90a1cab2
commit 23dd644f4a
7 changed files with 250 additions and 19 deletions

View File

@ -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(

View File

@ -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

View File

@ -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:

View File

@ -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'

View File

@ -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):

View File

@ -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

View File

@ -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 = []