1 """Support for hunter douglas shades."""
3 from __future__
import annotations
5 from collections.abc
import Callable, Iterable
6 from dataclasses
import replace
7 from datetime
import datetime, timedelta
10 from typing
import Any
12 from aiopvapi.helpers.constants
import (
19 from aiopvapi.resources.shade
import BaseShade, ShadePosition
32 from .const
import STATE_ATTRIBUTE_ROOM_NAME
33 from .coordinator
import PowerviewShadeUpdateCoordinator
34 from .entity
import ShadeEntity
35 from .model
import PowerviewConfigEntry, PowerviewDeviceInfo
37 _LOGGER = logging.getLogger(__name__)
41 TRANSITION_COMPLETE_DURATION = 40
52 entry: PowerviewConfigEntry,
53 async_add_entities: AddEntitiesCallback,
55 """Set up the hunter douglas shades."""
56 pv_entry = entry.runtime_data
57 coordinator = pv_entry.coordinator
59 async
def _async_initial_refresh() -> None:
60 """Force position refresh shortly after adding.
62 Legacy shades can become out of sync with hub when moved
63 using physical remotes. This also allows reducing speed
64 of calls to older generation hubs in an effort to
68 for shade
in pv_entry.shade_data.values():
69 _LOGGER.debug(
"Initial refresh of shade: %s", shade.name)
70 async
with coordinator.radio_operation_lock:
71 await shade.refresh(suppress_timeout=
True)
73 entities: list[ShadeEntity] = []
74 for shade
in pv_entry.shade_data.values():
75 room_name = getattr(pv_entry.room_data.get(shade.room_id), ATTR_NAME,
"")
78 coordinator, pv_entry.device_info, room_name, shade, shade.name
85 entry.async_create_background_task(
87 _async_initial_refresh(),
88 f
"powerview {entry.title} initial shade refresh",
93 """Representation of a powerview shade."""
95 _attr_device_class = CoverDeviceClass.SHADE
96 _attr_supported_features = (
97 CoverEntityFeature.OPEN
98 | CoverEntityFeature.CLOSE
99 | CoverEntityFeature.SET_POSITION
104 coordinator: PowerviewShadeUpdateCoordinator,
105 device_info: PowerviewDeviceInfo,
110 """Initialize the shade."""
111 super().
__init__(coordinator, device_info, room_name, shade, name)
112 self.
_shade_shade: BaseShade = shade
116 self.
_forced_resync_forced_resync: Callable[[],
None] |
None =
None
120 """If the device is hard wired we are polling state.
122 The hub will frequently provide the wrong state
123 for battery power devices so we set assumed
130 """Only poll if the device is hard wired.
132 We cannot poll battery powered devices
133 as it would drain their batteries in a matter
140 """Return the state attributes."""
141 return {STATE_ATTRIBUTE_ROOM_NAME: self.
_room_name_room_name}
145 """Return if the cover is closed."""
146 return self.
positionspositions.primary <= CLOSED_POSITION
150 """Return the current position of cover."""
155 """Return the steps to make a move."""
160 """Return the open position and required additional positions."""
161 return replace(self.
_shade_shade.open_position, velocity=self.
positionspositions.velocity)
165 """Return the close position and required additional positions."""
166 return replace(self.
_shade_shade.close_position, velocity=self.
positionspositions.velocity)
169 """Close the cover."""
177 """Open the cover."""
185 """Stop the cover."""
187 await self.
_shade_shade.stop()
192 """Don't allow a cover to go into an impossbile position."""
194 return target_hass_position
197 """Move the shade to a specific position."""
202 """Return a ShadePosition."""
203 return ShadePosition(
204 primary=target_hass_position,
205 velocity=self.
positionspositions.velocity,
209 """Execute a move that can affect multiple positions."""
210 _LOGGER.debug(
"Move request %s: %s", self.
namenamename, move)
211 async
with self.coordinator.radio_operation_lock:
212 response = await self.
_shade_shade.move(move)
213 _LOGGER.debug(
"Move response %s: %s", self.
namenamename, response)
219 """Move the shade to a position."""
220 target_hass_position = self.
_clamp_cover_limit_clamp_cover_limit(target_hass_position)
223 abs(current_hass_position - target_hass_position)
226 self.
_attr_is_opening_attr_is_opening = target_hass_position > current_hass_position
227 self.
_attr_is_closing_attr_is_closing = target_hass_position < current_hass_position
232 """Update the current cover position from the data."""
239 """Cancel any previous updates."""
252 est_time_to_complete_transition = 1 +
int(
253 TRANSITION_COMPLETE_DURATION * (steps / 100)
257 "Estimated time to complete transition of %s steps for %s: %s",
260 est_time_to_complete_transition,
267 est_time_to_complete_transition,
272 """Update status of the cover."""
273 _LOGGER.debug(
"Processing scheduled update for %s", self.
namenamename)
281 """Force a resync after an update since the hub may have stale state."""
283 _LOGGER.debug(
"Force resync of shade %s", self.
namenamename)
287 """Refresh the cover state and force the device cache to be bypassed."""
293 """When entity is added to hass."""
299 """Cancel any pending refreshes."""
304 """Check if an update is already in progress."""
309 """Update with new data from the coordinator."""
317 """Refresh shade position."""
323 async
with self.coordinator.radio_operation_lock:
324 await self.
_shade_shade.refresh(
325 suppress_timeout=
True
327 _LOGGER.debug(
"Process update %s: %s", self.
namenamename, self.
_shade_shade.current_position)
332 """Represent a standard shade."""
338 """Representation for PowerView shades with tilt capabilities."""
344 coordinator: PowerviewShadeUpdateCoordinator,
345 device_info: PowerviewDeviceInfo,
350 """Initialize the shade."""
351 super().
__init__(coordinator, device_info, room_name, shade, name)
353 CoverEntityFeature.OPEN_TILT
354 | CoverEntityFeature.CLOSE_TILT
355 | CoverEntityFeature.SET_TILT_POSITION
363 """Return the current cover tile position."""
368 """Return the steps to make a move."""
373 """Return the open tilt position and required additional positions."""
374 return replace(self.
_shade_shade.open_position_tilt, velocity=self.
positionspositions.velocity)
378 """Return the close tilt position and required additional positions."""
380 self.
_shade_shade.close_position_tilt, velocity=self.
positionspositions.velocity
384 """Close the cover tilt."""
390 """Open the cover tilt."""
396 """Move the tilt to a specific position."""
400 self, target_hass_tilt_position: int
402 """Move the tilt to a specific position."""
412 """Return a ShadePosition."""
413 return ShadePosition(
414 primary=target_hass_position,
415 velocity=self.
positionspositions.velocity,
420 """Return a ShadePosition."""
421 return ShadePosition(
422 tilt=target_hass_tilt_position,
423 velocity=self.
positionspositions.velocity,
427 """Stop the cover tilting."""
432 """Representation of a PowerView shade with tilt when closed capabilities.
434 API Class: ShadeBottomUpTiltOnClosed + ShadeBottomUpTiltOnClosed90
436 Type 1 - Bottom Up w/ 90° Tilt
437 Shade 44 - a shade thought to have been a firmware issue (type 0 usually don't tilt)
444 """Return the open position and required additional positions."""
445 return replace(self.
_shade_shade.open_position, velocity=self.
positionspositions.velocity)
449 """Return the close position and required additional positions."""
450 return replace(self.
_shade_shade.close_position, velocity=self.
positionspositions.velocity)
454 """Return the open tilt position and required additional positions."""
455 return replace(self.
_shade_shade.open_position_tilt, velocity=self.
positionspositions.velocity)
459 """Return the close tilt position and required additional positions."""
461 self.
_shade_shade.close_position_tilt, velocity=self.
positionspositions.velocity
466 """Representation of a PowerView shade with tilt anywhere capabilities.
468 API Class: ShadeBottomUpTiltAnywhere, ShadeVerticalTiltAnywhere
470 Type 2 - Bottom Up w/ 180° Tilt
471 Type 4 - Vertical (Traversing) w/ 180° Tilt
476 """Return a ShadePosition."""
477 return ShadePosition(
478 primary=target_hass_position,
480 velocity=self.
positionspositions.velocity,
485 """Return a ShadePosition."""
486 return ShadePosition(
488 tilt=target_hass_tilt_position,
489 velocity=self.
positionspositions.velocity,
494 """Representation of a shade with tilt only capability, no move.
496 API Class: ShadeTiltOnly
498 Type 5 - Tilt Only 180°
503 coordinator: PowerviewShadeUpdateCoordinator,
504 device_info: PowerviewDeviceInfo,
509 """Initialize the shade."""
510 super().
__init__(coordinator, device_info, room_name, shade, name)
512 CoverEntityFeature.OPEN_TILT
513 | CoverEntityFeature.CLOSE_TILT
514 | CoverEntityFeature.SET_TILT_POSITION
522 """Return the current position of cover."""
524 return CLOSED_POSITION
528 """Return the steps to make a move."""
533 """Return if the cover is closed."""
534 return self.
positionspositions.tilt <= CLOSED_POSITION
538 """Representation of a shade that lowers from the roof to the floor.
540 These shades are inverted where MAX_POSITION equates to closed and MIN_POSITION is open
541 API Class: ShadeTopDown
550 """Return the current position of cover."""
552 return MAX_POSITION - self.
positionspositions.primary
555 """Move the shade to a specific position."""
560 """Return if the cover is closed."""
561 return (MAX_POSITION - self.
positionspositions.primary) <= CLOSED_POSITION
565 """Representation of a shade with top/down bottom/up capabilities.
567 Base methods shared between the two shades created
568 Child Classes: PowerViewShadeTDBUBottom / PowerViewShadeTDBUTop
569 API Class: ShadeTopDownBottomUp
574 """Return the steps to make a move."""
579 """Representation of the bottom PowerViewShadeDualRailBase shade.
581 These shades have top/down bottom up functionality and two entities.
582 Sibling Class: PowerViewShadeTDBUTop
583 API Class: ShadeTopDownBottomUp
586 _attr_translation_key =
"bottom"
590 coordinator: PowerviewShadeUpdateCoordinator,
591 device_info: PowerviewDeviceInfo,
596 """Initialize the shade."""
597 super().
__init__(coordinator, device_info, room_name, shade, name)
602 """Don't allow a cover to go into an impossbile position."""
603 return min(target_hass_position, (MAX_POSITION - self.
positionspositions.secondary))
607 """Return a ShadePosition."""
608 return ShadePosition(
609 primary=target_hass_position,
610 secondary=self.
positionspositions.secondary,
611 velocity=self.
positionspositions.velocity,
616 """Representation of the top PowerViewShadeDualRailBase shade.
618 These shades have top/down bottom up functionality and two entities.
619 Sibling Class: PowerViewShadeTDBUBottom
620 API Class: ShadeTopDownBottomUp
623 _attr_translation_key =
"top"
627 coordinator: PowerviewShadeUpdateCoordinator,
628 device_info: PowerviewDeviceInfo,
633 """Initialize the shade."""
634 super().
__init__(coordinator, device_info, room_name, shade, name)
639 """Certain shades create multiple entities.
641 Do not poll shade multiple times. One shade will return data
642 for both and multiple polling will cause timeouts.
648 """Return if the cover is closed."""
650 return self.
positionspositions.secondary <= CLOSED_POSITION
654 """Return the current position of cover."""
660 """Return the open position and required additional positions."""
663 return ShadePosition(
664 primary=MIN_POSITION,
665 secondary=MAX_POSITION,
666 velocity=self.
positionspositions.velocity,
671 """Don't allow a cover to go into an impossbile position."""
672 return min(target_hass_position, (MAX_POSITION - self.
positionspositions.primary))
676 """Return a ShadePosition."""
677 return ShadePosition(
679 secondary=target_hass_position,
680 velocity=self.
positionspositions.velocity,
685 """Represent a shade that has a front sheer and rear opaque panel.
687 This equates to two shades being controlled by one motor
692 """Return the steps to make a move."""
696 primary = (self.
positionspositions.primary / 2) + 50
700 secondary = self.
positionspositions.secondary / 2
701 return ceil(primary + secondary)
705 """Return the open position and required additional positions."""
706 return ShadePosition(
707 primary=MAX_POSITION,
708 velocity=self.
positionspositions.velocity,
713 """Return the open position and required additional positions."""
714 return ShadePosition(
715 secondary=MIN_POSITION,
716 velocity=self.
positionspositions.velocity,
721 """Represent a shade that has a front sheer and rear opaque panel.
723 This equates to two shades being controlled by one motor.
724 The front shade must be completely down before the rear shade will move.
725 Sibling Class: PowerViewShadeDualOverlappedFront, PowerViewShadeDualOverlappedRear
726 API Class: ShadeDualOverlapped
728 Type 8 - Duolite (front and rear shades)
731 _attr_translation_key =
"combined"
735 coordinator: PowerviewShadeUpdateCoordinator,
736 device_info: PowerviewDeviceInfo,
741 """Initialize the shade."""
742 super().
__init__(coordinator, device_info, room_name, shade, name)
747 """Return if the cover is closed."""
749 return self.
positionspositions.secondary <= CLOSED_POSITION
753 """Return the current position of cover."""
756 position = (self.
positionspositions.primary / 2) + 50
757 if self.
positionspositions.primary == MIN_POSITION:
758 position = self.
positionspositions.secondary / 2
760 return ceil(position)
764 """Return a ShadePosition."""
766 if target_hass_position <= 50:
767 target_hass_position = target_hass_position * 2
768 return ShadePosition(
769 secondary=target_hass_position,
770 velocity=self.
positionspositions.velocity,
774 target_hass_position = (target_hass_position - 50) * 2
775 return ShadePosition(
776 primary=target_hass_position,
777 velocity=self.
positionspositions.velocity,
782 """Represent the shade front panel - These have an opaque panel too.
784 This equates to two shades being controlled by one motor.
785 The front shade must be completely down before the rear shade will move.
787 PowerViewShadeDualOverlappedCombined, PowerViewShadeDualOverlappedRear
789 ShadeDualOverlapped + ShadeDualOverlappedTilt90 + ShadeDualOverlappedTilt180
791 Type 8 - Duolite (front and rear shades)
792 Type 9 - Duolite with 90° Tilt (front bottom up shade that also tilts
793 plus a rear opaque (non-tilting) shade)
794 Type 10 - Duolite with 180° Tilt
797 _attr_translation_key =
"front"
801 coordinator: PowerviewShadeUpdateCoordinator,
802 device_info: PowerviewDeviceInfo,
807 """Initialize the shade."""
808 super().
__init__(coordinator, device_info, room_name, shade, name)
813 """Certain shades create multiple entities.
815 Do not poll shade multiple times. Combined shade will return data
816 and multiple polling will cause timeouts.
822 """Return a ShadePosition."""
823 return ShadePosition(
824 primary=target_hass_position,
825 velocity=self.
positionspositions.velocity,
830 """Return the close position and required additional positions."""
831 return ShadePosition(
832 primary=MIN_POSITION,
833 velocity=self.
positionspositions.velocity,
838 """Represent the shade front panel - These have an opaque panel too.
840 This equates to two shades being controlled by one motor.
841 The front shade must be completely down before the rear shade will move.
843 PowerViewShadeDualOverlappedCombined, PowerViewShadeDualOverlappedFront
845 ShadeDualOverlapped + ShadeDualOverlappedTilt90 + ShadeDualOverlappedTilt180
847 Type 8 - Duolite (front and rear shades)
848 Type 9 - Duolite with 90° Tilt (front bottom up shade that also tilts plus
849 a rear opaque (non-tilting) shade)
850 Type 10 - Duolite with 180° Tilt
853 _attr_translation_key =
"rear"
857 coordinator: PowerviewShadeUpdateCoordinator,
858 device_info: PowerviewDeviceInfo,
863 """Initialize the shade."""
864 super().
__init__(coordinator, device_info, room_name, shade, name)
869 """Certain shades create multiple entities.
871 Do not poll shade multiple times. Combined shade will return data
872 and multiple polling will cause timeouts.
878 """Return if the cover is closed."""
880 return self.
positionspositions.secondary <= CLOSED_POSITION
884 """Return the current position of cover."""
889 """Return a ShadePosition."""
890 return ShadePosition(
891 secondary=target_hass_position,
892 velocity=self.
positionspositions.velocity,
897 """Return the open position and required additional positions."""
898 return ShadePosition(
899 secondary=MAX_POSITION,
900 velocity=self.
positionspositions.velocity,
905 """Represent a shade that has a front sheer and rear opaque panel.
907 This equates to two shades being controlled by one motor.
908 The front shade must be completely down before the rear shade will move.
909 Tilting this shade will also force positional change of the main roller.
911 Sibling Class: PowerViewShadeDualOverlappedFront, PowerViewShadeDualOverlappedRear
912 API Class: ShadeDualOverlappedTilt90 + ShadeDualOverlappedTilt180
914 Type 9 - Duolite with 90° Tilt (front bottom up shade that also tilts plus a rear opaque (non-tilting) shade)
915 Type 10 - Duolite with 180° Tilt
921 coordinator: PowerviewShadeUpdateCoordinator,
922 device_info: PowerviewDeviceInfo,
927 """Initialize the shade."""
928 super().
__init__(coordinator, device_info, room_name, shade, name)
930 CoverEntityFeature.OPEN_TILT
931 | CoverEntityFeature.CLOSE_TILT
932 | CoverEntityFeature.SET_TILT_POSITION
940 """Return the steps to make a move."""
944 primary = (self.
positionspositions.primary / 2) + 50
948 secondary = self.
positionspositions.secondary / 2
950 return ceil(primary + secondary + tilt)
954 """Return a ShadePosition."""
955 return ShadePosition(
956 tilt=target_hass_tilt_position,
957 velocity=self.
positionspositions.velocity,
962 """Return the open tilt position and required additional positions."""
963 return replace(self.
_shade_shade.open_position_tilt, velocity=self.
positionspositions.velocity)
967 """Return the open tilt position and required additional positions."""
969 self.
_shade_shade.close_position_tilt, velocity=self.
positionspositions.velocity
974 0: (PowerViewShade,),
975 1: (PowerViewShadeWithTiltOnClosed,),
976 2: (PowerViewShadeWithTiltAnywhere,),
977 3: (PowerViewShade,),
978 4: (PowerViewShadeWithTiltAnywhere,),
979 5: (PowerViewShadeTiltOnly,),
980 6: (PowerViewShadeTopDown,),
982 PowerViewShadeTDBUTop,
983 PowerViewShadeTDBUBottom,
986 PowerViewShadeDualOverlappedCombined,
987 PowerViewShadeDualOverlappedFront,
988 PowerViewShadeDualOverlappedRear,
991 PowerViewShadeDualOverlappedCombinedTilt,
992 PowerViewShadeDualOverlappedFront,
993 PowerViewShadeDualOverlappedRear,
996 PowerViewShadeDualOverlappedCombinedTilt,
997 PowerViewShadeDualOverlappedFront,
998 PowerViewShadeDualOverlappedRear,
1001 PowerViewShadeDualOverlappedCombined,
1002 PowerViewShadeDualOverlappedFront,
1003 PowerViewShadeDualOverlappedRear,
1009 coordinator: PowerviewShadeUpdateCoordinator,
1010 device_info: PowerviewDeviceInfo,
1013 name_before_refresh: str,
1014 ) -> Iterable[ShadeEntity]:
1015 """Create a PowerViewShade entity."""
1016 classes: Iterable[BaseShade] = TYPE_TO_CLASSES.get(
1017 shade.capability.type, (PowerViewShade,)
1020 "%s %s (%s) detected as %a %s",
1023 shade.capability.type,
1028 cls(coordinator, device_info, room_name, shade, name_before_refresh)
current_cover_tilt_position
int|None current_cover_position(self)
None async_stop_cover(self, **Any kwargs)
None async_close_cover(self, **Any kwargs)
int transition_steps(self)
None _async_update_shade_from_group(self)
None _async_execute_move(self, ShadePosition move)
None _async_force_refresh_state(self)
ShadePosition _get_shade_move(self, int target_hass_position)
int current_cover_position(self)
None _async_set_cover_position(self, int target_hass_position)
None async_added_to_hass(self)
dict[str, str] extra_state_attributes(self)
None __init__(self, PowerviewShadeUpdateCoordinator coordinator, PowerviewDeviceInfo device_info, str room_name, BaseShade shade, str name)
bool _update_in_progress(self)
_scheduled_transition_update
None _async_force_resync(self, *Any _)
ShadePosition close_position(self)
None _async_update_shade_data(self, ShadePosition shade_data)
None _async_schedule_update_for_transition(self, int steps)
None _async_cancel_scheduled_transition_update(self)
None async_stop_cover(self, **Any kwargs)
None async_set_cover_position(self, **Any kwargs)
ShadePosition open_position(self)
int _clamp_cover_limit(self, int target_hass_position)
None async_will_remove_from_hass(self)
None _async_complete_schedule_update(self, datetime _)
tuple _attr_supported_features
None async_open_cover(self, **Any kwargs)
int transition_steps(self)
ShadePosition open_position(self)
ShadePosition close_position(self)
ShadePosition open_tilt_position(self)
None __init__(self, PowerviewShadeUpdateCoordinator coordinator, PowerviewDeviceInfo device_info, str room_name, BaseShade shade, str name)
int transition_steps(self)
ShadePosition close_tilt_position(self)
ShadePosition _get_shade_tilt(self, int target_hass_tilt_position)
ShadePosition _get_shade_move(self, int target_hass_position)
None __init__(self, PowerviewShadeUpdateCoordinator coordinator, PowerviewDeviceInfo device_info, str room_name, BaseShade shade, str name)
None __init__(self, PowerviewShadeUpdateCoordinator coordinator, PowerviewDeviceInfo device_info, str room_name, BaseShade shade, str name)
ShadePosition close_position(self)
ShadePosition _get_shade_move(self, int target_hass_position)
ShadePosition open_position(self)
None __init__(self, PowerviewShadeUpdateCoordinator coordinator, PowerviewDeviceInfo device_info, str room_name, BaseShade shade, str name)
ShadePosition _get_shade_move(self, int target_hass_position)
int transition_steps(self)
int _clamp_cover_limit(self, int target_hass_position)
None __init__(self, PowerviewShadeUpdateCoordinator coordinator, PowerviewDeviceInfo device_info, str room_name, BaseShade shade, str name)
ShadePosition _get_shade_move(self, int target_hass_position)
ShadePosition open_position(self)
ShadePosition _get_shade_move(self, int target_hass_position)
None __init__(self, PowerviewShadeUpdateCoordinator coordinator, PowerviewDeviceInfo device_info, str room_name, BaseShade shade, str name)
int _clamp_cover_limit(self, int target_hass_position)
int transition_steps(self)
None __init__(self, PowerviewShadeUpdateCoordinator coordinator, PowerviewDeviceInfo device_info, str room_name, BaseShade shade, str name)
None async_set_cover_position(self, **Any kwargs)
ShadePosition _get_shade_move(self, int target_hass_position)
ShadePosition _get_shade_tilt(self, int target_hass_tilt_position)
ShadePosition open_tilt_position(self)
None async_stop_cover_tilt(self, **Any kwargs)
None __init__(self, PowerviewShadeUpdateCoordinator coordinator, PowerviewDeviceInfo device_info, str room_name, BaseShade shade, str name)
None _async_set_cover_tilt_position(self, int target_hass_tilt_position)
ShadePosition _get_shade_move(self, int target_hass_position)
int transition_steps(self)
None async_close_cover_tilt(self, **Any kwargs)
ShadePosition close_tilt_position(self)
None async_open_cover_tilt(self, **Any kwargs)
ShadePosition _get_shade_tilt(self, int target_hass_tilt_position)
None async_set_cover_tilt_position(self, **Any kwargs)
ShadePosition open_position(self)
ShadePosition close_position(self)
ShadePosition close_tilt_position(self)
ShadePosition open_tilt_position(self)
PowerviewShadeData data(self)
ShadePosition positions(self)
None update_from_group_data(self, int shade_id)
None update_shade_position(self, int shade_id, ShadePosition new_position)
None async_write_ha_state(self)
None async_on_remove(self, CALLBACK_TYPE func)
str|UndefinedType|None name(self)
Callable[[], None] async_add_listener(self, CALLBACK_TYPE update_callback, Any context=None)
None async_setup_entry(HomeAssistant hass, PowerviewConfigEntry entry, AddEntitiesCallback async_add_entities)
Iterable[ShadeEntity] create_powerview_shade_entity(PowerviewShadeUpdateCoordinator coordinator, PowerviewDeviceInfo device_info, str room_name, BaseShade shade, str name_before_refresh)
bool is_supported(str name, ViCareRequiredKeysMixin entity_description, vicare_device)
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)