Compare commits

...

5 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
4b5dc3f016 Address code review feedback: simplify logic and improve test clarity
Co-authored-by: edenhaus <26537646+edenhaus@users.noreply.github.com>
2026-02-16 14:02:10 +00:00
copilot-swe-agent[bot]
6688281acf Clear opening/closing state when target equals current position
Co-authored-by: edenhaus <26537646+edenhaus@users.noreply.github.com>
2026-02-16 14:01:04 +00:00
copilot-swe-agent[bot]
033da68f02 Use attribute-based approach for OPENING/CLOSING states in Z-Wave JS covers
Co-authored-by: edenhaus <26537646+edenhaus@users.noreply.github.com>
2026-02-16 13:59:59 +00:00
copilot-swe-agent[bot]
e547e84df7 Add OPENING/CLOSING states to Z-Wave JS multilevel switch covers
Co-authored-by: edenhaus <26537646+edenhaus@users.noreply.github.com>
2026-02-16 13:44:51 +00:00
copilot-swe-agent[bot]
a7d342fe9b Initial plan 2026-02-16 13:40:43 +00:00
2 changed files with 266 additions and 1 deletions

View File

@@ -138,6 +138,24 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
"""Return range between fully opened and fully closed position."""
return self._fully_open_position - self._fully_closed_position
@callback
def on_value_update(self) -> None:
"""Handle value updates for the cover.
Clear opening/closing state when movement completes.
"""
# Clear opening/closing state when target matches current
if (
self._current_position_value
and self._current_position_value.value is not None
and self._target_position_value
and self._target_position_value.value is not None
and self._current_position_value.value == self._target_position_value.value
):
self._attr_is_opening = False
self._attr_is_closing = False
self.async_write_ha_state()
@property
def is_closed(self) -> bool | None:
"""Return true if cover is closed."""
@@ -159,30 +177,58 @@ class CoverPositionMixin(ZWaveBaseEntity, CoverEntity):
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
assert self._target_position_value
assert self._current_position_value
target_position = self.percent_to_zwave_position(kwargs[ATTR_POSITION])
current_position = self._current_position_value.value
# Determine direction before issuing command
if current_position is not None and target_position > current_position:
self._attr_is_opening = True
self._attr_is_closing = False
elif current_position is not None and target_position < current_position:
self._attr_is_opening = False
self._attr_is_closing = True
else:
# Target equals current or position is None - clear opening/closing state
self._attr_is_opening = False
self._attr_is_closing = False
await self._async_set_value(
self._target_position_value,
self.percent_to_zwave_position(kwargs[ATTR_POSITION]),
target_position,
)
self.async_write_ha_state()
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
assert self._target_position_value
self._attr_is_opening = True
self._attr_is_closing = False
await self._async_set_value(
self._target_position_value, self._fully_open_position
)
self.async_write_ha_state()
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close cover."""
assert self._target_position_value
self._attr_is_opening = False
self._attr_is_closing = True
await self._async_set_value(
self._target_position_value, self._fully_closed_position
)
self.async_write_ha_state()
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop cover."""
assert self._stop_position_value
# Clear opening/closing state when stopped
self._attr_is_opening = False
self._attr_is_closing = False
# Stop the cover, will stop regardless of the actual direction of travel.
await self._async_set_value(self._stop_position_value, False)
self.async_write_ha_state()
class CoverTiltMixin(ZWaveBaseEntity, CoverEntity):
@@ -425,15 +471,24 @@ class ZWaveWindowCovering(CoverPositionMixin, CoverTiltMixin):
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
self._attr_is_opening = True
self._attr_is_closing = False
await self._async_set_value(self._up_value, True)
self.async_write_ha_state()
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the cover."""
self._attr_is_opening = False
self._attr_is_closing = True
await self._async_set_value(self._down_value, True)
self.async_write_ha_state()
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
self._attr_is_opening = False
self._attr_is_closing = False
await self._async_set_value(self._up_value, False)
self.async_write_ha_state()
class ZwaveMotorizedBarrier(ZWaveBaseEntity, CoverEntity):

View File

@@ -1202,3 +1202,213 @@ async def test_window_covering_open_close(
assert args["value"] is False
client.async_send_command.reset_mock()
async def test_multilevel_switch_cover_opening_closing_state(
hass: HomeAssistant, client, aeotec_nano_shutter, integration
) -> None:
"""Test multilevel switch cover OPENING and CLOSING states."""
node = aeotec_nano_shutter
state = hass.states.get(AEOTEC_SHUTTER_COVER_ENTITY)
assert state
assert state.state == CoverState.CLOSED
assert state.attributes[ATTR_CURRENT_POSITION] == 0
# Call open_cover - should immediately set state to OPENING
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_OPEN_COVER,
{ATTR_ENTITY_ID: AEOTEC_SHUTTER_COVER_ENTITY},
blocking=True,
)
state = hass.states.get(AEOTEC_SHUTTER_COVER_ENTITY)
assert state.state == CoverState.OPENING
# Simulate cover moving: update targetValue to 99
event = Event(
type="value updated",
data={
"source": "node",
"event": "value updated",
"nodeId": 3,
"args": {
"commandClassName": "Multilevel Switch",
"commandClass": 38,
"endpoint": 0,
"property": "targetValue",
"newValue": 99,
"prevValue": 0,
"propertyName": "targetValue",
},
},
)
node.receive_event(event)
# State should still be OPENING
state = hass.states.get(AEOTEC_SHUTTER_COVER_ENTITY)
assert state.state == CoverState.OPENING
# Simulate cover moving: currentValue is now 50, targetValue is still 99
event = Event(
type="value updated",
data={
"source": "node",
"event": "value updated",
"nodeId": 3,
"args": {
"commandClassName": "Multilevel Switch",
"commandClass": 38,
"endpoint": 0,
"property": "currentValue",
"newValue": 50,
"prevValue": 0,
"propertyName": "currentValue",
},
},
)
node.receive_event(event)
# State should still be OPENING while moving
state = hass.states.get(AEOTEC_SHUTTER_COVER_ENTITY)
assert state.state == CoverState.OPENING
assert state.attributes[ATTR_CURRENT_POSITION] == 51
# Simulate cover fully opened: currentValue reaches targetValue
event = Event(
type="value updated",
data={
"source": "node",
"event": "value updated",
"nodeId": 3,
"args": {
"commandClassName": "Multilevel Switch",
"commandClass": 38,
"endpoint": 0,
"property": "currentValue",
"newValue": 99,
"prevValue": 50,
"propertyName": "currentValue",
},
},
)
node.receive_event(event)
# State should now be OPEN (cleared opening state)
state = hass.states.get(AEOTEC_SHUTTER_COVER_ENTITY)
assert state.state == CoverState.OPEN
assert state.attributes[ATTR_CURRENT_POSITION] == 100
# Call close_cover - should immediately set state to CLOSING
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_CLOSE_COVER,
{ATTR_ENTITY_ID: AEOTEC_SHUTTER_COVER_ENTITY},
blocking=True,
)
state = hass.states.get(AEOTEC_SHUTTER_COVER_ENTITY)
assert state.state == CoverState.CLOSING
# Simulate target value update
event = Event(
type="value updated",
data={
"source": "node",
"event": "value updated",
"nodeId": 3,
"args": {
"commandClassName": "Multilevel Switch",
"commandClass": 38,
"endpoint": 0,
"property": "targetValue",
"newValue": 0,
"prevValue": 99,
"propertyName": "targetValue",
},
},
)
node.receive_event(event)
# State should still be CLOSING
state = hass.states.get(AEOTEC_SHUTTER_COVER_ENTITY)
assert state.state == CoverState.CLOSING
# Simulate cover moving: currentValue is now 50
event = Event(
type="value updated",
data={
"source": "node",
"event": "value updated",
"nodeId": 3,
"args": {
"commandClassName": "Multilevel Switch",
"commandClass": 38,
"endpoint": 0,
"property": "currentValue",
"newValue": 50,
"prevValue": 99,
"propertyName": "currentValue",
},
},
)
node.receive_event(event)
# State should still be CLOSING while moving
state = hass.states.get(AEOTEC_SHUTTER_COVER_ENTITY)
assert state.state == CoverState.CLOSING
assert state.attributes[ATTR_CURRENT_POSITION] == 51
# Simulate cover fully closed: currentValue reaches targetValue
event = Event(
type="value updated",
data={
"source": "node",
"event": "value updated",
"nodeId": 3,
"args": {
"commandClassName": "Multilevel Switch",
"commandClass": 38,
"endpoint": 0,
"property": "currentValue",
"newValue": 0,
"prevValue": 50,
"propertyName": "currentValue",
},
},
)
node.receive_event(event)
# State should now be CLOSED (cleared closing state)
state = hass.states.get(AEOTEC_SHUTTER_COVER_ENTITY)
assert state.state == CoverState.CLOSED
assert state.attributes[ATTR_CURRENT_POSITION] == 0
# Test stop cover clears the state
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_OPEN_COVER,
{ATTR_ENTITY_ID: AEOTEC_SHUTTER_COVER_ENTITY},
blocking=True,
)
state = hass.states.get(AEOTEC_SHUTTER_COVER_ENTITY)
assert state.state == CoverState.OPENING
# Stop the cover
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_STOP_COVER,
{ATTR_ENTITY_ID: AEOTEC_SHUTTER_COVER_ENTITY},
blocking=True,
)
# Stop command should immediately clear opening/closing state
state = hass.states.get(AEOTEC_SHUTTER_COVER_ENTITY)
assert state.state == CoverState.CLOSED # Current position is 0 (closed)
assert state.state != CoverState.OPENING
assert state.state != CoverState.CLOSING