mirror of
https://github.com/home-assistant/core.git
synced 2026-09-26 01:11:51 -04:00
Re-discover Z-Wave list sensors when metadata states change (#166271)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
co-authored by
Martin Hjelmare
parent
5ef28932e5
commit
c598aa6964
@@ -108,6 +108,8 @@ from .const import (
|
||||
DOMAIN,
|
||||
ESPHOME_ADDON_VERSION,
|
||||
EVENT_DEVICE_ADDED_TO_REGISTRY,
|
||||
EVENT_METADATA_UPDATED,
|
||||
EVENT_VALUE_ADDED,
|
||||
EVENT_VALUE_UPDATED,
|
||||
LIB_LOGGER,
|
||||
LOGGER,
|
||||
@@ -599,6 +601,7 @@ class ControllerEvents:
|
||||
f"{DOMAIN}.node_reset_and_removed.{dev_id[1]}",
|
||||
)
|
||||
|
||||
self.node_events.value_updates_disc_info.pop(node.node_id, None)
|
||||
self.remove_device(device)
|
||||
|
||||
@callback
|
||||
@@ -756,6 +759,9 @@ class NodeEvents:
|
||||
self.dev_reg = controller_events.dev_reg
|
||||
self.ent_reg = er.async_get(hass)
|
||||
self.hass = hass
|
||||
self.value_updates_disc_info: dict[
|
||||
int, dict[str, PlatformZwaveDiscoveryInfo]
|
||||
] = {}
|
||||
|
||||
async def async_on_node_ready(self, node: ZwaveNode) -> None:
|
||||
"""Handle node ready event."""
|
||||
@@ -766,29 +772,23 @@ class NodeEvents:
|
||||
# Remove any old value ids if this is a reinterview.
|
||||
self.controller_events.discovered_value_ids.pop(device.id, None)
|
||||
|
||||
# Store the discovery info so it can be reused when re-discovering entities
|
||||
value_updates_disc_info: dict[str, PlatformZwaveDiscoveryInfo] = {}
|
||||
self.value_updates_disc_info[node.node_id] = value_updates_disc_info
|
||||
|
||||
# run discovery on all node values and create/update entities
|
||||
await asyncio.gather(
|
||||
*(
|
||||
self.async_handle_discovery_info(
|
||||
device, disc_info, value_updates_disc_info
|
||||
)
|
||||
for disc_info in async_discover_node_values(
|
||||
node, device, self.controller_events.discovered_value_ids
|
||||
)
|
||||
)
|
||||
)
|
||||
for disc_info in async_discover_node_values(
|
||||
node, device, self.controller_events.discovered_value_ids
|
||||
):
|
||||
self.async_handle_discovery_info(device, disc_info, value_updates_disc_info)
|
||||
|
||||
# add listeners to handle new values that get added later
|
||||
for event in ("value added", EVENT_VALUE_UPDATED, "metadata updated"):
|
||||
for event in (EVENT_VALUE_ADDED, EVENT_VALUE_UPDATED, EVENT_METADATA_UPDATED):
|
||||
self.config_entry.async_on_unload(
|
||||
node.on(
|
||||
event,
|
||||
lambda event: self.hass.async_create_task(
|
||||
self.async_on_value_added(
|
||||
value_updates_disc_info, event["value"]
|
||||
)
|
||||
lambda event: self.async_on_value_added(
|
||||
value_updates_disc_info, event["value"]
|
||||
),
|
||||
)
|
||||
)
|
||||
@@ -852,7 +852,8 @@ class NodeEvents:
|
||||
# an upstream bug, or the change has been reverted.
|
||||
async_delete_issue(self.hass, DOMAIN, issue_id)
|
||||
|
||||
async def async_handle_discovery_info(
|
||||
@callback
|
||||
def async_handle_discovery_info(
|
||||
self,
|
||||
device: dr.DeviceEntry,
|
||||
disc_info: PlatformZwaveDiscoveryInfo,
|
||||
@@ -897,15 +898,17 @@ class NodeEvents:
|
||||
)
|
||||
)
|
||||
|
||||
async def async_on_value_added(
|
||||
@callback
|
||||
def async_on_value_added(
|
||||
self,
|
||||
value_updates_disc_info: dict[str, PlatformZwaveDiscoveryInfo],
|
||||
value: Value,
|
||||
) -> None:
|
||||
"""Fire value updated event."""
|
||||
# If node isn't ready or a device for this node doesn't already exist, we can
|
||||
# let the node ready event handler perform discovery. If a value has already
|
||||
# been processed, we don't need to do it again
|
||||
"""Run discovery when a value is added, updated or its metadata is changed."""
|
||||
# If node isn't ready or a device for this node doesn't already
|
||||
# exist, we can let the node ready event handler perform discovery.
|
||||
# If a value has already been processed, we don't need to do it
|
||||
# again
|
||||
device_id = get_device_id(
|
||||
self.controller_events.driver_events.driver, value.node
|
||||
)
|
||||
@@ -917,16 +920,10 @@ class NodeEvents:
|
||||
return
|
||||
|
||||
LOGGER.debug("Processing node %s added value %s", value.node, value)
|
||||
await asyncio.gather(
|
||||
*(
|
||||
self.async_handle_discovery_info(
|
||||
device, disc_info, value_updates_disc_info
|
||||
)
|
||||
for disc_info in async_discover_single_value(
|
||||
value, device, self.controller_events.discovered_value_ids
|
||||
)
|
||||
)
|
||||
)
|
||||
for disc_info in async_discover_single_value(
|
||||
value, device, self.controller_events.discovered_value_ids
|
||||
):
|
||||
self.async_handle_discovery_info(device, disc_info, value_updates_disc_info)
|
||||
|
||||
@callback
|
||||
def async_on_value_notification(self, notification: ValueNotification) -> None:
|
||||
|
||||
@@ -45,6 +45,7 @@ EVENT_DEVICE_ADDED_TO_REGISTRY = f"{DOMAIN}_device_added_to_registry"
|
||||
EVENT_VALUE_ADDED = "value added"
|
||||
EVENT_VALUE_REMOVED = "value removed"
|
||||
EVENT_VALUE_UPDATED = "value updated"
|
||||
EVENT_METADATA_UPDATED = "metadata updated"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
LIB_LOGGER = logging.getLogger("zwave_js_server")
|
||||
|
||||
@@ -14,7 +14,6 @@ from zwave_js_server.model.value import (
|
||||
get_value_id_str,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -24,6 +23,7 @@ from homeassistant.helpers.typing import UNDEFINED
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
EVENT_METADATA_UPDATED,
|
||||
EVENT_VALUE_ADDED,
|
||||
EVENT_VALUE_REMOVED,
|
||||
EVENT_VALUE_UPDATED,
|
||||
@@ -31,7 +31,7 @@ from .const import (
|
||||
)
|
||||
from .discovery_data_template import BaseDiscoverySchemaDataTemplate
|
||||
from .helpers import get_device_id, get_unique_id, get_valueless_base_unique_id
|
||||
from .models import PlatformZwaveDiscoveryInfo, ZwaveDiscoveryInfo
|
||||
from .models import PlatformZwaveDiscoveryInfo, ZwaveDiscoveryInfo, ZwaveJSConfigEntry
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
@@ -55,10 +55,11 @@ class ZWaveBaseEntity(Entity):
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
info: ZwaveDiscoveryInfo | NewZwaveDiscoveryInfo
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: ZwaveJSConfigEntry,
|
||||
driver: Driver,
|
||||
info: ZwaveDiscoveryInfo | NewZwaveDiscoveryInfo,
|
||||
) -> None:
|
||||
@@ -144,6 +145,9 @@ class ZWaveBaseEntity(Entity):
|
||||
self.async_on_remove(
|
||||
self.info.node.on(EVENT_VALUE_REMOVED, self._value_removed)
|
||||
)
|
||||
self.async_on_remove(
|
||||
self.info.node.on(EVENT_METADATA_UPDATED, self._metadata_updated)
|
||||
)
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
@@ -304,6 +308,57 @@ class ZWaveBaseEntity(Entity):
|
||||
self.on_value_update()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def should_rediscover_on_metadata_update(self) -> bool:
|
||||
"""Check if a metadata update requires entity rediscovery.
|
||||
|
||||
To be overridden by subclasses that need to detect metadata changes.
|
||||
Return True if the entity needs to be removed and re-discovered.
|
||||
"""
|
||||
return False
|
||||
|
||||
@callback
|
||||
def _metadata_updated(self, event_data: dict) -> None:
|
||||
"""Handle metadata update requiring entity rediscovery.
|
||||
|
||||
Should not be overridden by subclasses.
|
||||
"""
|
||||
value = event_data["value"]
|
||||
if value.value_id != self.info.primary_value.value_id:
|
||||
return
|
||||
|
||||
if not self.should_rediscover_on_metadata_update():
|
||||
return
|
||||
|
||||
LOGGER.debug(
|
||||
"[%s] Metadata options changed for %s, removing for rediscovery",
|
||||
self.entity_id,
|
||||
value.value_id,
|
||||
)
|
||||
|
||||
self.hass.async_create_task(self._async_remove_and_rediscover(value))
|
||||
|
||||
async def _async_remove_and_rediscover(self, value: ZwaveValue) -> None:
|
||||
"""Remove entity and trigger re-discovery with updated metadata."""
|
||||
assert self.device_entry is not None
|
||||
controller_events = (
|
||||
self.config_entry.runtime_data.driver_events.controller_events
|
||||
)
|
||||
|
||||
# Remove entity first so the unique_id is freed up
|
||||
await self.async_remove()
|
||||
|
||||
# Now clear from discovered_value_ids and trigger re-discovery
|
||||
# using the existing discovery info dict
|
||||
controller_events.discovered_value_ids[self.device_entry.id].discard(
|
||||
value.value_id
|
||||
)
|
||||
node_events = controller_events.node_events
|
||||
value_updates_disc_info = node_events.value_updates_disc_info[
|
||||
value.node.node_id
|
||||
]
|
||||
node_events.async_on_value_added(value_updates_disc_info, value)
|
||||
|
||||
@callback
|
||||
def get_zwave_value(
|
||||
self,
|
||||
|
||||
@@ -985,6 +985,13 @@ class ZWaveListSensor(ZwaveSensor):
|
||||
self._attr_device_class = SensorDeviceClass.ENUM
|
||||
self._attr_options = list(info.primary_value.metadata.states.values())
|
||||
|
||||
@callback
|
||||
def should_rediscover_on_metadata_update(self) -> bool:
|
||||
"""Check if metadata states have changed."""
|
||||
return list(self.info.primary_value.metadata.states.values()) != (
|
||||
self._attr_options or []
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, str] | None:
|
||||
"""Return the device specific state attributes."""
|
||||
|
||||
@@ -1446,12 +1446,17 @@ async def test_node_removed(
|
||||
assert old_device
|
||||
assert old_device.id
|
||||
|
||||
node_events = integration.runtime_data.driver_events.controller_events.node_events
|
||||
assert node.node_id in node_events.value_updates_disc_info
|
||||
|
||||
event = {"node": node, "reason": 0}
|
||||
|
||||
client.driver.controller.emit("node removed", event)
|
||||
await hass.async_block_till_done()
|
||||
# Assert device has been removed
|
||||
assert not device_registry.async_get(old_device.id)
|
||||
# Assert value_updates_disc_info has been cleaned up
|
||||
assert node.node_id not in node_events.value_updates_disc_info
|
||||
|
||||
|
||||
async def test_replace_same_node(
|
||||
|
||||
@@ -926,6 +926,106 @@ async def test_opening_state_sensor(
|
||||
assert hass.states.get(entity_id) is None
|
||||
|
||||
|
||||
async def test_opening_state_sensor_metadata_options_change(
|
||||
hass: HomeAssistant,
|
||||
hoppe_ehandle_connectsense: Node,
|
||||
integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test Opening state sensor is rediscovered when metadata options change."""
|
||||
entity_id = "sensor.ehandle_connectsense_opening_state"
|
||||
node = hoppe_ehandle_connectsense
|
||||
|
||||
# Verify initial state with 2 options
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "Closed"
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENUM
|
||||
assert state.attributes[ATTR_OPTIONS] == ["Closed", "Open"]
|
||||
|
||||
# Simulate metadata update adding "Tilted" state
|
||||
event = Event(
|
||||
"metadata updated",
|
||||
{
|
||||
"source": "node",
|
||||
"event": "metadata updated",
|
||||
"nodeId": node.node_id,
|
||||
"args": {
|
||||
"commandClassName": "Notification",
|
||||
"commandClass": 113,
|
||||
"endpoint": 0,
|
||||
"property": "Access Control",
|
||||
"propertyKey": "Opening state",
|
||||
"propertyName": "Access Control",
|
||||
"propertyKeyName": "Opening state",
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": True,
|
||||
"writeable": False,
|
||||
"label": "Opening state",
|
||||
"ccSpecific": {"notificationType": 6},
|
||||
"min": 0,
|
||||
"max": 255,
|
||||
"states": {
|
||||
"0": "Closed",
|
||||
"1": "Open",
|
||||
"2": "Tilted",
|
||||
},
|
||||
"stateful": True,
|
||||
"secret": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
node.receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Entity should be rediscovered with 3 options
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes[ATTR_OPTIONS] == ["Closed", "Open", "Tilted"]
|
||||
|
||||
# Simulate metadata update removing "Tilted" state
|
||||
event = Event(
|
||||
"metadata updated",
|
||||
{
|
||||
"source": "node",
|
||||
"event": "metadata updated",
|
||||
"nodeId": node.node_id,
|
||||
"args": {
|
||||
"commandClassName": "Notification",
|
||||
"commandClass": 113,
|
||||
"endpoint": 0,
|
||||
"property": "Access Control",
|
||||
"propertyKey": "Opening state",
|
||||
"propertyName": "Access Control",
|
||||
"propertyKeyName": "Opening state",
|
||||
"metadata": {
|
||||
"type": "number",
|
||||
"readable": True,
|
||||
"writeable": False,
|
||||
"label": "Opening state",
|
||||
"ccSpecific": {"notificationType": 6},
|
||||
"min": 0,
|
||||
"max": 255,
|
||||
"states": {
|
||||
"0": "Closed",
|
||||
"1": "Open",
|
||||
},
|
||||
"stateful": True,
|
||||
"secret": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
node.receive_event(event)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Entity should be rediscovered with 2 options again
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.attributes[ATTR_OPTIONS] == ["Closed", "Open"]
|
||||
|
||||
|
||||
CONTROLLER_STATISTICS_ENTITY_PREFIX = "sensor.z_stick_gen5_usb_controller_"
|
||||
# controller statistics with initial state of 0
|
||||
CONTROLLER_STATISTICS_SUFFIXES = {
|
||||
|
||||
Reference in New Issue
Block a user