Home Assistant Unofficial Reference 2024.12.1
entity.py
Go to the documentation of this file.
1 """Homekit Controller entities."""
2 
3 from __future__ import annotations
4 
5 from typing import Any
6 
7 from aiohomekit.model.characteristics import (
8  EVENT_CHARACTERISTICS,
9  Characteristic,
10  CharacteristicPermissions,
11  CharacteristicsTypes,
12 )
13 from aiohomekit.model.services import Service, ServicesTypes
14 
15 from homeassistant.core import CALLBACK_TYPE, callback
16 from homeassistant.helpers.device_registry import DeviceInfo
17 from homeassistant.helpers.entity import Entity
18 from homeassistant.helpers.typing import ConfigType
19 
20 from .connection import HKDevice, valid_serial_number
21 from .utils import folded_name
22 
23 
25  """Representation of a Home Assistant HomeKit device."""
26 
27  _attr_should_poll = False
28  pollable_characteristics: list[tuple[int, int]]
29  watchable_characteristics: list[tuple[int, int]]
30  all_characteristics: set[tuple[int, int]]
31  all_iids: set[int]
32  accessory_info: Service
33 
34  def __init__(self, accessory: HKDevice, devinfo: ConfigType) -> None:
35  """Initialise a generic HomeKit device."""
36  self._accessory_accessory = accessory
37  self._aid: int = devinfo["aid"]
38  self._iid: int = devinfo["iid"]
39  self._entity_key: tuple[int, int | None, int | None] = (
40  self._aid,
41  None,
42  self._iid,
43  )
44  self._char_name_char_name: str | None = None
45  self._char_subscription_char_subscription: CALLBACK_TYPE | None = None
46  self.async_setupasync_setup()
47  self._attr_unique_id_attr_unique_id = f"{accessory.unique_id}_{self._aid}_{self._iid}"
48  super().__init__()
49 
50  @callback
51  def _async_handle_entity_removed(self) -> None:
52  """Handle entity removal."""
53  # We call _async_unsubscribe_chars as soon as we
54  # know the entity is about to be removed so we do not try to
55  # update characteristics that no longer exist. It will get
56  # called in async_will_remove_from_hass as well, but that is
57  # too late.
58  self._async_unsubscribe_chars_async_unsubscribe_chars()
59  self.hasshass.async_create_task(self.async_removeasync_remove(force_remove=True))
60 
61  @callback
63  """Handle accessory or service disappearance."""
64  entity_map = self._accessory_accessory.entity_map
65  if not (
66  accessory := entity_map.aid_or_none(self._aid)
67  ) or not accessory.services.iid_or_none(self._iid):
68  self._async_handle_entity_removed_async_handle_entity_removed()
69  return True
70  return False
71 
72  @callback
73  def _async_config_changed(self) -> None:
74  """Handle accessory discovery changes."""
75  if not self._async_remove_entity_if_accessory_or_service_disappeared_async_remove_entity_if_accessory_or_service_disappeared():
76  self._async_reconfigure_async_reconfigure()
77 
78  @callback
79  def _async_clear_property_cache(self, properties: tuple[str, ...]) -> None:
80  """Clear the cache of properties."""
81  for prop in properties:
82  self.__dict__.pop(prop, None)
83 
84  @callback
85  def _async_reconfigure(self) -> None:
86  """Reconfigure the entity."""
87  self._async_unsubscribe_chars_async_unsubscribe_chars()
88  self.async_setupasync_setup()
89  self._async_subscribe_chars_async_subscribe_chars()
90  self.async_write_ha_stateasync_write_ha_state()
91 
92  async def async_added_to_hass(self) -> None:
93  """Entity added to hass."""
94  self._async_subscribe_chars_async_subscribe_chars()
95  self.async_on_removeasync_on_remove(
96  self._accessory_accessory.async_subscribe_config_changed(self._async_config_changed_async_config_changed)
97  )
98  self.async_on_removeasync_on_remove(
99  self._accessory_accessory.async_subscribe_availability(self._async_write_ha_state_async_write_ha_state)
100  )
101 
102  async def async_will_remove_from_hass(self) -> None:
103  """Prepare to be removed from hass."""
104  self._async_unsubscribe_chars_async_unsubscribe_chars()
105  self._accessory_accessory.async_entity_key_removed(self._entity_key)
106 
107  @callback
108  def _async_unsubscribe_chars(self) -> None:
109  """Handle unsubscribing from characteristics."""
110  if self._char_subscription_char_subscription:
111  self._char_subscription_char_subscription()
112  self._char_subscription_char_subscription = None
113  self._accessory_accessory.remove_pollable_characteristics(self.pollable_characteristicspollable_characteristics)
114  self._accessory_accessory.remove_watchable_characteristics(self.watchable_characteristicswatchable_characteristics)
115 
116  @callback
117  def _async_subscribe_chars(self) -> None:
118  """Handle registering characteristics to watch and subscribe."""
119  self._accessory_accessory.add_pollable_characteristics(self.pollable_characteristicspollable_characteristics)
120  self._accessory_accessory.add_watchable_characteristics(self.watchable_characteristicswatchable_characteristics)
121  self._char_subscription_char_subscription = self._accessory_accessory.async_subscribe(
122  self.all_characteristicsall_characteristics, self._async_write_ha_state_async_write_ha_state
123  )
124 
125  async def async_put_characteristics(self, characteristics: dict[str, Any]) -> None:
126  """Write characteristics to the device.
127 
128  A characteristic type is unique within a service, but in order to write
129  to a named characteristic on a bridge we need to turn its type into
130  an aid and iid, and send it as a list of tuples, which is what this
131  helper does.
132 
133  E.g. you can do:
134 
135  await entity.async_put_characteristics({
136  CharacteristicsTypes.ON: True
137  })
138  """
139  payload = self.serviceservice.build_update(characteristics)
140  return await self._accessory_accessory.put_characteristics(payload)
141 
142  @callback
143  def async_setup(self) -> None:
144  """Configure an entity based on its HomeKit characteristics metadata."""
145  accessory = self._accessory_accessory
146  self.accessoryaccessory = accessory.entity_map.aid(self._aid)
147  self.serviceservice = self.accessoryaccessory.services.iid(self._iid)
148  accessory_info = self.accessoryaccessory.services.first(
149  service_type=ServicesTypes.ACCESSORY_INFORMATION
150  )
151  assert accessory_info
152  self.accessory_infoaccessory_info = accessory_info
153  # If we re-setup, we need to make sure we make new
154  # lists since we passed them to the connection before
155  # and we do not want to inadvertently modify the old
156  # ones.
157  self.pollable_characteristicspollable_characteristics = []
158  self.watchable_characteristicswatchable_characteristics = []
159  self.all_characteristicsall_characteristics = set()
160  self.all_iidsall_iids = set()
161 
162  char_types = self.get_characteristic_typesget_characteristic_types()
163 
164  # Setup events and/or polling for characteristics directly attached to this entity
165  for char in self.serviceservice.characteristics.filter(char_types=char_types):
166  self._setup_characteristic_setup_characteristic(char)
167 
168  # Setup events and/or polling for characteristics attached to sub-services of this
169  # entity (like an INPUT_SOURCE).
170  for service in self.accessoryaccessory.services.filter(parent_service=self.serviceservice):
171  for char in service.characteristics.filter(char_types=char_types):
172  self._setup_characteristic_setup_characteristic(char)
173 
174  self.all_characteristicsall_characteristics.update(self.pollable_characteristicspollable_characteristics)
175  self.all_characteristicsall_characteristics.update(self.watchable_characteristicswatchable_characteristics)
176  self.all_iidsall_iids = {iid for _, iid in self.all_characteristicsall_characteristics}
177 
178  def _setup_characteristic(self, char: Characteristic) -> None:
179  """Configure an entity based on a HomeKit characteristics metadata."""
180  # Build up a list of (aid, iid) tuples to poll on update()
181  if (
182  CharacteristicPermissions.paired_read in char.perms
183  and char.type not in EVENT_CHARACTERISTICS
184  ):
185  self.pollable_characteristicspollable_characteristics.append((self._aid, char.iid))
186 
187  # Build up a list of (aid, iid) tuples to subscribe to
188  if CharacteristicPermissions.events in char.perms:
189  self.watchable_characteristicswatchable_characteristics.append((self._aid, char.iid))
190 
191  if self._char_name_char_name is None:
192  self._char_name_char_name = char.service.value(CharacteristicsTypes.NAME)
193 
194  @property
195  def old_unique_id(self) -> str:
196  """Return the OLD ID of this device."""
197  info = self.accessory_infoaccessory_info
198  serial = info.value(CharacteristicsTypes.SERIAL_NUMBER)
199  if valid_serial_number(serial):
200  return f"homekit-{serial}-{self._iid}"
201  # Some accessories do not have a serial number
202  return f"homekit-{self._accessory.unique_id}-{self._aid}-{self._iid}"
203 
204  @property
205  def default_name(self) -> str | None:
206  """Return the default name of the device."""
207  return None
208 
209  @property
210  def name(self) -> str | None:
211  """Return the name of the device if any."""
212  accessory_name = self.accessoryaccessory.name
213  # If the service has a name char, use that, if not
214  # fallback to the default name provided by the subclass
215  if device_name := self._char_name_char_name or self.default_namedefault_name:
216  folded_device_name = folded_name(device_name)
217  folded_accessory_name = folded_name(accessory_name)
218  # Sometimes the device name includes the accessory
219  # name already like My ecobee Occupancy / My ecobee
220  if folded_device_name.startswith(folded_accessory_name):
221  return device_name
222  if (
223  folded_accessory_name not in folded_device_name
224  and folded_device_name not in folded_accessory_name
225  ):
226  return f"{accessory_name} {device_name}"
227  return accessory_name
228 
229  @property
230  def available(self) -> bool:
231  """Return True if entity is available."""
232  all_iids = self.all_iidsall_iids
233  for char in self.serviceservice.characteristics:
234  if char.iid in all_iids and not char.available:
235  return False
236  return self._accessory_accessory.available
237 
238  @property
239  def device_info(self) -> DeviceInfo:
240  """Return the device info."""
241  return self._accessory_accessory.device_info_for_accessory(self.accessoryaccessory)
242 
243  def get_characteristic_types(self) -> list[str]:
244  """Define the homekit characteristics the entity cares about."""
245  raise NotImplementedError
246 
247  async def async_update(self) -> None:
248  """Update the entity."""
249  await self._accessory_accessory.async_request_update()
250 
251 
253  """A HomeKit entity that is related to an entire accessory rather than a specific service or characteristic."""
254 
255  def __init__(self, accessory: HKDevice, devinfo: ConfigType) -> None:
256  """Initialise a generic HomeKit accessory."""
257  super().__init__(accessory, devinfo)
258  self._attr_unique_id_attr_unique_id_attr_unique_id = f"{accessory.unique_id}_{self._aid}"
259 
260  @property
261  def old_unique_id(self) -> str:
262  """Return the old ID of this device."""
263  serial = self.accessory_infoaccessory_info.value(CharacteristicsTypes.SERIAL_NUMBER)
264  return f"homekit-{serial}-aid:{self._aid}"
265 
266 
268  """A HomeKit entity that is related to an single characteristic rather than a whole service.
269 
270  This is typically used to expose additional sensor, binary_sensor or number entities that don't belong with
271  the service entity.
272  """
273 
274  def __init__(
275  self, accessory: HKDevice, devinfo: ConfigType, char: Characteristic
276  ) -> None:
277  """Initialise a generic single characteristic HomeKit entity."""
278  self._char_char = char
279  super().__init__(accessory, devinfo)
280  self._entity_key_entity_key = (self._aid, self._iid, char.iid)
281 
282  @callback
284  """Handle characteristic disappearance."""
285  if (
286  not self._accessory_accessory.entity_map.aid(self._aid)
287  .services.iid(self._iid)
288  .get_char_by_iid(self._char_char.iid)
289  ):
290  self._async_handle_entity_removed_async_handle_entity_removed()
291  return True
292  return False
293 
294  @callback
295  def _async_config_changed(self) -> None:
296  """Handle accessory discovery changes."""
297  if (
298  not self._async_remove_entity_if_accessory_or_service_disappeared_async_remove_entity_if_accessory_or_service_disappeared()
299  and not self._async_remove_entity_if_characteristics_disappeared_async_remove_entity_if_characteristics_disappeared()
300  ):
301  super()._async_reconfigure()
302 
303 
305  """A HomeKit entity that is related to an single characteristic rather than a whole service.
306 
307  This is typically used to expose additional sensor, binary_sensor or number entities that don't belong with
308  the service entity.
309  """
310 
311  def __init__(
312  self, accessory: HKDevice, devinfo: ConfigType, char: Characteristic
313  ) -> None:
314  """Initialise a generic single characteristic HomeKit entity."""
315  super().__init__(accessory, devinfo, char)
316  self._attr_unique_id_attr_unique_id_attr_unique_id = (
317  f"{accessory.unique_id}_{self._aid}_{char.service.iid}_{char.iid}"
318  )
319 
320  @property
321  def old_unique_id(self) -> str:
322  """Return the old ID of this device."""
323  serial = self.accessory_infoaccessory_info.value(CharacteristicsTypes.SERIAL_NUMBER)
324  return f"homekit-{serial}-aid:{self._aid}-sid:{self._char.service.iid}-cid:{self._char.iid}"
None __init__(self, HKDevice accessory, ConfigType devinfo)
Definition: entity.py:255
None __init__(self, HKDevice accessory, ConfigType devinfo, Characteristic char)
Definition: entity.py:276
None __init__(self, HKDevice accessory, ConfigType devinfo, Characteristic char)
Definition: entity.py:313
None async_put_characteristics(self, dict[str, Any] characteristics)
Definition: entity.py:125
None __init__(self, HKDevice accessory, ConfigType devinfo)
Definition: entity.py:34
None _async_clear_property_cache(self, tuple[str,...] properties)
Definition: entity.py:79
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
None async_remove(self, *bool force_remove=False)
Definition: entity.py:1387
CALLBACK_TYPE async_subscribe(HomeAssistant hass, str topic, Callable[[ReceiveMessage], Coroutine[Any, Any, None]|None] msg_callback, int qos=DEFAULT_QOS, str|None encoding=DEFAULT_ENCODING)
Definition: client.py:194