Report an uncalibrated Shelly roller by its last direction (#180485)

This commit is contained in:
Franck Nijhof
2026-08-30 21:13:50 +03:00
committed by GitHub
parent 8202748849
commit 4016b01b89
2 changed files with 111 additions and 5 deletions
+26 -3
View File
@@ -120,13 +120,33 @@ class BlockShellyCover(ShellyBlockAttributeEntity, CoverEntity):
self.control_result: dict[str, Any] | None = None
self._attr_name = None # Main device entity
self._attr_unique_id: str = f"{coordinator.mac}-{block.description}"
if self.coordinator.device.settings["rollers"][0]["positioning"]:
self._positioning: bool = self.coordinator.device.settings["rollers"][0][
"positioning"
]
# Without positioning the direction it last travelled in is all there is,
# and that says nothing about where it stopped
self._attr_assumed_state = not self._positioning
if self._positioning:
self._attr_supported_features |= CoverEntityFeature.SET_POSITION
@property
@override
def is_closed(self) -> bool:
def is_closed(self) -> bool | None:
"""If cover is closed."""
if not self._positioning:
# An uncalibrated roller parks its position on 101, so the direction
# it last travelled in is all there is to go on
last_direction = self.coordinator.device.status["rollers"][0].get(
"last_direction"
)
if self.control_result:
last_direction = self.control_result.get(
"last_direction", last_direction
)
if not last_direction:
return None
return cast(str, last_direction) == "close"
if self.control_result:
return cast(bool, self.control_result["current_pos"] == 0)
@@ -134,8 +154,11 @@ class BlockShellyCover(ShellyBlockAttributeEntity, CoverEntity):
@property
@override
def current_cover_position(self) -> int:
def current_cover_position(self) -> int | None:
"""Position of the cover."""
if not self._positioning:
return None
if self.control_result:
return cast(int, self.control_result["current_pos"])
+85 -2
View File
@@ -1,7 +1,7 @@
"""Tests for Shelly cover platform."""
from copy import deepcopy
from unittest.mock import Mock
from unittest.mock import AsyncMock, Mock
from freezegun.api import FrozenDateTimeFactory
import pytest
@@ -23,7 +23,13 @@ from homeassistant.components.cover import (
CoverState,
)
from homeassistant.components.shelly.const import RPC_COVER_UPDATE_TIME_SEC
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
from homeassistant.const import (
ATTR_ASSUMED_STATE,
ATTR_ENTITY_ID,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_registry import EntityRegistry
@@ -111,6 +117,83 @@ async def test_block_device_update(
state = hass.states.get("cover.test_name")
assert state
assert state.state == CoverState.OPEN
assert ATTR_ASSUMED_STATE not in state.attributes
@pytest.mark.parametrize(
("last_direction", "expected_state"),
[
("close", CoverState.CLOSED),
("open", CoverState.OPEN),
# Nothing has moved since the device booted
(None, STATE_UNKNOWN),
],
)
async def test_block_device_roller_without_positioning(
hass: HomeAssistant,
mock_block_device: Mock,
monkeypatch: pytest.MonkeyPatch,
last_direction: str | None,
expected_state: str,
) -> None:
"""Test an uncalibrated roller reports the direction it last travelled in."""
settings = deepcopy(mock_block_device.settings)
settings["rollers"][0]["positioning"] = False
monkeypatch.setattr(mock_block_device, "settings", settings)
status = deepcopy(mock_block_device.status)
# An uncalibrated roller parks its position on 101
status["rollers"] = [{"current_pos": 101, "last_direction": last_direction}]
monkeypatch.setattr(mock_block_device, "status", status)
await init_integration(hass, 1)
assert (state := hass.states.get("cover.test_name"))
assert state.state == expected_state
assert state.attributes.get(ATTR_CURRENT_POSITION) is None
# Stopping mid travel leaves the direction saying more than it knows, so
# both buttons stay available
assert state.attributes[ATTR_ASSUMED_STATE] is True
async def test_block_device_roller_without_positioning_stopped(
hass: HomeAssistant,
mock_block_device: Mock,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test stopping an uncalibrated roller keeps it on its last direction."""
settings = deepcopy(mock_block_device.settings)
settings["rollers"][0]["positioning"] = False
monkeypatch.setattr(mock_block_device, "settings", settings)
status = deepcopy(mock_block_device.status)
status["rollers"] = [{"current_pos": 101, "last_direction": "close"}]
monkeypatch.setattr(mock_block_device, "status", status)
# An uncalibrated roller answers a command with position 101 as well
monkeypatch.setattr(
mock_block_device.blocks[ROLLER_BLOCK_ID],
"set_state",
AsyncMock(
side_effect=lambda go, roller_pos=0: {"current_pos": 101, "state": go}
),
)
await init_integration(hass, 1)
entity_id = "cover.test_name"
assert (state := hass.states.get(entity_id))
assert state.state == CoverState.CLOSED
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_STOP_COVER,
{ATTR_ENTITY_ID: entity_id},
blocking=True,
)
assert (state := hass.states.get(entity_id))
assert state.state == CoverState.CLOSED
async def test_block_device_no_roller_blocks(