Home Assistant Unofficial Reference 2024.12.1
cover.py
Go to the documentation of this file.
1 """Support for Homekit covers."""
2 
3 from __future__ import annotations
4 
5 from typing import Any
6 
7 from aiohomekit.model.characteristics import CharacteristicsTypes
8 from aiohomekit.model.services import Service, ServicesTypes
9 from propcache import cached_property
10 
12  ATTR_POSITION,
13  ATTR_TILT_POSITION,
14  CoverDeviceClass,
15  CoverEntity,
16  CoverEntityFeature,
17  CoverState,
18 )
19 from homeassistant.config_entries import ConfigEntry
20 from homeassistant.const import Platform
21 from homeassistant.core import HomeAssistant, callback
22 from homeassistant.helpers.entity_platform import AddEntitiesCallback
23 
24 from . import KNOWN_DEVICES
25 from .connection import HKDevice
26 from .entity import HomeKitEntity
27 
28 STATE_STOPPED = "stopped"
29 
30 CURRENT_GARAGE_STATE_MAP = {
31  0: CoverState.OPEN,
32  1: CoverState.CLOSED,
33  2: CoverState.OPENING,
34  3: CoverState.CLOSING,
35  4: STATE_STOPPED,
36 }
37 
38 TARGET_GARAGE_STATE_MAP = {
39  CoverState.OPEN: 0,
40  CoverState.CLOSED: 1,
41  STATE_STOPPED: 2,
42 }
43 
44 CURRENT_WINDOW_STATE_MAP = {
45  0: CoverState.CLOSING,
46  1: CoverState.OPENING,
47  2: STATE_STOPPED,
48 }
49 
50 
52  hass: HomeAssistant,
53  config_entry: ConfigEntry,
54  async_add_entities: AddEntitiesCallback,
55 ) -> None:
56  """Set up Homekit covers."""
57  hkid: str = config_entry.data["AccessoryPairingID"]
58  conn: HKDevice = hass.data[KNOWN_DEVICES][hkid]
59 
60  @callback
61  def async_add_service(service: Service) -> bool:
62  if not (entity_class := ENTITY_TYPES.get(service.type)):
63  return False
64  info = {"aid": service.accessory.aid, "iid": service.iid}
65  entity: HomeKitEntity = entity_class(conn, info)
66  conn.async_migrate_unique_id(
67  entity.old_unique_id, entity.unique_id, Platform.COVER
68  )
69  async_add_entities([entity])
70  return True
71 
72  conn.add_listener(async_add_service)
73 
74 
76  """Representation of a HomeKit Garage Door."""
77 
78  _attr_device_class = CoverDeviceClass.GARAGE
79  _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
80 
81  def get_characteristic_types(self) -> list[str]:
82  """Define the homekit characteristics the entity cares about."""
83  return [
84  CharacteristicsTypes.DOOR_STATE_CURRENT,
85  CharacteristicsTypes.DOOR_STATE_TARGET,
86  CharacteristicsTypes.OBSTRUCTION_DETECTED,
87  ]
88 
89  @property
90  def _state(self) -> str:
91  """Return the current state of the garage door."""
92  value = self.serviceservice.value(CharacteristicsTypes.DOOR_STATE_CURRENT)
93  return CURRENT_GARAGE_STATE_MAP[value]
94 
95  @property
96  def is_closed(self) -> bool:
97  """Return true if cover is closed, else False."""
98  return self._state_state_state == CoverState.CLOSED
99 
100  @property
101  def is_closing(self) -> bool:
102  """Return if the cover is closing or not."""
103  return self._state_state_state == CoverState.CLOSING
104 
105  @property
106  def is_opening(self) -> bool:
107  """Return if the cover is opening or not."""
108  return self._state_state_state == CoverState.OPENING
109 
110  async def async_open_cover(self, **kwargs: Any) -> None:
111  """Send open command."""
112  await self.set_door_stateset_door_state(CoverState.OPEN)
113 
114  async def async_close_cover(self, **kwargs: Any) -> None:
115  """Send close command."""
116  await self.set_door_stateset_door_state(CoverState.CLOSED)
117 
118  async def set_door_state(self, state: str) -> None:
119  """Send state command."""
120  await self.async_put_characteristicsasync_put_characteristics(
121  {CharacteristicsTypes.DOOR_STATE_TARGET: TARGET_GARAGE_STATE_MAP[state]}
122  )
123 
124  @property
125  def extra_state_attributes(self) -> dict[str, Any]:
126  """Return the optional state attributes."""
127  obstruction_detected = self.serviceservice.value(
128  CharacteristicsTypes.OBSTRUCTION_DETECTED
129  )
130  return {"obstruction-detected": obstruction_detected is True}
131 
132 
134  """Representation of a HomeKit Window or Window Covering."""
135 
136  @callback
137  def _async_reconfigure(self) -> None:
138  """Reconfigure entity."""
139  self._async_clear_property_cache_async_clear_property_cache(("supported_features",))
140  super()._async_reconfigure()
141 
142  def get_characteristic_types(self) -> list[str]:
143  """Define the homekit characteristics the entity cares about."""
144  return [
145  CharacteristicsTypes.POSITION_STATE,
146  CharacteristicsTypes.POSITION_CURRENT,
147  CharacteristicsTypes.POSITION_TARGET,
148  CharacteristicsTypes.POSITION_HOLD,
149  CharacteristicsTypes.VERTICAL_TILT_CURRENT,
150  CharacteristicsTypes.VERTICAL_TILT_TARGET,
151  CharacteristicsTypes.HORIZONTAL_TILT_CURRENT,
152  CharacteristicsTypes.HORIZONTAL_TILT_TARGET,
153  CharacteristicsTypes.OBSTRUCTION_DETECTED,
154  ]
155 
156  @cached_property
157  def supported_features(self) -> CoverEntityFeature:
158  """Flag supported features."""
159  features = (
160  CoverEntityFeature.OPEN
161  | CoverEntityFeature.CLOSE
162  | CoverEntityFeature.SET_POSITION
163  )
164 
165  if self.serviceservice.has(CharacteristicsTypes.POSITION_HOLD):
166  features |= CoverEntityFeature.STOP
167 
168  if self.serviceservice.has(
169  CharacteristicsTypes.VERTICAL_TILT_CURRENT
170  ) or self.serviceservice.has(CharacteristicsTypes.HORIZONTAL_TILT_CURRENT):
171  features |= (
172  CoverEntityFeature.OPEN_TILT
173  | CoverEntityFeature.CLOSE_TILT
174  | CoverEntityFeature.SET_TILT_POSITION
175  )
176 
177  return features
178 
179  @property
180  def current_cover_position(self) -> int:
181  """Return the current position of cover."""
182  return self.serviceservice.value(CharacteristicsTypes.POSITION_CURRENT)
183 
184  @property
185  def is_closed(self) -> bool:
186  """Return true if cover is closed, else False."""
188 
189  @property
190  def is_closing(self) -> bool:
191  """Return if the cover is closing or not."""
192  value = self.serviceservice.value(CharacteristicsTypes.POSITION_STATE)
193  state = CURRENT_WINDOW_STATE_MAP[value]
194  return state == CoverState.CLOSING
195 
196  @property
197  def is_opening(self) -> bool:
198  """Return if the cover is opening or not."""
199  value = self.serviceservice.value(CharacteristicsTypes.POSITION_STATE)
200  state = CURRENT_WINDOW_STATE_MAP[value]
201  return state == CoverState.OPENING
202 
203  @property
204  def is_horizontal_tilt(self) -> bool:
205  """Return True if the service has a horizontal tilt characteristic."""
206  return (
207  self.serviceservice.value(CharacteristicsTypes.HORIZONTAL_TILT_CURRENT) is not None
208  )
209 
210  @property
211  def is_vertical_tilt(self) -> bool:
212  """Return True if the service has a vertical tilt characteristic."""
213  return (
214  self.serviceservice.value(CharacteristicsTypes.VERTICAL_TILT_CURRENT) is not None
215  )
216 
217  @property
218  def current_cover_tilt_position(self) -> int | None:
219  """Return current position of cover tilt."""
220  if self.is_vertical_tiltis_vertical_tilt:
221  char = self.serviceservice[CharacteristicsTypes.VERTICAL_TILT_CURRENT]
222  elif self.is_horizontal_tiltis_horizontal_tilt:
223  char = self.serviceservice[CharacteristicsTypes.HORIZONTAL_TILT_CURRENT]
224  else:
225  return None
226 
227  # Recalculate tilt_position. Convert arc to percent scale based on min/max values.
228  tilt_position = char.value
229  min_value = char.minValue
230  max_value = char.maxValue
231  total_range = int(max_value or 0) - int(min_value or 0)
232 
233  if (
234  tilt_position is None
235  or min_value is None
236  or max_value is None
237  or total_range <= 0
238  ):
239  return None
240 
241  # inverted scale
242  if min_value == -90 and max_value == 0:
243  return abs(int(100 / total_range * (tilt_position - max_value)))
244  # normal scale
245  return abs(int(100 / total_range * (tilt_position - min_value)))
246 
247  async def async_stop_cover(self, **kwargs: Any) -> None:
248  """Send hold command."""
249  await self.async_put_characteristicsasync_put_characteristics({CharacteristicsTypes.POSITION_HOLD: 1})
250 
251  async def async_open_cover(self, **kwargs: Any) -> None:
252  """Send open command."""
253  await self.async_set_cover_positionasync_set_cover_positionasync_set_cover_position(position=100)
254 
255  async def async_close_cover(self, **kwargs: Any) -> None:
256  """Send close command."""
257  await self.async_set_cover_positionasync_set_cover_positionasync_set_cover_position(position=0)
258 
259  async def async_set_cover_position(self, **kwargs: Any) -> None:
260  """Send position command."""
261  position = kwargs[ATTR_POSITION]
262  await self.async_put_characteristicsasync_put_characteristics(
263  {CharacteristicsTypes.POSITION_TARGET: position}
264  )
265 
266  async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
267  """Move the cover tilt to a specific position."""
268  tilt_position = kwargs[ATTR_TILT_POSITION]
269 
270  if self.is_vertical_tiltis_vertical_tilt:
271  char = self.serviceservice[CharacteristicsTypes.VERTICAL_TILT_TARGET]
272  elif self.is_horizontal_tiltis_horizontal_tilt:
273  char = self.serviceservice[CharacteristicsTypes.HORIZONTAL_TILT_TARGET]
274 
275  # Calculate tilt_position. Convert from 1-100 scale to arc degree scale respecting possible min/max Values.
276  min_value = char.minValue
277  max_value = char.maxValue
278  if min_value is None or max_value is None:
279  raise ValueError(
280  "Entity does not provide minValue and maxValue for the tilt"
281  )
282 
283  # inverted scale
284  if min_value == -90 and max_value == 0:
285  tilt_position = int(
286  tilt_position / 100 * (min_value - max_value) + max_value
287  )
288  else:
289  tilt_position = int(
290  tilt_position / 100 * (max_value - min_value) + min_value
291  )
292 
293  await self.async_put_characteristicsasync_put_characteristics({char.type: tilt_position})
294 
295  @property
296  def extra_state_attributes(self) -> dict[str, Any]:
297  """Return the optional state attributes."""
298  obstruction_detected = self.serviceservice.value(
299  CharacteristicsTypes.OBSTRUCTION_DETECTED
300  )
301  if not obstruction_detected:
302  return {}
303  return {"obstruction-detected": obstruction_detected}
304 
305 
307  """Representation of a HomeKit Window."""
308 
309  _attr_device_class = CoverDeviceClass.WINDOW
310 
311 
312 ENTITY_TYPES = {
313  ServicesTypes.GARAGE_DOOR_OPENER: HomeKitGarageDoorCover,
314  ServicesTypes.WINDOW_COVERING: HomeKitWindowCover,
315  ServicesTypes.WINDOW: HomeKitWindow,
316 }
None async_set_cover_position(self, **Any kwargs)
Definition: __init__.py:429
None async_put_characteristics(self, dict[str, Any] characteristics)
Definition: entity.py:125
None _async_clear_property_cache(self, tuple[str,...] properties)
Definition: entity.py:79
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: cover.py:55