Keep entities of dead Z-Wave devices available (#148611)

This commit is contained in:
AlCalzone 2025-07-11 23:45:57 +02:00 committed by GitHub
parent 87e641bf59
commit ad881d892b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 54 additions and 48 deletions

View File

@ -5,7 +5,6 @@ from __future__ import annotations
from collections.abc import Sequence
from typing import Any
from zwave_js_server.const import NodeStatus
from zwave_js_server.exceptions import BaseZwaveJSServerError
from zwave_js_server.model.driver import Driver
from zwave_js_server.model.value import (
@ -27,8 +26,6 @@ from .discovery import ZwaveDiscoveryInfo
from .helpers import get_device_id, get_unique_id, get_valueless_base_unique_id
EVENT_VALUE_REMOVED = "value removed"
EVENT_DEAD = "dead"
EVENT_ALIVE = "alive"
class ZWaveBaseEntity(Entity):
@ -141,11 +138,6 @@ class ZWaveBaseEntity(Entity):
)
)
for status_event in (EVENT_ALIVE, EVENT_DEAD):
self.async_on_remove(
self.info.node.on(status_event, self._node_status_alive_or_dead)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
@ -211,19 +203,7 @@ class ZWaveBaseEntity(Entity):
@property
def available(self) -> bool:
"""Return entity availability."""
return (
self.driver.client.connected
and bool(self.info.node.ready)
and self.info.node.status != NodeStatus.DEAD
)
@callback
def _node_status_alive_or_dead(self, event_data: dict) -> None:
"""Call when node status changes to alive or dead.
Should not be overridden by subclasses.
"""
self.async_write_ha_state()
return self.driver.client.connected and bool(self.info.node.ready)
@callback
def _value_changed(self, event_data: dict) -> None:

View File

@ -199,18 +199,13 @@ class ZWaveNodeFirmwareUpdate(UpdateEntity):
)
return
# If device is asleep/dead, wait for it to wake up/become alive before
# attempting an update
for status, event_name in (
(NodeStatus.ASLEEP, "wake up"),
(NodeStatus.DEAD, "alive"),
):
if self.node.status == status:
if not self._status_unsub:
self._status_unsub = self.node.once(
event_name, self._update_on_status_change
)
return
# If device is asleep, wait for it to wake up before attempting an update
if self.node.status == NodeStatus.ASLEEP:
if not self._status_unsub:
self._status_unsub = self.node.once(
"wake up", self._update_on_status_change
)
return
try:
# Retrieve all firmware updates including non-stable ones but filter

View File

@ -37,7 +37,11 @@ from homeassistant.helpers import (
)
from homeassistant.setup import async_setup_component
from .common import AIR_TEMPERATURE_SENSOR, EATON_RF9640_ENTITY
from .common import (
AIR_TEMPERATURE_SENSOR,
BULB_6_MULTI_COLOR_LIGHT_ENTITY,
EATON_RF9640_ENTITY,
)
from tests.common import (
MockConfigEntry,
@ -2168,3 +2172,39 @@ async def test_factory_reset_node(
assert len(notifications) == 1
assert list(notifications)[0] == msg_id
assert "network with the home ID `3245146787`" in notifications[msg_id]["message"]
async def test_entity_available_when_node_dead(
hass: HomeAssistant, client, bulb_6_multi_color, integration
) -> None:
"""Test that entities remain available even when the node is dead."""
node = bulb_6_multi_color
state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY)
assert state
assert state.state != STATE_UNAVAILABLE
# Send dead event to the node
event = Event(
"dead", data={"source": "node", "event": "dead", "nodeId": node.node_id}
)
node.receive_event(event)
await hass.async_block_till_done()
# Entity should remain available even though the node is dead
state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY)
assert state
assert state.state != STATE_UNAVAILABLE
# Send alive event to bring the node back
event = Event(
"alive", data={"source": "node", "event": "alive", "nodeId": node.node_id}
)
node.receive_event(event)
await hass.async_block_till_done()
# Entity should still be available
state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY)
assert state
assert state.state != STATE_UNAVAILABLE

View File

@ -28,7 +28,7 @@ from homeassistant.components.zwave_js.lock import (
SERVICE_SET_LOCK_CONFIGURATION,
SERVICE_SET_LOCK_USERCODE,
)
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@ -295,7 +295,8 @@ async def test_door_lock(
assert node.status == NodeStatus.DEAD
state = hass.states.get(SCHLAGE_BE469_LOCK_ENTITY)
assert state
assert state.state == STATE_UNAVAILABLE
# The state should still be locked, even if the node is dead
assert state.state == LockState.LOCKED
async def test_only_one_lock(

View File

@ -277,7 +277,7 @@ async def test_update_entity_dead(
zen_31,
integration,
) -> None:
"""Test update occurs when device is dead after it becomes alive."""
"""Test update occurs even when device is dead."""
event = Event(
"dead",
data={"source": "node", "event": "dead", "nodeId": zen_31.node_id},
@ -290,17 +290,7 @@ async def test_update_entity_dead(
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5, days=1))
await hass.async_block_till_done()
# Because node is asleep we shouldn't attempt to check for firmware updates
assert len(client.async_send_command.call_args_list) == 0
event = Event(
"alive",
data={"source": "node", "event": "alive", "nodeId": zen_31.node_id},
)
zen_31.receive_event(event)
await hass.async_block_till_done()
# Now that the node is up we can check for updates
# Checking for firmware updates should proceed even for dead nodes
assert len(client.async_send_command.call_args_list) > 0
args = client.async_send_command.call_args_list[0][0][0]