Fix cover state updates for legacy Multilevel Switch based Z-Wave covers (#165003)

This commit is contained in:
AlCalzone
2026-03-07 12:16:30 +01:00
committed by GitHub
parent 642f603ea2
commit beec21c4a9
2 changed files with 188 additions and 6 deletions
+18 -6
View File
@@ -87,6 +87,9 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
_current_position_value: ZwaveValue | None = None
_target_position_value: ZwaveValue | None = None
_stop_position_value: ZwaveValue | None = None
# Keep track of the target position for legacy devices
# that don't include the targetValue in their reports.
_commanded_target_position: int | None = None
def _set_position_values(
self,
@@ -153,12 +156,19 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
if not self._attr_is_opening and not self._attr_is_closing:
return
if (
(current := self._current_position_value) is not None
and (target := self._target_position_value) is not None
and current.value is not None
and current.value == target.value
):
if (current := self._current_position_value) is None or current.value is None:
return
# Prefer the Z-Wave targetValue property when the device reports it.
# Legacy multilevel switches only report currentValue, so fall back to
# the target position we commanded when targetValue is not available.
target_val = (
t.value
if (t := self._target_position_value) is not None and t.value is not None
else self._commanded_target_position
)
if target_val is not None and current.value == target_val:
self._attr_is_opening = False
self._attr_is_closing = False
@@ -203,6 +213,8 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
else:
return
self._commanded_target_position = target_position
self.async_write_ha_state()
async def async_set_cover_position(self, **kwargs: Any) -> None:
+170
View File
@@ -1676,3 +1676,173 @@ async def test_multilevel_switch_cover_moving_state_none_result(
state = hass.states.get(WINDOW_COVER_ENTITY)
assert state.state == CoverState.CLOSED
async def test_multilevel_switch_cover_unsupervised_no_target_value_update(
hass: HomeAssistant,
client: MagicMock,
chain_actuator_zws12: Node,
integration: MockConfigEntry,
) -> None:
"""Test cover transitions to CLOSED/OPEN without targetValue updates.
Regression test for issue #164915: covers with no supervision where the
device only sends currentValue updates (no targetValue updates) should
still properly transition from CLOSING/OPENING to CLOSED/OPEN once the
current position reaches the fully closed or fully open position.
"""
node = chain_actuator_zws12
state = hass.states.get(WINDOW_COVER_ENTITY)
assert state
assert state.state == CoverState.CLOSED
# Set cover to fully open position via an unsolicited device report.
# This mirrors the log sequence: currentValue 0 => 99
node.receive_event(
Event(
type="value updated",
data={
"source": "node",
"event": "value updated",
"nodeId": node.node_id,
"args": {
"commandClassName": "Multilevel Switch",
"commandClass": 38,
"endpoint": 0,
"property": "currentValue",
"newValue": 99,
"prevValue": 0,
"propertyName": "currentValue",
},
},
)
)
state = hass.states.get(WINDOW_COVER_ENTITY)
assert state.state == CoverState.OPEN
# Simulate SUCCESS_UNSUPERVISED (no Supervision CC on device).
client.async_send_command.return_value = {
"result": {"status": SetValueStatus.SUCCESS_UNSUPERVISED}
}
# Close cover – should optimistically enter CLOSING.
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_CLOSE_COVER,
{ATTR_ENTITY_ID: WINDOW_COVER_ENTITY},
blocking=True,
)
state = hass.states.get(WINDOW_COVER_ENTITY)
assert state.state == CoverState.CLOSING
# Simulate intermediate report from device (currentValue only, no targetValue).
# Log sequence: currentValue 99 => 78
node.receive_event(
Event(
type="value updated",
data={
"source": "node",
"event": "value updated",
"nodeId": node.node_id,
"args": {
"commandClassName": "Multilevel Switch",
"commandClass": 38,
"endpoint": 0,
"property": "currentValue",
"newValue": 78,
"prevValue": 99,
"propertyName": "currentValue",
},
},
)
)
state = hass.states.get(WINDOW_COVER_ENTITY)
assert state.state == CoverState.CLOSING
# Simulate device reaching the fully closed position.
# Log sequence: currentValue 78 => 0
node.receive_event(
Event(
type="value updated",
data={
"source": "node",
"event": "value updated",
"nodeId": node.node_id,
"args": {
"commandClassName": "Multilevel Switch",
"commandClass": 38,
"endpoint": 0,
"property": "currentValue",
"newValue": 0,
"prevValue": 78,
"propertyName": "currentValue",
},
},
)
)
# Cover must leave CLOSING and report CLOSED.
state = hass.states.get(WINDOW_COVER_ENTITY)
assert state.state == CoverState.CLOSED
# Now test the opening direction with the same conditions.
# Log sequence: currentValue 0 => 18 => 99
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_OPEN_COVER,
{ATTR_ENTITY_ID: WINDOW_COVER_ENTITY},
blocking=True,
)
state = hass.states.get(WINDOW_COVER_ENTITY)
assert state.state == CoverState.OPENING
node.receive_event(
Event(
type="value updated",
data={
"source": "node",
"event": "value updated",
"nodeId": node.node_id,
"args": {
"commandClassName": "Multilevel Switch",
"commandClass": 38,
"endpoint": 0,
"property": "currentValue",
"newValue": 18,
"prevValue": 0,
"propertyName": "currentValue",
},
},
)
)
state = hass.states.get(WINDOW_COVER_ENTITY)
assert state.state == CoverState.OPENING
node.receive_event(
Event(
type="value updated",
data={
"source": "node",
"event": "value updated",
"nodeId": node.node_id,
"args": {
"commandClassName": "Multilevel Switch",
"commandClass": 38,
"endpoint": 0,
"property": "currentValue",
"newValue": 99,
"prevValue": 18,
"propertyName": "currentValue",
},
},
)
)
# Cover must leave OPENING and report OPEN.
state = hass.states.get(WINDOW_COVER_ENTITY)
assert state.state == CoverState.OPEN