Update IDs for rename node/value (#24646)
* Update IDs for rename node/value * Rename devices and entities * Improved coveragepull/24905/head
parent
7f90a1cab2
commit
23dd644f4a
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = []
|
||||
|
|
Loading…
Reference in New Issue