Home Assistant Unofficial Reference 2024.12.1
cover.py
Go to the documentation of this file.
1 """Support for hunter douglas shades."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable, Iterable
6 from dataclasses import replace
7 from datetime import datetime, timedelta
8 import logging
9 from math import ceil
10 from typing import Any
11 
12 from aiopvapi.helpers.constants import (
13  ATTR_NAME,
14  CLOSED_POSITION,
15  MAX_POSITION,
16  MIN_POSITION,
17  MOTION_STOP,
18 )
19 from aiopvapi.resources.shade import BaseShade, ShadePosition
20 
22  ATTR_POSITION,
23  ATTR_TILT_POSITION,
24  CoverDeviceClass,
25  CoverEntity,
26  CoverEntityFeature,
27 )
28 from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
29 from homeassistant.helpers.entity_platform import AddEntitiesCallback
30 from homeassistant.helpers.event import async_call_later
31 
32 from .const import STATE_ATTRIBUTE_ROOM_NAME
33 from .coordinator import PowerviewShadeUpdateCoordinator
34 from .entity import ShadeEntity
35 from .model import PowerviewConfigEntry, PowerviewDeviceInfo
36 
37 _LOGGER = logging.getLogger(__name__)
38 
39 # Estimated time it takes to complete a transition
40 # from one state to another
41 TRANSITION_COMPLETE_DURATION = 40
42 
43 PARALLEL_UPDATES = 1
44 
45 RESYNC_DELAY = 60
46 
47 SCAN_INTERVAL = timedelta(minutes=10)
48 
49 
51  hass: HomeAssistant,
52  entry: PowerviewConfigEntry,
53  async_add_entities: AddEntitiesCallback,
54 ) -> None:
55  """Set up the hunter douglas shades."""
56  pv_entry = entry.runtime_data
57  coordinator = pv_entry.coordinator
58 
59  async def _async_initial_refresh() -> None:
60  """Force position refresh shortly after adding.
61 
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
65  prevent hub crashes.
66  """
67 
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) # default 15 second timeout
72 
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, "")
76  entities.extend(
78  coordinator, pv_entry.device_info, room_name, shade, shade.name
79  )
80  )
81 
82  async_add_entities(entities)
83 
84  # background the fetching of state for initial launch
85  entry.async_create_background_task(
86  hass,
87  _async_initial_refresh(),
88  f"powerview {entry.title} initial shade refresh",
89  )
90 
91 
93  """Representation of a powerview shade."""
94 
95  _attr_device_class = CoverDeviceClass.SHADE
96  _attr_supported_features = (
97  CoverEntityFeature.OPEN
98  | CoverEntityFeature.CLOSE
99  | CoverEntityFeature.SET_POSITION
100  )
101 
102  def __init__(
103  self,
104  coordinator: PowerviewShadeUpdateCoordinator,
105  device_info: PowerviewDeviceInfo,
106  room_name: str,
107  shade: BaseShade,
108  name: str,
109  ) -> None:
110  """Initialize the shade."""
111  super().__init__(coordinator, device_info, room_name, shade, name)
112  self._shade_shade: BaseShade = shade
113  self._scheduled_transition_update_scheduled_transition_update: CALLBACK_TYPE | None = None
114  if self._shade_shade.is_supported(MOTION_STOP):
115  self._attr_supported_features_attr_supported_features |= CoverEntityFeature.STOP
116  self._forced_resync_forced_resync: Callable[[], None] | None = None
117 
118  @property
119  def assumed_state(self) -> bool:
120  """If the device is hard wired we are polling state.
121 
122  The hub will frequently provide the wrong state
123  for battery power devices so we set assumed
124  state in this case.
125  """
126  return not self._is_hard_wired_is_hard_wired
127 
128  @property
129  def should_poll(self) -> bool:
130  """Only poll if the device is hard wired.
131 
132  We cannot poll battery powered devices
133  as it would drain their batteries in a matter
134  of days.
135  """
136  return self._is_hard_wired_is_hard_wired
137 
138  @property
139  def extra_state_attributes(self) -> dict[str, str]:
140  """Return the state attributes."""
141  return {STATE_ATTRIBUTE_ROOM_NAME: self._room_name_room_name}
142 
143  @property
144  def is_closed(self) -> bool:
145  """Return if the cover is closed."""
146  return self.positionspositions.primary <= CLOSED_POSITION
147 
148  @property
149  def current_cover_position(self) -> int:
150  """Return the current position of cover."""
151  return self.positionspositions.primary
152 
153  @property
154  def transition_steps(self) -> int:
155  """Return the steps to make a move."""
156  return self.positionspositions.primary
157 
158  @property
159  def open_position(self) -> ShadePosition:
160  """Return the open position and required additional positions."""
161  return replace(self._shade_shade.open_position, velocity=self.positionspositions.velocity)
162 
163  @property
164  def close_position(self) -> ShadePosition:
165  """Return the close position and required additional positions."""
166  return replace(self._shade_shade.close_position, velocity=self.positionspositions.velocity)
167 
168  async def async_close_cover(self, **kwargs: Any) -> None:
169  """Close the cover."""
170  self._async_schedule_update_for_transition_async_schedule_update_for_transition(self.transition_stepstransition_steps)
171  await self._async_execute_move_async_execute_move(self.close_positionclose_position)
172  self._attr_is_opening_attr_is_opening = False
173  self._attr_is_closing_attr_is_closing = True
174  self.async_write_ha_stateasync_write_ha_state()
175 
176  async def async_open_cover(self, **kwargs: Any) -> None:
177  """Open the cover."""
178  self._async_schedule_update_for_transition_async_schedule_update_for_transition(100 - self.transition_stepstransition_steps)
179  await self._async_execute_move_async_execute_move(self.open_positionopen_position)
180  self._attr_is_opening_attr_is_opening = True
181  self._attr_is_closing_attr_is_closing = False
182  self.async_write_ha_stateasync_write_ha_state()
183 
184  async def async_stop_cover(self, **kwargs: Any) -> None:
185  """Stop the cover."""
186  self._async_cancel_scheduled_transition_update_async_cancel_scheduled_transition_update()
187  await self._shade_shade.stop()
188  await self._async_force_refresh_state_async_force_refresh_state()
189 
190  @callback
191  def _clamp_cover_limit(self, target_hass_position: int) -> int:
192  """Don't allow a cover to go into an impossbile position."""
193  # no override required in base
194  return target_hass_position
195 
196  async def async_set_cover_position(self, **kwargs: Any) -> None:
197  """Move the shade to a specific position."""
198  await self._async_set_cover_position_async_set_cover_position(kwargs[ATTR_POSITION])
199 
200  @callback
201  def _get_shade_move(self, target_hass_position: int) -> ShadePosition:
202  """Return a ShadePosition."""
203  return ShadePosition(
204  primary=target_hass_position,
205  velocity=self.positionspositions.velocity,
206  )
207 
208  async def _async_execute_move(self, move: ShadePosition) -> None:
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)
214 
215  # Process the response from the hub (including new positions)
216  self.datadatadata.update_shade_position(self._shade_shade.id, response)
217 
218  async def _async_set_cover_position(self, target_hass_position: int) -> None:
219  """Move the shade to a position."""
220  target_hass_position = self._clamp_cover_limit_clamp_cover_limit(target_hass_position)
221  current_hass_position = self.current_cover_positioncurrent_cover_positioncurrent_cover_positioncurrent_cover_position
222  self._async_schedule_update_for_transition_async_schedule_update_for_transition(
223  abs(current_hass_position - target_hass_position)
224  )
225  await self._async_execute_move_async_execute_move(self._get_shade_move_get_shade_move(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
228  self.async_write_ha_stateasync_write_ha_state()
229 
230  @callback
231  def _async_update_shade_data(self, shade_data: ShadePosition) -> None:
232  """Update the current cover position from the data."""
233  self.datadatadata.update_shade_position(self._shade_shade.id, shade_data)
234  self._attr_is_opening_attr_is_opening = False
235  self._attr_is_closing_attr_is_closing = False
236 
237  @callback
239  """Cancel any previous updates."""
240  if self._scheduled_transition_update_scheduled_transition_update:
241  self._scheduled_transition_update_scheduled_transition_update()
242  self._scheduled_transition_update_scheduled_transition_update = None
243  if self._forced_resync_forced_resync:
244  self._forced_resync_forced_resync()
245  self._forced_resync_forced_resync = None
246 
247  @callback
248  def _async_schedule_update_for_transition(self, steps: int) -> None:
249  # Cancel any previous updates
250  self._async_cancel_scheduled_transition_update_async_cancel_scheduled_transition_update()
251 
252  est_time_to_complete_transition = 1 + int(
253  TRANSITION_COMPLETE_DURATION * (steps / 100)
254  )
255 
256  _LOGGER.debug(
257  "Estimated time to complete transition of %s steps for %s: %s",
258  steps,
259  self.namenamename,
260  est_time_to_complete_transition,
261  )
262 
263  # Schedule a forced update for when we expect the transition
264  # to be completed.
265  self._scheduled_transition_update_scheduled_transition_update = async_call_later(
266  self.hasshasshass,
267  est_time_to_complete_transition,
268  self._async_complete_schedule_update_async_complete_schedule_update,
269  )
270 
271  async def _async_complete_schedule_update(self, _: datetime) -> None:
272  """Update status of the cover."""
273  _LOGGER.debug("Processing scheduled update for %s", self.namenamename)
274  self._scheduled_transition_update_scheduled_transition_update = None
275  await self._async_force_refresh_state_async_force_refresh_state()
276  self._forced_resync_forced_resync = async_call_later(
277  self.hasshasshass, RESYNC_DELAY, self._async_force_resync_async_force_resync
278  )
279 
280  async def _async_force_resync(self, *_: Any) -> None:
281  """Force a resync after an update since the hub may have stale state."""
282  self._forced_resync_forced_resync = None
283  _LOGGER.debug("Force resync of shade %s", self.namenamename)
284  await self._async_force_refresh_state_async_force_refresh_state()
285 
286  async def _async_force_refresh_state(self) -> None:
287  """Refresh the cover state and force the device cache to be bypassed."""
288  await self.async_updateasync_updateasync_update()
289  self.async_write_ha_stateasync_write_ha_state()
290 
291  # pylint: disable-next=hass-missing-super-call
292  async def async_added_to_hass(self) -> None:
293  """When entity is added to hass."""
294  self.async_on_removeasync_on_remove(
295  self.coordinator.async_add_listener(self._async_update_shade_from_group_async_update_shade_from_group)
296  )
297 
298  async def async_will_remove_from_hass(self) -> None:
299  """Cancel any pending refreshes."""
300  self._async_cancel_scheduled_transition_update_async_cancel_scheduled_transition_update()
301 
302  @property
303  def _update_in_progress(self) -> bool:
304  """Check if an update is already in progress."""
305  return bool(self._scheduled_transition_update_scheduled_transition_update or self._forced_resync_forced_resync)
306 
307  @callback
309  """Update with new data from the coordinator."""
310  if self._update_in_progress_update_in_progress:
311  # If a transition is in progress the data will be wrong
312  return
313  self.datadatadata.update_from_group_data(self._shade_shade.id)
314  self.async_write_ha_stateasync_write_ha_state()
315 
316  async def async_update(self) -> None:
317  """Refresh shade position."""
318  if self._update_in_progress_update_in_progress:
319  # The update will likely timeout and
320  # error if are already have one in flight
321  return
322  # suppress timeouts caused by hub nightly reboot
323  async with self.coordinator.radio_operation_lock:
324  await self._shade_shade.refresh(
325  suppress_timeout=True
326  ) # default 15 second timeout
327  _LOGGER.debug("Process update %s: %s", self.namenamename, self._shade_shade.current_position)
328  self._async_update_shade_data_async_update_shade_data(self._shade_shade.current_position)
329 
330 
332  """Represent a standard shade."""
333 
334  _attr_name = None
335 
336 
338  """Representation for PowerView shades with tilt capabilities."""
339 
340  _attr_name = None
341 
342  def __init__(
343  self,
344  coordinator: PowerviewShadeUpdateCoordinator,
345  device_info: PowerviewDeviceInfo,
346  room_name: str,
347  shade: BaseShade,
348  name: str,
349  ) -> None:
350  """Initialize the shade."""
351  super().__init__(coordinator, device_info, room_name, shade, name)
352  self._attr_supported_features_attr_supported_features |= (
353  CoverEntityFeature.OPEN_TILT
354  | CoverEntityFeature.CLOSE_TILT
355  | CoverEntityFeature.SET_TILT_POSITION
356  )
357  if self._shade_shade.is_supported(MOTION_STOP):
358  self._attr_supported_features_attr_supported_features |= CoverEntityFeature.STOP_TILT
359  self._max_tilt_max_tilt = self._shade_shade.shade_limits.tilt_max
360 
361  @property
362  def current_cover_tilt_position(self) -> int:
363  """Return the current cover tile position."""
364  return self.positionspositions.tilt
365 
366  @property
367  def transition_steps(self) -> int:
368  """Return the steps to make a move."""
369  return self.positionspositions.primary + self.positionspositions.tilt
370 
371  @property
372  def open_tilt_position(self) -> ShadePosition:
373  """Return the open tilt position and required additional positions."""
374  return replace(self._shade_shade.open_position_tilt, velocity=self.positionspositions.velocity)
375 
376  @property
377  def close_tilt_position(self) -> ShadePosition:
378  """Return the close tilt position and required additional positions."""
379  return replace(
380  self._shade_shade.close_position_tilt, velocity=self.positionspositions.velocity
381  )
382 
383  async def async_close_cover_tilt(self, **kwargs: Any) -> None:
384  """Close the cover tilt."""
385  self._async_schedule_update_for_transition_async_schedule_update_for_transition(self.transition_stepstransition_stepstransition_steps)
386  await self._async_execute_move_async_execute_move(self.close_tilt_positionclose_tilt_position)
387  self.async_write_ha_stateasync_write_ha_state()
388 
389  async def async_open_cover_tilt(self, **kwargs: Any) -> None:
390  """Open the cover tilt."""
391  self._async_schedule_update_for_transition_async_schedule_update_for_transition(100 - self.transition_stepstransition_stepstransition_steps)
392  await self._async_execute_move_async_execute_move(self.open_tilt_positionopen_tilt_position)
393  self.async_write_ha_stateasync_write_ha_state()
394 
395  async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
396  """Move the tilt to a specific position."""
397  await self._async_set_cover_tilt_position_async_set_cover_tilt_position(kwargs[ATTR_TILT_POSITION])
398 
400  self, target_hass_tilt_position: int
401  ) -> None:
402  """Move the tilt to a specific position."""
403  final_position = self.current_cover_positioncurrent_cover_positioncurrent_cover_positioncurrent_cover_position + target_hass_tilt_position
404  self._async_schedule_update_for_transition_async_schedule_update_for_transition(
405  abs(self.transition_stepstransition_stepstransition_steps - final_position)
406  )
407  await self._async_execute_move_async_execute_move(self._get_shade_tilt_get_shade_tilt(target_hass_tilt_position))
408  self.async_write_ha_stateasync_write_ha_state()
409 
410  @callback
411  def _get_shade_move(self, target_hass_position: int) -> ShadePosition:
412  """Return a ShadePosition."""
413  return ShadePosition(
414  primary=target_hass_position,
415  velocity=self.positionspositions.velocity,
416  )
417 
418  @callback
419  def _get_shade_tilt(self, target_hass_tilt_position: int) -> ShadePosition:
420  """Return a ShadePosition."""
421  return ShadePosition(
422  tilt=target_hass_tilt_position,
423  velocity=self.positionspositions.velocity,
424  )
425 
426  async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
427  """Stop the cover tilting."""
428  await self.async_stop_coverasync_stop_coverasync_stop_cover()
429 
430 
432  """Representation of a PowerView shade with tilt when closed capabilities.
433 
434  API Class: ShadeBottomUpTiltOnClosed + ShadeBottomUpTiltOnClosed90
435 
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)
438  """
439 
440  _attr_name = None
441 
442  @property
443  def open_position(self) -> ShadePosition:
444  """Return the open position and required additional positions."""
445  return replace(self._shade_shade.open_position, velocity=self.positionspositions.velocity)
446 
447  @property
448  def close_position(self) -> ShadePosition:
449  """Return the close position and required additional positions."""
450  return replace(self._shade_shade.close_position, velocity=self.positionspositions.velocity)
451 
452  @property
453  def open_tilt_position(self) -> ShadePosition:
454  """Return the open tilt position and required additional positions."""
455  return replace(self._shade_shade.open_position_tilt, velocity=self.positionspositions.velocity)
456 
457  @property
458  def close_tilt_position(self) -> ShadePosition:
459  """Return the close tilt position and required additional positions."""
460  return replace(
461  self._shade_shade.close_position_tilt, velocity=self.positionspositions.velocity
462  )
463 
464 
466  """Representation of a PowerView shade with tilt anywhere capabilities.
467 
468  API Class: ShadeBottomUpTiltAnywhere, ShadeVerticalTiltAnywhere
469 
470  Type 2 - Bottom Up w/ 180° Tilt
471  Type 4 - Vertical (Traversing) w/ 180° Tilt
472  """
473 
474  @callback
475  def _get_shade_move(self, target_hass_position: int) -> ShadePosition:
476  """Return a ShadePosition."""
477  return ShadePosition(
478  primary=target_hass_position,
479  tilt=self.positionspositions.tilt,
480  velocity=self.positionspositions.velocity,
481  )
482 
483  @callback
484  def _get_shade_tilt(self, target_hass_tilt_position: int) -> ShadePosition:
485  """Return a ShadePosition."""
486  return ShadePosition(
487  primary=self.positionspositions.primary,
488  tilt=target_hass_tilt_position,
489  velocity=self.positionspositions.velocity,
490  )
491 
492 
494  """Representation of a shade with tilt only capability, no move.
495 
496  API Class: ShadeTiltOnly
497 
498  Type 5 - Tilt Only 180°
499  """
500 
501  def __init__(
502  self,
503  coordinator: PowerviewShadeUpdateCoordinator,
504  device_info: PowerviewDeviceInfo,
505  room_name: str,
506  shade: BaseShade,
507  name: str,
508  ) -> None:
509  """Initialize the shade."""
510  super().__init__(coordinator, device_info, room_name, shade, name)
511  self._attr_supported_features_attr_supported_features_attr_supported_features = (
512  CoverEntityFeature.OPEN_TILT
513  | CoverEntityFeature.CLOSE_TILT
514  | CoverEntityFeature.SET_TILT_POSITION
515  )
516  if self._shade_shade.is_supported(MOTION_STOP):
517  self._attr_supported_features_attr_supported_features_attr_supported_features |= CoverEntityFeature.STOP_TILT
518  self._max_tilt_max_tilt_max_tilt = self._shade_shade.shade_limits.tilt_max
519 
520  @property
521  def current_cover_position(self) -> int:
522  """Return the current position of cover."""
523  # allows using parent class with no other alterations
524  return CLOSED_POSITION
525 
526  @property
527  def transition_steps(self) -> int:
528  """Return the steps to make a move."""
529  return self.positionspositions.tilt
530 
531  @property
532  def is_closed(self) -> bool:
533  """Return if the cover is closed."""
534  return self.positionspositions.tilt <= CLOSED_POSITION
535 
536 
538  """Representation of a shade that lowers from the roof to the floor.
539 
540  These shades are inverted where MAX_POSITION equates to closed and MIN_POSITION is open
541  API Class: ShadeTopDown
542 
543  Type 6 - Top Down
544  """
545 
546  _attr_name = None
547 
548  @property
549  def current_cover_position(self) -> int:
550  """Return the current position of cover."""
551  # inverted positioning
552  return MAX_POSITION - self.positionspositions.primary
553 
554  async def async_set_cover_position(self, **kwargs: Any) -> None:
555  """Move the shade to a specific position."""
556  await self._async_set_cover_position_async_set_cover_position(MAX_POSITION - kwargs[ATTR_POSITION])
557 
558  @property
559  def is_closed(self) -> bool:
560  """Return if the cover is closed."""
561  return (MAX_POSITION - self.positionspositions.primary) <= CLOSED_POSITION
562 
563 
565  """Representation of a shade with top/down bottom/up capabilities.
566 
567  Base methods shared between the two shades created
568  Child Classes: PowerViewShadeTDBUBottom / PowerViewShadeTDBUTop
569  API Class: ShadeTopDownBottomUp
570  """
571 
572  @property
573  def transition_steps(self) -> int:
574  """Return the steps to make a move."""
575  return self.positionspositions.primary + self.positionspositions.secondary
576 
577 
579  """Representation of the bottom PowerViewShadeDualRailBase shade.
580 
581  These shades have top/down bottom up functionality and two entities.
582  Sibling Class: PowerViewShadeTDBUTop
583  API Class: ShadeTopDownBottomUp
584  """
585 
586  _attr_translation_key = "bottom"
587 
588  def __init__(
589  self,
590  coordinator: PowerviewShadeUpdateCoordinator,
591  device_info: PowerviewDeviceInfo,
592  room_name: str,
593  shade: BaseShade,
594  name: str,
595  ) -> None:
596  """Initialize the shade."""
597  super().__init__(coordinator, device_info, room_name, shade, name)
598  self._attr_unique_id_attr_unique_id_attr_unique_id = f"{self._attr_unique_id}_bottom"
599 
600  @callback
601  def _clamp_cover_limit(self, target_hass_position: int) -> int:
602  """Don't allow a cover to go into an impossbile position."""
603  return min(target_hass_position, (MAX_POSITION - self.positionspositions.secondary))
604 
605  @callback
606  def _get_shade_move(self, target_hass_position: int) -> ShadePosition:
607  """Return a ShadePosition."""
608  return ShadePosition(
609  primary=target_hass_position,
610  secondary=self.positionspositions.secondary,
611  velocity=self.positionspositions.velocity,
612  )
613 
614 
616  """Representation of the top PowerViewShadeDualRailBase shade.
617 
618  These shades have top/down bottom up functionality and two entities.
619  Sibling Class: PowerViewShadeTDBUBottom
620  API Class: ShadeTopDownBottomUp
621  """
622 
623  _attr_translation_key = "top"
624 
625  def __init__(
626  self,
627  coordinator: PowerviewShadeUpdateCoordinator,
628  device_info: PowerviewDeviceInfo,
629  room_name: str,
630  shade: BaseShade,
631  name: str,
632  ) -> None:
633  """Initialize the shade."""
634  super().__init__(coordinator, device_info, room_name, shade, name)
635  self._attr_unique_id_attr_unique_id_attr_unique_id = f"{self._attr_unique_id}_top"
636 
637  @property
638  def should_poll(self) -> bool:
639  """Certain shades create multiple entities.
640 
641  Do not poll shade multiple times. One shade will return data
642  for both and multiple polling will cause timeouts.
643  """
644  return False
645 
646  @property
647  def is_closed(self) -> bool:
648  """Return if the cover is closed."""
649  # top shade needs to check other motor
650  return self.positionspositions.secondary <= CLOSED_POSITION
651 
652  @property
653  def current_cover_position(self) -> int:
654  """Return the current position of cover."""
655  # these need to be inverted to report state correctly in HA
656  return self.positionspositions.secondary
657 
658  @property
659  def open_position(self) -> ShadePosition:
660  """Return the open position and required additional positions."""
661  # these shades share a class in parent API
662  # override open position for top shade
663  return ShadePosition(
664  primary=MIN_POSITION,
665  secondary=MAX_POSITION,
666  velocity=self.positionspositions.velocity,
667  )
668 
669  @callback
670  def _clamp_cover_limit(self, target_hass_position: int) -> int:
671  """Don't allow a cover to go into an impossbile position."""
672  return min(target_hass_position, (MAX_POSITION - self.positionspositions.primary))
673 
674  @callback
675  def _get_shade_move(self, target_hass_position: int) -> ShadePosition:
676  """Return a ShadePosition."""
677  return ShadePosition(
678  primary=self.positionspositions.primary,
679  secondary=target_hass_position,
680  velocity=self.positionspositions.velocity,
681  )
682 
683 
685  """Represent a shade that has a front sheer and rear opaque panel.
686 
687  This equates to two shades being controlled by one motor
688  """
689 
690  @property
691  def transition_steps(self) -> int:
692  """Return the steps to make a move."""
693  # poskind 1 represents the second half of the shade in hass
694  # front must be fully closed before rear can move
695  # 51 - 100 is equiv to 1-100 on other shades - one motor, two shades
696  primary = (self.positionspositions.primary / 2) + 50
697  # poskind 2 represents the shade first half of the shade in hass
698  # rear (opaque) must be fully open before front can move
699  # 51 - 100 is equiv to 1-100 on other shades - one motor, two shades
700  secondary = self.positionspositions.secondary / 2
701  return ceil(primary + secondary)
702 
703  @property
704  def open_position(self) -> ShadePosition:
705  """Return the open position and required additional positions."""
706  return ShadePosition(
707  primary=MAX_POSITION,
708  velocity=self.positionspositions.velocity,
709  )
710 
711  @property
712  def close_position(self) -> ShadePosition:
713  """Return the open position and required additional positions."""
714  return ShadePosition(
715  secondary=MIN_POSITION,
716  velocity=self.positionspositions.velocity,
717  )
718 
719 
721  """Represent a shade that has a front sheer and rear opaque panel.
722 
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
727 
728  Type 8 - Duolite (front and rear shades)
729  """
730 
731  _attr_translation_key = "combined"
732 
733  def __init__(
734  self,
735  coordinator: PowerviewShadeUpdateCoordinator,
736  device_info: PowerviewDeviceInfo,
737  room_name: str,
738  shade: BaseShade,
739  name: str,
740  ) -> None:
741  """Initialize the shade."""
742  super().__init__(coordinator, device_info, room_name, shade, name)
743  self._attr_unique_id_attr_unique_id_attr_unique_id = f"{self._attr_unique_id}_combined"
744 
745  @property
746  def is_closed(self) -> bool:
747  """Return if the cover is closed."""
748  # if rear shade is down it is closed
749  return self.positionspositions.secondary <= CLOSED_POSITION
750 
751  @property
752  def current_cover_position(self) -> int:
753  """Return the current position of cover."""
754  # if front is open return that (other positions are impossible)
755  # if front shade is closed get position of rear
756  position = (self.positionspositions.primary / 2) + 50
757  if self.positionspositions.primary == MIN_POSITION:
758  position = self.positionspositions.secondary / 2
759 
760  return ceil(position)
761 
762  @callback
763  def _get_shade_move(self, target_hass_position: int) -> ShadePosition:
764  """Return a ShadePosition."""
765  # 0 - 50 represents the rear blockut shade
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,
771  )
772 
773  # 51 <= target_hass_position <= 100 (51-100 represents front sheer shade)
774  target_hass_position = (target_hass_position - 50) * 2
775  return ShadePosition(
776  primary=target_hass_position,
777  velocity=self.positionspositions.velocity,
778  )
779 
780 
782  """Represent the shade front panel - These have an opaque panel too.
783 
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.
786  Sibling Class:
787  PowerViewShadeDualOverlappedCombined, PowerViewShadeDualOverlappedRear
788  API Class:
789  ShadeDualOverlapped + ShadeDualOverlappedTilt90 + ShadeDualOverlappedTilt180
790 
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
795  """
796 
797  _attr_translation_key = "front"
798 
799  def __init__(
800  self,
801  coordinator: PowerviewShadeUpdateCoordinator,
802  device_info: PowerviewDeviceInfo,
803  room_name: str,
804  shade: BaseShade,
805  name: str,
806  ) -> None:
807  """Initialize the shade."""
808  super().__init__(coordinator, device_info, room_name, shade, name)
809  self._attr_unique_id_attr_unique_id_attr_unique_id = f"{self._attr_unique_id}_front"
810 
811  @property
812  def should_poll(self) -> bool:
813  """Certain shades create multiple entities.
814 
815  Do not poll shade multiple times. Combined shade will return data
816  and multiple polling will cause timeouts.
817  """
818  return False
819 
820  @callback
821  def _get_shade_move(self, target_hass_position: int) -> ShadePosition:
822  """Return a ShadePosition."""
823  return ShadePosition(
824  primary=target_hass_position,
825  velocity=self.positionspositions.velocity,
826  )
827 
828  @property
829  def close_position(self) -> ShadePosition:
830  """Return the close position and required additional positions."""
831  return ShadePosition(
832  primary=MIN_POSITION,
833  velocity=self.positionspositions.velocity,
834  )
835 
836 
838  """Represent the shade front panel - These have an opaque panel too.
839 
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.
842  Sibling Class:
843  PowerViewShadeDualOverlappedCombined, PowerViewShadeDualOverlappedFront
844  API Class:
845  ShadeDualOverlapped + ShadeDualOverlappedTilt90 + ShadeDualOverlappedTilt180
846 
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
851  """
852 
853  _attr_translation_key = "rear"
854 
855  def __init__(
856  self,
857  coordinator: PowerviewShadeUpdateCoordinator,
858  device_info: PowerviewDeviceInfo,
859  room_name: str,
860  shade: BaseShade,
861  name: str,
862  ) -> None:
863  """Initialize the shade."""
864  super().__init__(coordinator, device_info, room_name, shade, name)
865  self._attr_unique_id_attr_unique_id_attr_unique_id = f"{self._attr_unique_id}_rear"
866 
867  @property
868  def should_poll(self) -> bool:
869  """Certain shades create multiple entities.
870 
871  Do not poll shade multiple times. Combined shade will return data
872  and multiple polling will cause timeouts.
873  """
874  return False
875 
876  @property
877  def is_closed(self) -> bool:
878  """Return if the cover is closed."""
879  # if rear shade is down it is closed
880  return self.positionspositions.secondary <= CLOSED_POSITION
881 
882  @property
883  def current_cover_position(self) -> int:
884  """Return the current position of cover."""
885  return self.positionspositions.secondary
886 
887  @callback
888  def _get_shade_move(self, target_hass_position: int) -> ShadePosition:
889  """Return a ShadePosition."""
890  return ShadePosition(
891  secondary=target_hass_position,
892  velocity=self.positionspositions.velocity,
893  )
894 
895  @property
896  def open_position(self) -> ShadePosition:
897  """Return the open position and required additional positions."""
898  return ShadePosition(
899  secondary=MAX_POSITION,
900  velocity=self.positionspositions.velocity,
901  )
902 
903 
905  """Represent a shade that has a front sheer and rear opaque panel.
906 
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.
910 
911  Sibling Class: PowerViewShadeDualOverlappedFront, PowerViewShadeDualOverlappedRear
912  API Class: ShadeDualOverlappedTilt90 + ShadeDualOverlappedTilt180
913 
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
916  """
917 
918  # type
919  def __init__(
920  self,
921  coordinator: PowerviewShadeUpdateCoordinator,
922  device_info: PowerviewDeviceInfo,
923  room_name: str,
924  shade: BaseShade,
925  name: str,
926  ) -> None:
927  """Initialize the shade."""
928  super().__init__(coordinator, device_info, room_name, shade, name)
929  self._attr_supported_features_attr_supported_features |= (
930  CoverEntityFeature.OPEN_TILT
931  | CoverEntityFeature.CLOSE_TILT
932  | CoverEntityFeature.SET_TILT_POSITION
933  )
934  if self._shade_shade.is_supported(MOTION_STOP):
935  self._attr_supported_features_attr_supported_features |= CoverEntityFeature.STOP_TILT
936  self._max_tilt_max_tilt = self._shade_shade.shade_limits.tilt_max
937 
938  @property
939  def transition_steps(self) -> int:
940  """Return the steps to make a move."""
941  # poskind 1 represents the second half of the shade in hass
942  # front must be fully closed before rear can move
943  # 51 - 100 is equiv to 1-100 on other shades - one motor, two shades
944  primary = (self.positionspositions.primary / 2) + 50
945  # poskind 2 represents the shade first half of the shade in hass
946  # rear (opaque) must be fully open before front can move
947  # 51 - 100 is equiv to 1-100 on other shades - one motor, two shades
948  secondary = self.positionspositions.secondary / 2
949  tilt = self.positionspositions.tilt
950  return ceil(primary + secondary + tilt)
951 
952  @callback
953  def _get_shade_tilt(self, target_hass_tilt_position: int) -> ShadePosition:
954  """Return a ShadePosition."""
955  return ShadePosition(
956  tilt=target_hass_tilt_position,
957  velocity=self.positionspositions.velocity,
958  )
959 
960  @property
961  def open_tilt_position(self) -> ShadePosition:
962  """Return the open tilt position and required additional positions."""
963  return replace(self._shade_shade.open_position_tilt, velocity=self.positionspositions.velocity)
964 
965  @property
966  def close_tilt_position(self) -> ShadePosition:
967  """Return the open tilt position and required additional positions."""
968  return replace(
969  self._shade_shade.close_position_tilt, velocity=self.positionspositions.velocity
970  )
971 
972 
973 TYPE_TO_CLASSES = {
974  0: (PowerViewShade,),
975  1: (PowerViewShadeWithTiltOnClosed,),
976  2: (PowerViewShadeWithTiltAnywhere,),
977  3: (PowerViewShade,),
978  4: (PowerViewShadeWithTiltAnywhere,),
979  5: (PowerViewShadeTiltOnly,),
980  6: (PowerViewShadeTopDown,),
981  7: (
982  PowerViewShadeTDBUTop,
983  PowerViewShadeTDBUBottom,
984  ),
985  8: (
986  PowerViewShadeDualOverlappedCombined,
987  PowerViewShadeDualOverlappedFront,
988  PowerViewShadeDualOverlappedRear,
989  ),
990  9: (
991  PowerViewShadeDualOverlappedCombinedTilt,
992  PowerViewShadeDualOverlappedFront,
993  PowerViewShadeDualOverlappedRear,
994  ),
995  10: (
996  PowerViewShadeDualOverlappedCombinedTilt,
997  PowerViewShadeDualOverlappedFront,
998  PowerViewShadeDualOverlappedRear,
999  ),
1000  11: (
1001  PowerViewShadeDualOverlappedCombined,
1002  PowerViewShadeDualOverlappedFront,
1003  PowerViewShadeDualOverlappedRear,
1004  ),
1005 }
1006 
1007 
1009  coordinator: PowerviewShadeUpdateCoordinator,
1010  device_info: PowerviewDeviceInfo,
1011  room_name: str,
1012  shade: BaseShade,
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,)
1018  )
1019  _LOGGER.debug(
1020  "%s %s (%s) detected as %a %s",
1021  room_name,
1022  shade.name,
1023  shade.capability.type,
1024  classes,
1025  shade.raw_data,
1026  )
1027  return [
1028  cls(coordinator, device_info, room_name, shade, name_before_refresh)
1029  for cls in classes
1030  ]
None async_stop_cover(self, **Any kwargs)
Definition: __init__.py:438
ShadePosition _get_shade_move(self, int target_hass_position)
Definition: cover.py:201
None _async_set_cover_position(self, int target_hass_position)
Definition: cover.py:218
None __init__(self, PowerviewShadeUpdateCoordinator coordinator, PowerviewDeviceInfo device_info, str room_name, BaseShade shade, str name)
Definition: cover.py:109
None __init__(self, PowerviewShadeUpdateCoordinator coordinator, PowerviewDeviceInfo device_info, str room_name, BaseShade shade, str name)
Definition: cover.py:926
None __init__(self, PowerviewShadeUpdateCoordinator coordinator, PowerviewDeviceInfo device_info, str room_name, BaseShade shade, str name)
Definition: cover.py:740
None __init__(self, PowerviewShadeUpdateCoordinator coordinator, PowerviewDeviceInfo device_info, str room_name, BaseShade shade, str name)
Definition: cover.py:806
None __init__(self, PowerviewShadeUpdateCoordinator coordinator, PowerviewDeviceInfo device_info, str room_name, BaseShade shade, str name)
Definition: cover.py:862
None __init__(self, PowerviewShadeUpdateCoordinator coordinator, PowerviewDeviceInfo device_info, str room_name, BaseShade shade, str name)
Definition: cover.py:595
ShadePosition _get_shade_move(self, int target_hass_position)
Definition: cover.py:606
ShadePosition _get_shade_move(self, int target_hass_position)
Definition: cover.py:675
None __init__(self, PowerviewShadeUpdateCoordinator coordinator, PowerviewDeviceInfo device_info, str room_name, BaseShade shade, str name)
Definition: cover.py:632
None __init__(self, PowerviewShadeUpdateCoordinator coordinator, PowerviewDeviceInfo device_info, str room_name, BaseShade shade, str name)
Definition: cover.py:508
ShadePosition _get_shade_tilt(self, int target_hass_tilt_position)
Definition: cover.py:484
None __init__(self, PowerviewShadeUpdateCoordinator coordinator, PowerviewDeviceInfo device_info, str room_name, BaseShade shade, str name)
Definition: cover.py:349
None _async_set_cover_tilt_position(self, int target_hass_tilt_position)
Definition: cover.py:401
ShadePosition _get_shade_move(self, int target_hass_position)
Definition: cover.py:411
ShadePosition _get_shade_tilt(self, int target_hass_tilt_position)
Definition: cover.py:419
None update_shade_position(self, int shade_id, ShadePosition new_position)
Definition: shade_data.py:70
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
str|UndefinedType|None name(self)
Definition: entity.py:738
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)
Definition: cover.py:54
Iterable[ShadeEntity] create_powerview_shade_entity(PowerviewShadeUpdateCoordinator coordinator, PowerviewDeviceInfo device_info, str room_name, BaseShade shade, str name_before_refresh)
Definition: cover.py:1014
bool is_supported(str name, ViCareRequiredKeysMixin entity_description, vicare_device)
Definition: utils.py:56
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)
Definition: event.py:1597