Home Assistant Unofficial Reference 2024.12.1
entity_component.py
Go to the documentation of this file.
1 """Helpers for components that manage entities."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Callable, Iterable
7 from datetime import timedelta
8 import logging
9 from types import ModuleType
10 from typing import Any, Generic
11 
12 from typing_extensions import TypeVar
13 
14 from homeassistant import config as conf_util
15 from homeassistant.config_entries import ConfigEntry
16 from homeassistant.const import (
17  CONF_ENTITY_NAMESPACE,
18  CONF_SCAN_INTERVAL,
19  EVENT_HOMEASSISTANT_STOP,
20 )
21 from homeassistant.core import (
22  Event,
23  HassJob,
24  HassJobType,
25  HomeAssistant,
26  ServiceCall,
27  ServiceResponse,
28  SupportsResponse,
29  callback,
30 )
31 from homeassistant.exceptions import HomeAssistantError
32 from homeassistant.loader import async_get_integration, bind_hass
33 from homeassistant.setup import async_prepare_setup_platform
34 
35 from . import config_validation as cv, discovery, entity, service
36 from .entity_platform import EntityPlatform
37 from .typing import ConfigType, DiscoveryInfoType, VolDictType, VolSchemaType
38 
39 DEFAULT_SCAN_INTERVAL = timedelta(seconds=15)
40 DATA_INSTANCES = "entity_components"
41 
42 _EntityT = TypeVar("_EntityT", bound=entity.Entity, default=entity.Entity)
43 
44 
45 @bind_hass
46 async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None:
47  """Trigger an update for an entity."""
48  domain = entity_id.partition(".")[0]
49  entity_comp: EntityComponent[entity.Entity] | None
50  entity_comp = hass.data.get(DATA_INSTANCES, {}).get(domain)
51 
52  if entity_comp is None:
53  logging.getLogger(__name__).warning(
54  "Forced update failed. Component for %s not loaded.", entity_id
55  )
56  return
57 
58  if (entity_obj := entity_comp.get_entity(entity_id)) is None:
59  logging.getLogger(__name__).warning(
60  "Forced update failed. Entity %s not found.", entity_id
61  )
62  return
63 
64  await entity_obj.async_update_ha_state(True)
65 
66 
67 class EntityComponent(Generic[_EntityT]):
68  """The EntityComponent manages platforms that manage entities.
69 
70  An example of an entity component is 'light', which manages platforms such
71  as 'hue.light'.
72 
73  This class has the following responsibilities:
74  - Process the configuration and set up a platform based component, for example light.
75  - Manage the platforms and their entities.
76  - Help extract the entities from a service call.
77  - Listen for discovery events for platforms related to the domain.
78  """
79 
80  def __init__(
81  self,
82  logger: logging.Logger,
83  domain: str,
84  hass: HomeAssistant,
85  scan_interval: timedelta = DEFAULT_SCAN_INTERVAL,
86  ) -> None:
87  """Initialize an entity component."""
88  self.loggerlogger = logger
89  self.hasshass = hass
90  self.domaindomain = domain
91  self.scan_intervalscan_interval = scan_interval
92 
93  self.configconfig: ConfigType | None = None
94 
95  domain_platform = self._async_init_entity_platform_async_init_entity_platform(domain, None)
96  self._platforms_platforms: dict[
97  str | tuple[str, timedelta | None, str | None], EntityPlatform
98  ] = {domain: domain_platform}
99  self.async_add_entitiesasync_add_entities = domain_platform.async_add_entities
100  self.add_entitiesadd_entities = domain_platform.add_entities
101  self._entities: dict[str, entity.Entity] = domain_platform.domain_entities
102  hass.data.setdefault(DATA_INSTANCES, {})[domain] = self
103 
104  @property
105  def entities(self) -> Iterable[_EntityT]:
106  """Return an iterable that returns all entities.
107 
108  As the underlying dicts may change when async context is lost,
109  callers that iterate over this asynchronously should make a copy
110  using list() before iterating.
111  """
112  return self._entities.values() # type: ignore[return-value]
113 
114  def get_entity(self, entity_id: str) -> _EntityT | None:
115  """Get an entity."""
116  return self._entities.get(entity_id) # type: ignore[return-value]
117 
118  def register_shutdown(self) -> None:
119  """Register shutdown on Home Assistant STOP event.
120 
121  Note: this is only required if the integration never calls
122  `setup` or `async_setup`.
123  """
124  self.hasshass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown_async_shutdown)
125 
126  def setup(self, config: ConfigType) -> None:
127  """Set up a full entity component.
128 
129  This doesn't block the executor to protect from deadlocks.
130  """
131  self.hasshass.create_task(
132  self.async_setupasync_setup(config), f"EntityComponent setup {self.domain}"
133  )
134 
135  async def async_setup(self, config: ConfigType) -> None:
136  """Set up a full entity component.
137 
138  Loads the platforms from the config and will listen for supported
139  discovered platforms.
140 
141  This method must be run in the event loop.
142  """
143  self.register_shutdownregister_shutdown()
144 
145  self.configconfig = config
146 
147  # Look in config for Domain, Domain 2, Domain 3 etc and load them
148  for p_type, p_config in conf_util.config_per_platform(config, self.domaindomain):
149  if p_type is not None:
150  self.hasshass.async_create_task_internal(
151  self.async_setup_platformasync_setup_platform(p_type, p_config),
152  f"EntityComponent setup platform {p_type} {self.domain}",
153  eager_start=True,
154  )
155 
156  # Generic discovery listener for loading platform dynamically
157  # Refer to: homeassistant.helpers.discovery.async_load_platform()
158  discovery.async_listen_platform(
159  self.hasshass, self.domaindomain, self._async_component_platform_discovered_async_component_platform_discovered
160  )
161 
163  self, platform: str, info: dict[str, Any] | None
164  ) -> None:
165  """Handle the loading of a platform."""
166  await self.async_setup_platformasync_setup_platform(platform, {}, info)
167 
168  async def async_setup_entry(self, config_entry: ConfigEntry) -> bool:
169  """Set up a config entry."""
170  platform_type = config_entry.domain
171  platform = await async_prepare_setup_platform(
172  self.hasshass,
173  # In future PR we should make hass_config part of the constructor
174  # params.
175  self.configconfig or {},
176  self.domaindomain,
177  platform_type,
178  )
179 
180  if platform is None:
181  return False
182 
183  key = config_entry.entry_id
184 
185  if key in self._platforms_platforms:
186  raise ValueError(
187  f"Config entry {config_entry.title} ({key}) for "
188  f"{platform_type}.{self.domain} has already been setup!"
189  )
190 
191  self._platforms_platforms[key] = self._async_init_entity_platform_async_init_entity_platform(
192  platform_type,
193  platform,
194  scan_interval=getattr(platform, "SCAN_INTERVAL", None),
195  )
196 
197  return await self._platforms_platforms[key].async_setup_entry(config_entry)
198 
199  async def async_unload_entry(self, config_entry: ConfigEntry) -> bool:
200  """Unload a config entry."""
201  key = config_entry.entry_id
202 
203  if (platform := self._platforms_platforms.pop(key, None)) is None:
204  raise ValueError("Config entry was never loaded!")
205 
206  await platform.async_reset()
207  return True
208 
210  self, service_call: ServiceCall, expand_group: bool = True
211  ) -> list[_EntityT]:
212  """Extract all known and available entities from a service call.
213 
214  Will return an empty list if entities specified but unknown.
215 
216  This method must be run in the event loop.
217  """
218  return await service.async_extract_entities(
219  self.hasshass, self.entitiesentities, service_call, expand_group
220  )
221 
222  @callback
224  self,
225  name: str,
226  schema: VolDictType | VolSchemaType,
227  func: str | Callable[..., Any],
228  required_features: list[int] | None = None,
229  supports_response: SupportsResponse = SupportsResponse.NONE,
230  ) -> None:
231  """Register an entity service with a legacy response format."""
232  if isinstance(schema, dict):
233  schema = cv.make_entity_service_schema(schema)
234 
235  service_func: str | HassJob[..., Any]
236  service_func = func if isinstance(func, str) else HassJob(func)
237 
238  async def handle_service(
239  call: ServiceCall,
240  ) -> ServiceResponse:
241  """Handle the service."""
242 
243  result = await service.entity_service_call(
244  self.hasshass, self._entities, service_func, call, required_features
245  )
246 
247  if result:
248  if len(result) > 1:
249  raise HomeAssistantError(
250  "Deprecated service call matched more than one entity"
251  )
252  return result.popitem()[1]
253  return None
254 
255  self.hasshass.services.async_register(
256  self.domaindomain, name, handle_service, schema, supports_response
257  )
258 
259  @callback
261  self,
262  name: str,
263  schema: VolDictType | VolSchemaType | None,
264  func: str | Callable[..., Any],
265  required_features: list[int] | None = None,
266  supports_response: SupportsResponse = SupportsResponse.NONE,
267  ) -> None:
268  """Register an entity service."""
269  service.async_register_entity_service(
270  self.hasshass,
271  self.domaindomain,
272  name,
273  entities=self._entities,
274  func=func,
275  job_type=HassJobType.Coroutinefunction,
276  required_features=required_features,
277  schema=schema,
278  supports_response=supports_response,
279  )
280 
282  self,
283  platform_type: str,
284  platform_config: ConfigType,
285  discovery_info: DiscoveryInfoType | None = None,
286  ) -> None:
287  """Set up a platform for this component."""
288  if self.configconfig is None:
289  raise RuntimeError("async_setup needs to be called first")
290 
291  platform = await async_prepare_setup_platform(
292  self.hasshass, self.configconfig, self.domaindomain, platform_type
293  )
294 
295  if platform is None:
296  return
297 
298  # Use config scan interval, fallback to platform if none set
299  scan_interval = platform_config.get(
300  CONF_SCAN_INTERVAL, getattr(platform, "SCAN_INTERVAL", None)
301  )
302  entity_namespace = platform_config.get(CONF_ENTITY_NAMESPACE)
303 
304  key = (platform_type, scan_interval, entity_namespace)
305 
306  if key not in self._platforms_platforms:
307  self._platforms_platforms[key] = self._async_init_entity_platform_async_init_entity_platform(
308  platform_type, platform, scan_interval, entity_namespace
309  )
310 
311  await self._platforms_platforms[key].async_setup(platform_config, discovery_info)
312 
313  async def _async_reset(self) -> None:
314  """Remove entities and reset the entity component to initial values.
315 
316  This method must be run in the event loop.
317  """
318  tasks = []
319 
320  for key, platform in self._platforms_platforms.items():
321  if key == self.domaindomain:
322  tasks.append(platform.async_reset())
323  else:
324  tasks.append(platform.async_destroy())
325 
326  if tasks:
327  await asyncio.gather(*tasks)
328 
329  self._platforms_platforms = {self.domaindomain: self._platforms_platforms[self.domaindomain]}
330  self.configconfig = None
331 
332  async def async_remove_entity(self, entity_id: str) -> None:
333  """Remove an entity managed by one of the platforms."""
334  found = None
335 
336  for platform in self._platforms_platforms.values():
337  if entity_id in platform.entities:
338  found = platform
339  break
340 
341  if found:
342  await found.async_remove_entity(entity_id)
343 
345  self, *, skip_reset: bool = False
346  ) -> ConfigType | None:
347  """Prepare reloading this entity component.
348 
349  This method must be run in the event loop.
350  """
351  try:
352  conf = await conf_util.async_hass_config_yaml(self.hasshass)
353  except HomeAssistantError as err:
354  self.loggerlogger.error(err)
355  return None
356 
357  integration = await async_get_integration(self.hasshass, self.domaindomain)
358 
359  processed_conf = await conf_util.async_process_component_and_handle_errors(
360  self.hasshass, conf, integration
361  )
362 
363  if processed_conf is None:
364  return None
365 
366  if not skip_reset:
367  await self._async_reset_async_reset()
368 
369  return processed_conf
370 
371  @callback
373  self,
374  platform_type: str,
375  platform: ModuleType | None,
376  scan_interval: timedelta | None = None,
377  entity_namespace: str | None = None,
378  ) -> EntityPlatform:
379  """Initialize an entity platform."""
380  if scan_interval is None:
381  scan_interval = self.scan_intervalscan_interval
382 
383  entity_platform = EntityPlatform(
384  hass=self.hasshass,
385  logger=self.loggerlogger,
386  domain=self.domaindomain,
387  platform_name=platform_type,
388  platform=platform,
389  scan_interval=scan_interval,
390  entity_namespace=entity_namespace,
391  )
392  entity_platform.async_prepare()
393  return entity_platform
394 
395  @callback
396  def _async_shutdown(self, event: Event) -> None:
397  """Call when Home Assistant is stopping."""
398  for platform in self._platforms_platforms.values():
399  platform.async_shutdown()
list[_EntityT] async_extract_from_service(self, ServiceCall service_call, bool expand_group=True)
EntityPlatform _async_init_entity_platform(self, str platform_type, ModuleType|None platform, timedelta|None scan_interval=None, str|None entity_namespace=None)
None __init__(self, logging.Logger logger, str domain, HomeAssistant hass, timedelta scan_interval=DEFAULT_SCAN_INTERVAL)
None async_setup_platform(self, str platform_type, ConfigType platform_config, DiscoveryInfoType|None discovery_info=None)
None _async_component_platform_discovered(self, str platform, dict[str, Any]|None info)
bool async_unload_entry(self, ConfigEntry config_entry)
ConfigType|None async_prepare_reload(self, *bool skip_reset=False)
bool async_setup_entry(self, ConfigEntry config_entry)
None async_register_legacy_entity_service(self, str name, VolDictType|VolSchemaType schema, str|Callable[..., Any] func, list[int]|None required_features=None, SupportsResponse supports_response=SupportsResponse.NONE)
None async_register_entity_service(self, str name, VolDictType|VolSchemaType|None schema, str|Callable[..., Any] func, list[int]|None required_features=None, SupportsResponse supports_response=SupportsResponse.NONE)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_update_entity(HomeAssistant hass, str entity_id)
Integration async_get_integration(HomeAssistant hass, str domain)
Definition: loader.py:1354
ModuleType|None async_prepare_setup_platform(core.HomeAssistant hass, ConfigType hass_config, str domain, str platform_name)
Definition: setup.py:487