Return None for Velux cover position when unknown (#168566)

This commit is contained in:
mnaggatz
2026-04-27 14:35:28 +01:00
committed by abmantis
parent 7203f61e7a
commit db91c0eaee
3 changed files with 115 additions and 22 deletions
+26 -13
View File
@@ -97,13 +97,17 @@ class VeluxCover(VeluxEntity, CoverEntity):
self._attr_device_class = CoverDeviceClass.SHUTTER
@property
def current_cover_position(self) -> int:
def current_cover_position(self) -> int | None:
"""Return the current position of the cover."""
if not self.node.position.known:
return None
return 100 - self.node.position.position_percent
@property
def is_closed(self) -> bool:
def is_closed(self) -> bool | None:
"""Return if the cover is closed."""
if not self.node.position.known:
return None
return self.node.position.closed
@property
@@ -168,22 +172,29 @@ class VeluxDualRollerShutter(VeluxCover):
self.part = part
@property
def current_cover_position(self) -> int:
"""Return the current position of the cover."""
def _part_position(self) -> Position:
"""Return the pyvlx Position for this part of the shutter."""
if self.part == VeluxDualRollerPart.UPPER:
return 100 - self.node.position_upper_curtain.position_percent
return self.node.position_upper_curtain
if self.part == VeluxDualRollerPart.LOWER:
return 100 - self.node.position_lower_curtain.position_percent
return 100 - self.node.position.position_percent
return self.node.position_lower_curtain
return self.node.position
@property
def is_closed(self) -> bool:
def current_cover_position(self) -> int | None:
"""Return the current position of the cover."""
position = self._part_position
if not position.known:
return None
return 100 - position.position_percent
@property
def is_closed(self) -> bool | None:
"""Return if the cover is closed."""
if self.part == VeluxDualRollerPart.UPPER:
return self.node.position_upper_curtain.closed
if self.part == VeluxDualRollerPart.LOWER:
return self.node.position_lower_curtain.closed
return self.node.position.closed
position = self._part_position
if not position.known:
return None
return position.closed
@wrap_pyvlx_call_exceptions
async def async_close_cover(self, **kwargs: Any) -> None:
@@ -227,6 +238,8 @@ class VeluxBlind(VeluxCover):
@property
def current_cover_tilt_position(self) -> int | None:
"""Return the current tilt position of the cover."""
if not self.node.orientation.known:
return None
return 100 - self.node.orientation.position_percent
@wrap_pyvlx_call_exceptions
+17 -9
View File
@@ -74,7 +74,7 @@ def mock_window() -> AsyncMock:
window.device_updated_cbs = []
window.is_opening = False
window.is_closing = False
window.position = MagicMock(position_percent=30, closed=False)
window.position = MagicMock(position_percent=30, closed=False, known=True)
window.wink = AsyncMock()
window.pyvlx = MagicMock()
return window
@@ -89,9 +89,13 @@ def mock_dual_roller_shutter() -> AsyncMock:
cover.serial_number = "987654321"
cover.is_opening = False
cover.is_closing = False
cover.position_upper_curtain = MagicMock(position_percent=30, closed=False)
cover.position_lower_curtain = MagicMock(position_percent=30, closed=False)
cover.position = MagicMock(position_percent=30, closed=False)
cover.position_upper_curtain = MagicMock(
position_percent=30, closed=False, known=True
)
cover.position_lower_curtain = MagicMock(
position_percent=30, closed=False, known=True
)
cover.position = MagicMock(position_percent=30, closed=False, known=True)
cover.pyvlx = MagicMock()
return cover
@@ -104,11 +108,11 @@ def mock_blind() -> AsyncMock:
blind.name = "Test Blind"
blind.serial_number = "4711"
# Standard cover position (used by current_cover_position)
blind.position = MagicMock(position_percent=40, closed=False)
blind.position = MagicMock(position_percent=40, closed=False, known=True)
blind.is_opening = False
blind.is_closing = False
# Orientation/tilt-related attributes and methods
blind.orientation = MagicMock(position_percent=25)
blind.orientation = MagicMock(position_percent=25, known=True)
blind.open_orientation = AsyncMock()
blind.close_orientation = AsyncMock()
blind.stop_orientation = AsyncMock()
@@ -175,9 +179,13 @@ def mock_cover_type(request: pytest.FixtureRequest) -> AsyncMock:
cover.serial_number = f"serial_{request.param.__name__}"
cover.is_opening = False
cover.is_closing = False
cover.position = MagicMock(position_percent=30, closed=False)
cover.position_upper_curtain = MagicMock(position_percent=30, closed=False)
cover.position_lower_curtain = MagicMock(position_percent=30, closed=False)
cover.position = MagicMock(position_percent=30, closed=False, known=True)
cover.position_upper_curtain = MagicMock(
position_percent=30, closed=False, known=True
)
cover.position_lower_curtain = MagicMock(
position_percent=30, closed=False, known=True
)
cover.pyvlx = MagicMock()
return cover
+72
View File
@@ -33,6 +33,7 @@ from homeassistant.const import (
STATE_CLOSING,
STATE_OPEN,
STATE_OPENING,
STATE_UNKNOWN,
Platform,
)
from homeassistant.core import HomeAssistant
@@ -475,6 +476,77 @@ async def test_non_blind_has_no_tilt_position(
assert "current_tilt_position" not in state.attributes
# Unknown position tests
async def test_window_unknown_position(
hass: HomeAssistant, mock_window: AsyncMock
) -> None:
"""When the device position is not known, state and position must be unknown."""
entity_id = "cover.test_window"
mock_window.position.known = False
await update_callback_entity(hass, mock_window)
state = hass.states.get(entity_id)
assert state is not None
assert state.state == STATE_UNKNOWN
assert state.attributes.get("current_position") is None
@pytest.mark.parametrize(
("unknown_attr", "unknown_entity_id"),
[
("position", "cover.test_dual_roller_shutter"),
("position_upper_curtain", "cover.test_dual_roller_shutter_upper_shutter"),
("position_lower_curtain", "cover.test_dual_roller_shutter_lower_shutter"),
],
)
async def test_dual_roller_shutter_unknown_position(
hass: HomeAssistant,
mock_dual_roller_shutter: AsyncMock,
unknown_attr: str,
unknown_entity_id: str,
) -> None:
"""Each part falls back to unknown independently when only its position is unknown."""
all_entity_ids = {
"cover.test_dual_roller_shutter",
"cover.test_dual_roller_shutter_upper_shutter",
"cover.test_dual_roller_shutter_lower_shutter",
}
getattr(mock_dual_roller_shutter, unknown_attr).known = False
await update_callback_entity(hass, mock_dual_roller_shutter)
state = hass.states.get(unknown_entity_id)
assert state is not None
assert state.state == STATE_UNKNOWN
assert state.attributes.get("current_position") is None
for entity_id in all_entity_ids - {unknown_entity_id}:
state = hass.states.get(entity_id)
assert state is not None
assert state.state != STATE_UNKNOWN
assert state.attributes.get("current_position") == 70
async def test_blind_unknown_tilt_position(
hass: HomeAssistant, mock_blind: AsyncMock
) -> None:
"""Tilt position must be None when the orientation is not known."""
entity_id = "cover.test_blind"
mock_blind.orientation.known = False
await update_callback_entity(hass, mock_blind)
state = hass.states.get(entity_id)
assert state is not None
assert state.attributes.get("current_tilt_position") is None
# Exception handling tests