mirror of
https://github.com/home-assistant/core.git
synced 2026-09-25 07:51:46 -05:00
Fix cover state updates for legacy Multilevel Switch based Z-Wave covers (#165003)
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user