From deee1baad520b07b96caf1daa2e3e152808301d7 Mon Sep 17 00:00:00 2001 From: Richard Mann Date: Sat, 11 Apr 2026 17:57:17 +1000 Subject: [PATCH] feat: add tilt-on-closed cover entity for types 18 and 44 --- .../hunterdouglas_powerview_ble/api.py | 5 +-- .../hunterdouglas_powerview_ble/cover.py | 31 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/custom_components/hunterdouglas_powerview_ble/api.py b/custom_components/hunterdouglas_powerview_ble/api.py index a0cc553..e11f755 100644 --- a/custom_components/hunterdouglas_powerview_ble/api.py +++ b/custom_components/hunterdouglas_powerview_ble/api.py @@ -79,6 +79,7 @@ class ShadeCapability(NamedTuple): has_tilt: bool = False tilt_only: bool = False + is_tilt_on_closed: bool = False # tilt only available when fully closed is_top_down: bool = False # position logic is inverted (SkyLift style) is_tdbu: bool = False # dual-rail Top Down Bottom Up (needs two entities) is_duolite: bool = False # dual-fabric sheer+opaque (needs three entities) @@ -95,8 +96,8 @@ SHADE_CAPABILITIES: Final[dict[int, ShadeCapability]] = { 39: ShadeCapability(has_tilt=True, tilt_only=True), 40: ShadeCapability(has_tilt=True, tilt_only=True), # tilt on closed (tilt only available at fully closed position) - 18: ShadeCapability(has_tilt=True), - 44: ShadeCapability(has_tilt=True), + 18: ShadeCapability(has_tilt=True, is_tilt_on_closed=True), + 44: ShadeCapability(has_tilt=True, is_tilt_on_closed=True), # top-down only (single rail, inverted position) 7: ShadeCapability(is_top_down=True), 10: ShadeCapability(is_top_down=True), diff --git a/custom_components/hunterdouglas_powerview_ble/cover.py b/custom_components/hunterdouglas_powerview_ble/cover.py index a39d968..a37e40e 100644 --- a/custom_components/hunterdouglas_powerview_ble/cover.py +++ b/custom_components/hunterdouglas_powerview_ble/cover.py @@ -34,6 +34,8 @@ def _add_entities( if caps.tilt_only: entities: list[PowerViewCover] = [PowerViewCoverTiltOnly(coordinator)] + elif caps.is_tilt_on_closed: + entities = [PowerViewCoverTiltOnClosed(coordinator)] elif caps.has_tilt: entities = [PowerViewCoverTilt(coordinator)] elif caps.is_top_down: @@ -262,6 +264,35 @@ class PowerViewCoverTilt(PowerViewCover): await self.async_set_cover_tilt_position(**_kwargs) +class PowerViewCoverTiltOnClosed(PowerViewCoverTilt): + """Representation of a PowerView shade whose tilt is only available when closed. + + Examples: Bottom Up 90° (type 18), Twist (type 44). + + If a tilt command arrives while the shade is open, the shade is closed first + so the tilt mechanism is engaged before the command is sent. + """ + + def __init__(self, coordinator: PVCoordinator) -> None: + """Initialize the shade.""" + LOGGER.debug("%s: init() PowerViewCoverTiltOnClosed", coordinator.name) + super().__init__(coordinator) + + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: + """Move the tilt to a specific position, closing first if needed.""" + if self.current_cover_position != CLOSED_POSITION: + LOGGER.debug("tilt-on-closed: closing shade before tilting") + try: + self._target_position = CLOSED_POSITION + await self._coord.api.close(velocity=self._coord.velocity) + self.async_write_ha_state() + except BleakError as err: + LOGGER.error("Failed to close cover '%s' before tilt: %s", self.name, err) + self._reset_target_position() + return + await super().async_set_cover_tilt_position(**kwargs) + + class PowerViewCoverTopDown(PowerViewCover): """Representation of a top-down PowerView shade.