Fix Z-Wave value updated trigger KeyError (#179992)

This commit is contained in:
Martin Hjelmare
2026-08-24 14:41:41 +02:00
committed by GitHub
parent f1742ecc7f
commit 658394dcaf
2 changed files with 117 additions and 15 deletions
@@ -115,19 +115,23 @@ async def async_attach_trigger(
@callback
def async_on_value_updated(
value: Value, device: dr.DeviceEntry, event: dict
value_id: str, device: dr.DeviceEntry, event: dict
) -> None:
"""Handle value update."""
event_value: Value = event["value"]
if event_value != value:
if event_value.value_id != value_id:
return
# Get previous value and its state value if it exists
prev_value_raw = event["args"]["prevValue"]
prev_value = value.metadata.states.get(str(prev_value_raw), prev_value_raw)
prev_value = event_value.metadata.states.get(
str(prev_value_raw), prev_value_raw
)
# Get current value and its state value if it exists
curr_value_raw = event["args"]["newValue"]
curr_value = value.metadata.states.get(str(curr_value_raw), curr_value_raw)
curr_value = event_value.metadata.states.get(
str(curr_value_raw), curr_value_raw
)
# Check from and to values against previous and current values respectively
for value_to_eval, raw_value_to_eval, match in (
(prev_value, prev_value_raw, from_value),
@@ -140,17 +144,17 @@ async def async_attach_trigger(
return
device_name = device.name_by_user or device.name
description = f"Z-Wave value {value.value_id} updated on {device_name}"
description = f"Z-Wave value {event_value.value_id} updated on {device_name}"
payload = {
ATTR_DEVICE_ID: device.id,
ATTR_NODE_ID: value.node.node_id,
ATTR_COMMAND_CLASS: value.command_class,
ATTR_COMMAND_CLASS_NAME: value.command_class_name,
ATTR_PROPERTY: value.property_,
ATTR_PROPERTY_NAME: value.property_name,
ATTR_NODE_ID: event_value.node.node_id,
ATTR_COMMAND_CLASS: event_value.command_class,
ATTR_COMMAND_CLASS_NAME: event_value.command_class_name,
ATTR_PROPERTY: event_value.property_,
ATTR_PROPERTY_NAME: event_value.property_name,
ATTR_ENDPOINT: endpoint,
ATTR_PROPERTY_KEY: value.property_key,
ATTR_PROPERTY_KEY_NAME: value.property_key_name,
ATTR_PROPERTY_KEY: event_value.property_key,
ATTR_PROPERTY_KEY_NAME: event_value.property_key_name,
ATTR_PREVIOUS_VALUE: prev_value,
ATTR_PREVIOUS_VALUE_RAW: prev_value_raw,
ATTR_CURRENT_VALUE: curr_value,
@@ -185,12 +189,10 @@ async def async_attach_trigger(
value_id = get_value_id_str(
node, command_class, property_, endpoint, property_key
)
value = node.values[value_id]
# We need to store the current value and device for the callback
unsubs.append(
node.on(
EVENT_VALUE_UPDATED,
functools.partial(async_on_value_updated, value, device),
functools.partial(async_on_value_updated, value_id, device),
)
)
+100
View File
@@ -1,5 +1,6 @@
"""The tests for Z-Wave JS automation triggers."""
import copy
from unittest.mock import patch
import pytest
@@ -1322,6 +1323,105 @@ async def test_server_reconnect_value_updated(
assert old_listener not in new_node._listeners.get(event_name, [])
async def test_server_reconnect_value_updated_missing_value(
hass: HomeAssistant,
client,
lock_schlage_be469,
lock_schlage_be469_state,
integration,
) -> None:
"""Test value_updated trigger re-registers when value is absent at reconnect."""
trigger_type = f"{DOMAIN}.value_updated"
no_value_filter = async_capture_events(hass, "no_value_filter")
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: [
{
"trigger": {
"platform": trigger_type,
"options": {
"entity_id": SCHLAGE_BE469_LOCK_ENTITY,
"command_class": CommandClass.DOOR_LOCK.value,
"property": "latchStatus",
},
},
"action": {
"event": "no_value_filter",
},
},
]
},
)
# Remove the node so we can re-add it without the target value, simulating the
# reconnect race where node.values is not yet fully populated when
# _create_zwave_listeners runs.
node_removed_event = Event(
type="node removed",
data={
"source": "controller",
"event": "node removed",
"reason": 0,
"node": lock_schlage_be469_state,
},
)
client.driver.controller.receive_event(node_removed_event)
assert 20 not in client.driver.controller.nodes
await hass.async_block_till_done()
partial_state = copy.deepcopy(lock_schlage_be469_state)
partial_state["values"] = [
v for v in partial_state["values"] if v.get("propertyName") != "latchStatus"
]
node_added_event = Event(
type="node added",
data={
"source": "controller",
"event": "node added",
"node": partial_state,
"result": {},
},
)
client.driver.controller.receive_event(node_added_event)
await hass.async_block_till_done()
# Reload the integration, which fires the connected_to_server signal that causes
# _create_zwave_listeners to re-register listeners on the new node. With the old
# code the missing latchStatus value would raise a KeyError and leave the trigger
# without a listener.
await hass.config_entries.async_reload(integration.entry_id)
await hass.async_block_till_done()
# Fire a value updated event on the new node — the trigger must fire even though
# the value was absent when _create_zwave_listeners ran.
new_node = client.driver.controller.nodes[20]
value_updated_event = Event(
type="value updated",
data={
"source": "node",
"event": "value updated",
"nodeId": new_node.node_id,
"args": {
"commandClassName": "Door Lock",
"commandClass": CommandClass.DOOR_LOCK.value,
"endpoint": 0,
"property": "latchStatus",
"newValue": "boo",
"prevValue": "hiss",
"propertyName": "latchStatus",
},
},
)
new_node.receive_event(value_updated_event)
await hass.async_block_till_done()
assert len(no_value_filter) == 1
async def test_zwave_js_old_syntax(
hass: HomeAssistant, client, lock_schlage_be469, integration
) -> None: