Add support for topdown shades to hunterdouglas_powerview (#62788)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
kingy444 2022-05-31 03:54:39 +10:00 committed by GitHub
parent 3399be2dad
commit 45e4dd379b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 602 additions and 261 deletions

View file

@ -1,14 +1,24 @@
"""Support for hunter douglas shades."""
from abc import abstractmethod
from __future__ import annotations
import asyncio
from collections.abc import Iterable
from contextlib import suppress
import logging
from typing import Any
from aiopvapi.helpers.constants import ATTR_POSITION1, ATTR_POSITION_DATA
from aiopvapi.helpers.constants import (
ATTR_POSITION1,
ATTR_POSITION2,
ATTR_POSITION_DATA,
)
from aiopvapi.resources.shade import (
ATTR_POSKIND1,
ATTR_POSKIND2,
MAX_POSITION,
MIN_POSITION,
BaseShade,
ShadeTdbu,
Silhouette,
factory as PvShade,
)
@ -22,7 +32,7 @@ from homeassistant.components.cover import (
CoverEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_call_later
@ -32,15 +42,19 @@ from .const import (
DEVICE_MODEL,
DOMAIN,
LEGACY_DEVICE_MODEL,
POS_KIND_PRIMARY,
POS_KIND_SECONDARY,
POS_KIND_VANE,
PV_API,
PV_ROOM_DATA,
PV_SHADE_DATA,
ROOM_ID_IN_SHADE,
ROOM_NAME_UNICODE,
SHADE_RESPONSE,
STATE_ATTRIBUTE_ROOM_NAME,
)
from .coordinator import PowerviewShadeUpdateCoordinator
from .entity import ShadeEntity
from .shade_data import PowerviewShadeMove
_LOGGER = logging.getLogger(__name__)
@ -52,11 +66,13 @@ PARALLEL_UPDATES = 1
RESYNC_DELAY = 60
POSKIND_NONE = 0
POSKIND_PRIMARY = 1
POSKIND_SECONDARY = 2
POSKIND_VANE = 3
POSKIND_ERROR = 4
# this equates to 0.75/100 in terms of hass blind position
# some blinds in a closed position report less than 655.35 (1%)
# but larger than 0 even though they are clearly closed
# Find 1 percent of MAX_POSITION, then find 75% of that number
# The means currently 491.5125 or less is closed position
# implemented for top/down shades, but also works fine with normal shades
CLOSED_POSITION = (0.75 / 100) * (MAX_POSITION - MIN_POSITION)
async def async_setup_entry(
@ -65,18 +81,17 @@ async def async_setup_entry(
"""Set up the hunter douglas shades."""
pv_data = hass.data[DOMAIN][entry.entry_id]
room_data = pv_data[PV_ROOM_DATA]
room_data: dict[str | int, Any] = pv_data[PV_ROOM_DATA]
shade_data = pv_data[PV_SHADE_DATA]
pv_request = pv_data[PV_API]
coordinator = pv_data[COORDINATOR]
device_info = pv_data[DEVICE_INFO]
coordinator: PowerviewShadeUpdateCoordinator = pv_data[COORDINATOR]
device_info: dict[str, Any] = pv_data[DEVICE_INFO]
entities = []
entities: list[ShadeEntity] = []
for raw_shade in shade_data.values():
# The shade may be out of sync with the hub
# so we force a refresh when we add it if
# possible
shade = PvShade(raw_shade, pv_request)
# so we force a refresh when we add it if possible
shade: BaseShade = PvShade(raw_shade, pv_request)
name_before_refresh = shade.name
with suppress(asyncio.TimeoutError):
async with async_timeout.timeout(1):
@ -88,9 +103,10 @@ async def async_setup_entry(
name_before_refresh,
)
continue
coordinator.data.update_shade_positions(shade.raw_data)
room_id = shade.raw_data.get(ROOM_ID_IN_SHADE)
room_name = room_data.get(room_id, {}).get(ROOM_NAME_UNICODE, "")
entities.append(
entities.extend(
create_powerview_shade_entity(
coordinator, device_info, room_name, shade, name_before_refresh
)
@ -99,26 +115,36 @@ async def async_setup_entry(
def create_powerview_shade_entity(
coordinator, device_info, room_name, shade, name_before_refresh
):
coordinator: PowerviewShadeUpdateCoordinator,
device_info: dict[str, Any],
room_name: str,
shade: BaseShade,
name_before_refresh: str,
) -> Iterable[ShadeEntity]:
"""Create a PowerViewShade entity."""
if isinstance(shade, Silhouette):
return PowerViewShadeSilhouette(
coordinator, device_info, room_name, shade, name_before_refresh
)
return PowerViewShade(
coordinator, device_info, room_name, shade, name_before_refresh
)
classes: list[BaseShade] = []
# order here is important as both ShadeTDBU are listed in aiovapi as can_tilt
# and both require their own class here to work
if isinstance(shade, ShadeTdbu):
classes.extend([PowerViewShadeTDBUTop, PowerViewShadeTDBUBottom])
elif isinstance(shade, Silhouette):
classes.append(PowerViewShadeSilhouette)
elif shade.can_tilt:
classes.append(PowerViewShadeWithTilt)
else:
classes.append(PowerViewShade)
return [
cls(coordinator, device_info, room_name, shade, name_before_refresh)
for cls in classes
]
def hd_position_to_hass(hd_position, max_val):
def hd_position_to_hass(hd_position: int, max_val: int = MAX_POSITION) -> int:
"""Convert hunter douglas position to hass position."""
return round((hd_position / max_val) * 100)
def hass_position_to_hd(hass_position, max_val):
def hass_position_to_hd(hass_position: int, max_val: int = MAX_POSITION) -> int:
"""Convert hass position to hunter douglas position."""
return int(hass_position / 100 * max_val)
@ -128,130 +154,126 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
# The hub frequently reports stale states
_attr_assumed_state = True
_attr_device_class = CoverDeviceClass.SHADE
_attr_supported_features = 0
def __init__(self, coordinator, device_info, room_name, shade, name):
def __init__(
self,
coordinator: PowerviewShadeUpdateCoordinator,
device_info: dict[str, Any],
room_name: str,
shade: BaseShade,
name: str,
) -> None:
"""Initialize the shade."""
super().__init__(coordinator, device_info, room_name, shade, name)
self._shade = shade
self._is_opening = False
self._is_closing = False
self._last_action_timestamp = 0
self._scheduled_transition_update = None
self._current_hd_cover_position = MIN_POSITION
self._shade: BaseShade = shade
self._attr_name = self._shade_name
self._scheduled_transition_update: CALLBACK_TYPE | None = None
if self._device_info[DEVICE_MODEL] != LEGACY_DEVICE_MODEL:
self._attr_supported_features |= CoverEntityFeature.STOP
self._forced_resync = None
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, str]:
"""Return the state attributes."""
return {STATE_ATTRIBUTE_ROOM_NAME: self._room_name}
@property
def is_closed(self):
"""Return if the cover is closed."""
return self._current_hd_cover_position == MIN_POSITION
return self.positions.primary <= CLOSED_POSITION
@property
def is_opening(self):
"""Return if the cover is opening."""
return self._is_opening
@property
def is_closing(self):
"""Return if the cover is closing."""
return self._is_closing
@property
def current_cover_position(self):
def current_cover_position(self) -> int:
"""Return the current position of cover."""
return hd_position_to_hass(self._current_hd_cover_position, MAX_POSITION)
return hd_position_to_hass(self.positions.primary, MAX_POSITION)
@property
def device_class(self):
"""Return device class."""
return CoverDeviceClass.SHADE
def transition_steps(self) -> int:
"""Return the steps to make a move."""
return hd_position_to_hass(self.positions.primary, MAX_POSITION)
@property
def name(self):
"""Return the name of the shade."""
return self._shade_name
def open_position(self) -> PowerviewShadeMove:
"""Return the open position and required additional positions."""
return PowerviewShadeMove(self._shade.open_position, {})
async def async_close_cover(self, **kwargs):
@property
def close_position(self) -> PowerviewShadeMove:
"""Return the close position and required additional positions."""
return PowerviewShadeMove(self._shade.close_position, {})
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the cover."""
await self._async_move(0)
self._async_schedule_update_for_transition(self.transition_steps)
await self._async_execute_move(self.close_position)
self._attr_is_opening = False
self._attr_is_closing = True
self.async_write_ha_state()
async def async_open_cover(self, **kwargs):
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
await self._async_move(100)
self._async_schedule_update_for_transition(100 - self.transition_steps)
await self._async_execute_move(self.open_position)
self._attr_is_opening = True
self._attr_is_closing = False
self.async_write_ha_state()
async def async_stop_cover(self, **kwargs):
async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover."""
# Cancel any previous updates
self._async_cancel_scheduled_transition_update()
self._async_update_from_command(await self._shade.stop())
self.data.update_from_response(await self._shade.stop())
await self._async_force_refresh_state()
async def async_set_cover_position(self, **kwargs):
"""Move the shade to a specific position."""
if ATTR_POSITION not in kwargs:
return
await self._async_move(kwargs[ATTR_POSITION])
@callback
def _clamp_cover_limit(self, target_hass_position: int) -> int:
"""Dont allow a cover to go into an impossbile position."""
# no override required in base
return target_hass_position
async def _async_move(self, target_hass_position):
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the shade to a specific position."""
await self._async_set_cover_position(kwargs[ATTR_POSITION])
@callback
def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove:
position_one = hass_position_to_hd(target_hass_position)
return PowerviewShadeMove(
{ATTR_POSITION1: position_one, ATTR_POSKIND1: POS_KIND_PRIMARY}, {}
)
async def _async_execute_move(self, move: PowerviewShadeMove) -> None:
"""Execute a move that can affect multiple positions."""
response = await self._shade.move(move.request)
# Process any positions we know will update as result
# of the request since the hub won't return them
for kind, position in move.new_positions.items():
self.data.update_shade_position(self._shade.id, position, kind)
# Finally process the response
self.data.update_from_response(response)
async def _async_set_cover_position(self, target_hass_position: int) -> None:
"""Move the shade to a position."""
current_hass_position = hd_position_to_hass(
self._current_hd_cover_position, MAX_POSITION
target_hass_position = self._clamp_cover_limit(target_hass_position)
current_hass_position = self.current_cover_position
self._async_schedule_update_for_transition(
abs(current_hass_position - target_hass_position)
)
steps_to_move = abs(current_hass_position - target_hass_position)
self._async_schedule_update_for_transition(steps_to_move)
self._async_update_from_command(
await self._shade.move(
{
ATTR_POSITION1: hass_position_to_hd(
target_hass_position, MAX_POSITION
),
ATTR_POSKIND1: POSKIND_PRIMARY,
}
)
)
self._is_opening = False
self._is_closing = False
if target_hass_position > current_hass_position:
self._is_opening = True
elif target_hass_position < current_hass_position:
self._is_closing = True
await self._async_execute_move(self._get_shade_move(target_hass_position))
self._attr_is_opening = target_hass_position > current_hass_position
self._attr_is_closing = target_hass_position < current_hass_position
self.async_write_ha_state()
@callback
def _async_update_from_command(self, raw_data):
"""Update the shade state after a command."""
if not raw_data or SHADE_RESPONSE not in raw_data:
return
self._async_process_new_shade_data(raw_data[SHADE_RESPONSE])
@callback
def _async_process_new_shade_data(self, data):
"""Process new data from an update."""
self._shade.raw_data = data
self._async_update_current_cover_position()
@callback
def _async_update_current_cover_position(self):
def _async_update_shade_data(self, shade_data: dict[str | int, Any]) -> None:
"""Update the current cover position from the data."""
_LOGGER.debug("Raw data update: %s", self._shade.raw_data)
position_data = self._shade.raw_data.get(ATTR_POSITION_DATA, {})
self._async_process_updated_position_data(position_data)
self._is_opening = False
self._is_closing = False
self.data.update_shade_positions(shade_data)
self._attr_is_opening = False
self._attr_is_closing = False
@callback
@abstractmethod
def _async_process_updated_position_data(self, position_data):
"""Process position data."""
@callback
def _async_cancel_scheduled_transition_update(self):
def _async_cancel_scheduled_transition_update(self) -> None:
"""Cancel any previous updates."""
if self._scheduled_transition_update:
self._scheduled_transition_update()
@ -261,9 +283,7 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
self._forced_resync = None
@callback
def _async_schedule_update_for_transition(self, steps):
self.async_write_ha_state()
def _async_schedule_update_for_transition(self, steps: int) -> None:
# Cancel any previous updates
self._async_cancel_scheduled_transition_update()
@ -278,7 +298,7 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
est_time_to_complete_transition,
)
# Schedule an update for when we expect the transition
# Schedule an forced update for when we expect the transition
# to be completed.
self._scheduled_transition_update = async_call_later(
self.hass,
@ -295,139 +315,281 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity):
self.hass, RESYNC_DELAY, self._async_force_resync
)
async def _async_force_resync(self, *_):
async def _async_force_resync(self, *_: Any) -> None:
"""Force a resync after an update since the hub may have stale state."""
self._forced_resync = None
_LOGGER.debug("Force resync of shade %s", self.name)
await self._async_force_refresh_state()
async def _async_force_refresh_state(self):
async def _async_force_refresh_state(self) -> None:
"""Refresh the cover state and force the device cache to be bypassed."""
await self._shade.refresh()
self._async_update_current_cover_position()
self._async_update_shade_data(self._shade.raw_data)
self.async_write_ha_state()
async def async_added_to_hass(self):
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
self._async_update_current_cover_position()
self.async_on_remove(
self.coordinator.async_add_listener(self._async_update_shade_from_group)
)
async def async_will_remove_from_hass(self):
async def async_will_remove_from_hass(self) -> None:
"""Cancel any pending refreshes."""
self._async_cancel_scheduled_transition_update()
@callback
def _async_update_shade_from_group(self):
def _async_update_shade_from_group(self) -> None:
"""Update with new data from the coordinator."""
if self._scheduled_transition_update or self._forced_resync:
# If a transition in in progress
# the data will be wrong
# If a transition is in progress the data will be wrong
return
self._async_process_new_shade_data(self.coordinator.data[self._shade.id])
self.data.update_from_group_data(self._shade.id)
self.async_write_ha_state()
class PowerViewShade(PowerViewShadeBase):
"""Represent a standard shade."""
_attr_supported_features = (
CoverEntityFeature.OPEN
| CoverEntityFeature.CLOSE
| CoverEntityFeature.SET_POSITION
)
def __init__(
self,
coordinator: PowerviewShadeUpdateCoordinator,
device_info: dict[str, Any],
room_name: str,
shade: BaseShade,
name: str,
) -> None:
"""Initialize the shade."""
super().__init__(coordinator, device_info, room_name, shade, name)
self._attr_supported_features |= (
CoverEntityFeature.OPEN
| CoverEntityFeature.CLOSE
| CoverEntityFeature.SET_POSITION
)
class PowerViewShadeTDBU(PowerViewShade):
"""Representation of a PowerView shade with top/down bottom/up capabilities."""
@property
def transition_steps(self) -> int:
"""Return the steps to make a move."""
return hd_position_to_hass(
self.positions.primary, MAX_POSITION
) + hd_position_to_hass(self.positions.secondary, MAX_POSITION)
class PowerViewShadeTDBUBottom(PowerViewShadeTDBU):
"""Representation of a top down bottom up powerview shade."""
def __init__(
self,
coordinator: PowerviewShadeUpdateCoordinator,
device_info: dict[str, Any],
room_name: str,
shade: BaseShade,
name: str,
) -> None:
"""Initialize the shade."""
super().__init__(coordinator, device_info, room_name, shade, name)
self._attr_unique_id = f"{self._shade.id}_bottom"
self._attr_name = f"{self._shade_name} Bottom"
@callback
def _async_process_updated_position_data(self, position_data):
"""Process position data."""
if ATTR_POSITION1 in position_data:
self._current_hd_cover_position = int(position_data[ATTR_POSITION1])
def _clamp_cover_limit(self, target_hass_position: int) -> int:
"""Dont allow a cover to go into an impossbile position."""
cover_top = hd_position_to_hass(self.positions.secondary, MAX_POSITION)
return min(target_hass_position, (100 - cover_top))
@callback
def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove:
position_bottom = hass_position_to_hd(target_hass_position)
position_top = self.positions.secondary
return PowerviewShadeMove(
{
ATTR_POSITION1: position_bottom,
ATTR_POSITION2: position_top,
ATTR_POSKIND1: POS_KIND_PRIMARY,
ATTR_POSKIND2: POS_KIND_SECONDARY,
},
{},
)
class PowerViewShadeTDBUTop(PowerViewShadeTDBU):
"""Representation of a top down bottom up powerview shade."""
def __init__(
self,
coordinator: PowerviewShadeUpdateCoordinator,
device_info: dict[str, Any],
room_name: str,
shade: BaseShade,
name: str,
) -> None:
"""Initialize the shade."""
super().__init__(coordinator, device_info, room_name, shade, name)
self._attr_unique_id = f"{self._shade.id}_top"
self._attr_name = f"{self._shade_name} Top"
# these shades share a class in parent API
# override open position for top shade
self._shade.open_position = {
ATTR_POSITION1: MIN_POSITION,
ATTR_POSITION2: MAX_POSITION,
ATTR_POSKIND1: POS_KIND_PRIMARY,
ATTR_POSKIND2: POS_KIND_SECONDARY,
}
@property
def is_closed(self):
"""Return if the cover is closed."""
# top shade needs to check other motor
return self.positions.secondary <= CLOSED_POSITION
@property
def current_cover_position(self) -> int:
"""Return the current position of cover."""
# these need to be inverted to report state correctly in HA
return hd_position_to_hass(self.positions.secondary, MAX_POSITION)
@callback
def _clamp_cover_limit(self, target_hass_position: int) -> int:
"""Dont allow a cover to go into an impossbile position."""
cover_bottom = hd_position_to_hass(self.positions.primary, MAX_POSITION)
return min(target_hass_position, (100 - cover_bottom))
@callback
def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove:
position_bottom = self.positions.primary
position_top = hass_position_to_hd(target_hass_position, MAX_POSITION)
return PowerviewShadeMove(
{
ATTR_POSITION1: position_bottom,
ATTR_POSITION2: position_top,
ATTR_POSKIND1: POS_KIND_PRIMARY,
ATTR_POSKIND2: POS_KIND_SECONDARY,
},
{},
)
class PowerViewShadeWithTilt(PowerViewShade):
"""Representation of a PowerView shade with tilt capabilities."""
_attr_supported_features = (
CoverEntityFeature.OPEN
| CoverEntityFeature.CLOSE
| CoverEntityFeature.SET_POSITION
| CoverEntityFeature.OPEN_TILT
| CoverEntityFeature.CLOSE_TILT
| CoverEntityFeature.STOP_TILT
| CoverEntityFeature.SET_TILT_POSITION
)
_max_tilt = MAX_POSITION
_tilt_steps = 10
def __init__(self, coordinator, device_info, room_name, shade, name):
def __init__(
self,
coordinator: PowerviewShadeUpdateCoordinator,
device_info: dict[str, Any],
room_name: str,
shade: BaseShade,
name: str,
) -> None:
"""Initialize the shade."""
super().__init__(coordinator, device_info, room_name, shade, name)
self._attr_current_cover_tilt_position = 0
async def async_open_cover_tilt(self, **kwargs):
"""Open the cover tilt."""
current_hass_position = hd_position_to_hass(
self._current_hd_cover_position, MAX_POSITION
self._attr_supported_features |= (
CoverEntityFeature.OPEN_TILT
| CoverEntityFeature.CLOSE_TILT
| CoverEntityFeature.SET_TILT_POSITION
)
steps_to_move = current_hass_position + self._tilt_steps
self._async_schedule_update_for_transition(steps_to_move)
self._async_update_from_command(await self._shade.tilt_open())
if self._device_info[DEVICE_MODEL] != LEGACY_DEVICE_MODEL:
self._attr_supported_features |= CoverEntityFeature.STOP_TILT
async def async_close_cover_tilt(self, **kwargs):
@property
def current_cover_tilt_position(self) -> int:
"""Return the current cover tile position."""
return hd_position_to_hass(self.positions.vane, self._max_tilt)
@property
def transition_steps(self):
"""Return the steps to make a move."""
return hd_position_to_hass(
self.positions.primary, MAX_POSITION
) + hd_position_to_hass(self.positions.vane, self._max_tilt)
@property
def open_position(self) -> PowerviewShadeMove:
"""Return the open position and required additional positions."""
return PowerviewShadeMove(
self._shade.open_position, {POS_KIND_VANE: MIN_POSITION}
)
@property
def close_position(self) -> PowerviewShadeMove:
"""Return the close position and required additional positions."""
return PowerviewShadeMove(
self._shade.close_position, {POS_KIND_VANE: MIN_POSITION}
)
@property
def open_tilt_position(self) -> PowerviewShadeMove:
"""Return the open tilt position and required additional positions."""
# next upstream api release to include self._shade.open_tilt_position
return PowerviewShadeMove(
{ATTR_POSKIND1: POS_KIND_VANE, ATTR_POSITION1: self._max_tilt},
{POS_KIND_PRIMARY: MIN_POSITION},
)
@property
def close_tilt_position(self) -> PowerviewShadeMove:
"""Return the close tilt position and required additional positions."""
# next upstream api release to include self._shade.close_tilt_position
return PowerviewShadeMove(
{ATTR_POSKIND1: POS_KIND_VANE, ATTR_POSITION1: MIN_POSITION},
{POS_KIND_PRIMARY: MIN_POSITION},
)
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
"""Close the cover tilt."""
current_hass_position = hd_position_to_hass(
self._current_hd_cover_position, MAX_POSITION
)
steps_to_move = current_hass_position + self._tilt_steps
self._async_schedule_update_for_transition(steps_to_move)
self._async_update_from_command(await self._shade.tilt_close())
self._async_schedule_update_for_transition(self.transition_steps)
await self._async_execute_move(self.close_tilt_position)
self.async_write_ha_state()
async def async_set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position."""
target_hass_tilt_position = kwargs[ATTR_TILT_POSITION]
current_hass_position = hd_position_to_hass(
self._current_hd_cover_position, MAX_POSITION
)
steps_to_move = current_hass_position + self._tilt_steps
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
"""Open the cover tilt."""
self._async_schedule_update_for_transition(100 - self.transition_steps)
await self._async_execute_move(self.open_tilt_position)
self.async_write_ha_state()
self._async_schedule_update_for_transition(steps_to_move)
self._async_update_from_command(
await self._shade.move(
{
ATTR_POSITION1: hass_position_to_hd(
target_hass_tilt_position, self._max_tilt
),
ATTR_POSKIND1: POSKIND_VANE,
}
)
)
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the vane to a specific position."""
await self._async_set_cover_tilt_position(kwargs[ATTR_TILT_POSITION])
async def async_stop_cover_tilt(self, **kwargs):
"""Stop the cover tilting."""
# Cancel any previous updates
await self.async_stop_cover()
async def _async_set_cover_tilt_position(
self, target_hass_tilt_position: int
) -> None:
"""Move the vane to a specific position."""
final_position = self.current_cover_position + target_hass_tilt_position
self._async_schedule_update_for_transition(
abs(self.transition_steps - final_position)
)
await self._async_execute_move(self._get_shade_tilt(target_hass_tilt_position))
self.async_write_ha_state()
@callback
def _async_process_updated_position_data(self, position_data):
"""Process position data."""
if ATTR_POSKIND1 not in position_data:
return
if int(position_data[ATTR_POSKIND1]) == POSKIND_PRIMARY:
self._current_hd_cover_position = int(position_data[ATTR_POSITION1])
self._attr_current_cover_tilt_position = 0
if int(position_data[ATTR_POSKIND1]) == POSKIND_VANE:
self._current_hd_cover_position = MIN_POSITION
self._attr_current_cover_tilt_position = hd_position_to_hass(
int(position_data[ATTR_POSITION1]), self._max_tilt
)
def _get_shade_move(self, target_hass_position: int) -> PowerviewShadeMove:
"""Return a PowerviewShadeMove."""
position_shade = hass_position_to_hd(target_hass_position)
return PowerviewShadeMove(
{ATTR_POSITION1: position_shade, ATTR_POSKIND1: POS_KIND_PRIMARY},
{POS_KIND_VANE: MIN_POSITION},
)
@callback
def _get_shade_tilt(self, target_hass_tilt_position: int) -> PowerviewShadeMove:
"""Return a PowerviewShadeMove."""
position_vane = hass_position_to_hd(target_hass_tilt_position, self._max_tilt)
return PowerviewShadeMove(
{ATTR_POSITION1: position_vane, ATTR_POSKIND1: POS_KIND_VANE},
{POS_KIND_PRIMARY: MIN_POSITION},
)
async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
"""Stop the cover tilting."""
await self.async_stop_cover()
class PowerViewShadeSilhouette(PowerViewShadeWithTilt):
"""Representation of a Silhouette PowerView shade."""
def __init__(self, coordinator, device_info, room_name, shade, name):
"""Initialize the shade."""
super().__init__(coordinator, device_info, room_name, shade, name)
self._max_tilt = 32767
self._tilt_steps = 4
_max_tilt = 32767