Home Assistant Unofficial Reference 2024.12.1
loader.py
Go to the documentation of this file.
1 """The methods for loading Home Assistant integrations.
2 
3 This module has quite some complex parts. I have tried to add as much
4 documentation as possible to keep it understandable.
5 """
6 
7 from __future__ import annotations
8 
9 import asyncio
10 from collections.abc import Callable, Iterable
11 from contextlib import suppress
12 from dataclasses import dataclass
13 import functools as ft
14 import importlib
15 import logging
16 import os
17 import pathlib
18 import sys
19 import time
20 from types import ModuleType
21 from typing import TYPE_CHECKING, Any, Literal, Protocol, TypedDict, cast
22 
23 from awesomeversion import (
24  AwesomeVersion,
25  AwesomeVersionException,
26  AwesomeVersionStrategy,
27 )
28 from propcache import cached_property
29 import voluptuous as vol
30 
31 from . import generated
32 from .const import Platform
33 from .core import HomeAssistant, callback
34 from .generated.application_credentials import APPLICATION_CREDENTIALS
35 from .generated.bluetooth import BLUETOOTH
36 from .generated.config_flows import FLOWS
37 from .generated.dhcp import DHCP
38 from .generated.mqtt import MQTT
39 from .generated.ssdp import SSDP
40 from .generated.usb import USB
41 from .generated.zeroconf import HOMEKIT, ZEROCONF
42 from .helpers.json import json_bytes, json_fragment
43 from .helpers.typing import UNDEFINED
44 from .util.hass_dict import HassKey
45 from .util.json import JSON_DECODE_EXCEPTIONS, json_loads
46 
47 if TYPE_CHECKING:
48  # The relative imports below are guarded by TYPE_CHECKING
49  # because they would cause a circular import otherwise.
50  from .config_entries import ConfigEntry
51  from .helpers import device_registry as dr
52  from .helpers.typing import ConfigType
53 
54 _LOGGER = logging.getLogger(__name__)
55 
56 #
57 # Integration.get_component will check preload platforms and
58 # try to import the code to avoid a thundering heard of import
59 # executor jobs later in the startup process.
60 #
61 # default platforms are prepopulated in this list to ensure that
62 # by the time the component is loaded, we check if the platform is
63 # available.
64 #
65 # This list can be extended by calling async_register_preload_platform
66 #
67 BASE_PRELOAD_PLATFORMS = [
68  "config",
69  "config_flow",
70  "diagnostics",
71  "energy",
72  "group",
73  "logbook",
74  "hardware",
75  "intent",
76  "media_source",
77  "recorder",
78  "repairs",
79  "system_health",
80  "trigger",
81 ]
82 
83 
84 @dataclass
86  """Blocked custom integration details."""
87 
88  lowest_good_version: AwesomeVersion | None
89  reason: str
90 
91 
92 BLOCKED_CUSTOM_INTEGRATIONS: dict[str, BlockedIntegration] = {
93  # Added in 2024.3.0 because of https://github.com/home-assistant/core/issues/112464
94  "start_time": BlockedIntegration(AwesomeVersion("1.1.7"), "breaks Home Assistant"),
95  # Added in 2024.5.1 because of
96  # https://community.home-assistant.io/t/psa-2024-5-upgrade-failure-and-dreame-vacuum-custom-integration/724612
97  "dreame_vacuum": BlockedIntegration(
98  AwesomeVersion("1.0.4"), "crashes Home Assistant"
99  ),
100  # Added in 2024.5.5 because of
101  # https://github.com/sh00t2kill/dolphin-robot/issues/185
102  "mydolphin_plus": BlockedIntegration(
103  AwesomeVersion("1.0.13"), "crashes Home Assistant"
104  ),
105  # Added in 2024.7.2 because of
106  # https://github.com/gcobb321/icloud3/issues/349
107  # Note: Current version 3.0.5.2, the fixed version is a guesstimate,
108  # as no solution is available at time of writing.
109  "icloud3": BlockedIntegration(
110  AwesomeVersion("3.0.5.3"), "prevents recorder from working"
111  ),
112  # Added in 2024.7.2 because of
113  # https://github.com/custom-components/places/issues/289
114  "places": BlockedIntegration(
115  AwesomeVersion("2.7.1"), "prevents recorder from working"
116  ),
117  # Added in 2024.7.2 because of
118  # https://github.com/enkama/hass-variables/issues/120
119  "variable": BlockedIntegration(
120  AwesomeVersion("3.4.4"), "prevents recorder from working"
121  ),
122 }
123 
124 DATA_COMPONENTS: HassKey[dict[str, ModuleType | ComponentProtocol]] = HassKey(
125  "components"
126 )
127 DATA_INTEGRATIONS: HassKey[dict[str, Integration | asyncio.Future[None]]] = HassKey(
128  "integrations"
129 )
130 DATA_MISSING_PLATFORMS: HassKey[dict[str, bool]] = HassKey("missing_platforms")
131 DATA_CUSTOM_COMPONENTS: HassKey[
132  dict[str, Integration] | asyncio.Future[dict[str, Integration]]
133 ] = HassKey("custom_components")
134 DATA_PRELOAD_PLATFORMS: HassKey[list[str]] = HassKey("preload_platforms")
135 PACKAGE_CUSTOM_COMPONENTS = "custom_components"
136 PACKAGE_BUILTIN = "homeassistant.components"
137 CUSTOM_WARNING = (
138  "We found a custom integration %s which has not "
139  "been tested by Home Assistant. This component might "
140  "cause stability problems, be sure to disable it if you "
141  "experience issues with Home Assistant"
142 )
143 IMPORT_EVENT_LOOP_WARNING = (
144  "We found an integration %s which is configured to "
145  "to import its code in the event loop. This component might "
146  "cause stability problems, be sure to disable it if you "
147  "experience issues with Home Assistant"
148 )
149 
150 MOVED_ZEROCONF_PROPS = ("macaddress", "model", "manufacturer")
151 
152 
153 class DHCPMatcherRequired(TypedDict, total=True):
154  """Matcher for the dhcp integration for required fields."""
155 
156  domain: str
157 
158 
159 class DHCPMatcherOptional(TypedDict, total=False):
160  """Matcher for the dhcp integration for optional fields."""
161 
162  macaddress: str
163  hostname: str
164  registered_devices: bool
165 
166 
168  """Matcher for the dhcp integration."""
169 
170 
171 class BluetoothMatcherRequired(TypedDict, total=True):
172  """Matcher for the bluetooth integration for required fields."""
173 
174  domain: str
175 
176 
177 class BluetoothMatcherOptional(TypedDict, total=False):
178  """Matcher for the bluetooth integration for optional fields."""
179 
180  local_name: str
181  service_uuid: str
182  service_data_uuid: str
183  manufacturer_id: int
184  manufacturer_data_start: list[int]
185  connectable: bool
186 
187 
189  """Matcher for the bluetooth integration."""
190 
191 
192 class USBMatcherRequired(TypedDict, total=True):
193  """Matcher for the usb integration for required fields."""
194 
195  domain: str
196 
197 
198 class USBMatcherOptional(TypedDict, total=False):
199  """Matcher for the usb integration for optional fields."""
200 
201  vid: str
202  pid: str
203  serial_number: str
204  manufacturer: str
205  description: str
206 
207 
209  """Matcher for the USB integration."""
210 
211 
212 @dataclass(slots=True)
214  """HomeKit model."""
215 
216  domain: str
217  always_discover: bool
218 
219 
220 class ZeroconfMatcher(TypedDict, total=False):
221  """Matcher for zeroconf."""
222 
223  domain: str
224  name: str
225  properties: dict[str, str]
226 
227 
228 class Manifest(TypedDict, total=False):
229  """Integration manifest.
230 
231  Note that none of the attributes are marked Optional here. However, some of
232  them may be optional in manifest.json in the sense that they can be omitted
233  altogether. But when present, they should not have null values in it.
234  """
235 
236  name: str
237  disabled: str
238  domain: str
239  integration_type: Literal[
240  "entity", "device", "hardware", "helper", "hub", "service", "system", "virtual"
241  ]
242  dependencies: list[str]
243  after_dependencies: list[str]
244  requirements: list[str]
245  config_flow: bool
246  documentation: str
247  issue_tracker: str
248  quality_scale: str
249  iot_class: str
250  bluetooth: list[dict[str, int | str]]
251  mqtt: list[str]
252  ssdp: list[dict[str, str]]
253  zeroconf: list[str | dict[str, str]]
254  dhcp: list[dict[str, bool | str]]
255  usb: list[dict[str, str]]
256  homekit: dict[str, list[str]]
257  is_built_in: bool
258  overwrites_built_in: bool
259  version: str
260  codeowners: list[str]
261  loggers: list[str]
262  import_executor: bool
263  single_config_entry: bool
264 
265 
266 def async_setup(hass: HomeAssistant) -> None:
267  """Set up the necessary data structures."""
269  hass.data[DATA_COMPONENTS] = {}
270  hass.data[DATA_INTEGRATIONS] = {}
271  hass.data[DATA_MISSING_PLATFORMS] = {}
272  hass.data[DATA_PRELOAD_PLATFORMS] = BASE_PRELOAD_PLATFORMS.copy()
273 
274 
275 def manifest_from_legacy_module(domain: str, module: ModuleType) -> Manifest:
276  """Generate a manifest from a legacy module."""
277  return {
278  "domain": domain,
279  "name": domain,
280  "requirements": getattr(module, "REQUIREMENTS", []),
281  "dependencies": getattr(module, "DEPENDENCIES", []),
282  "codeowners": [],
283  }
284 
285 
286 def _get_custom_components(hass: HomeAssistant) -> dict[str, Integration]:
287  """Return list of custom integrations."""
288  if hass.config.recovery_mode or hass.config.safe_mode:
289  return {}
290 
291  try:
292  import custom_components # pylint: disable=import-outside-toplevel
293  except ImportError:
294  return {}
295 
296  dirs = [
297  entry
298  for path in custom_components.__path__
299  for entry in pathlib.Path(path).iterdir()
300  if entry.is_dir()
301  ]
302 
303  integrations = _resolve_integrations_from_root(
304  hass,
305  custom_components,
306  [comp.name for comp in dirs],
307  )
308  return {
309  integration.domain: integration
310  for integration in integrations.values()
311  if integration is not None
312  }
313 
314 
316  hass: HomeAssistant,
317 ) -> dict[str, Integration]:
318  """Return cached list of custom integrations."""
319  comps_or_future = hass.data.get(DATA_CUSTOM_COMPONENTS)
320 
321  if comps_or_future is None:
322  future = hass.data[DATA_CUSTOM_COMPONENTS] = hass.loop.create_future()
323 
324  comps = await hass.async_add_executor_job(_get_custom_components, hass)
325 
326  hass.data[DATA_CUSTOM_COMPONENTS] = comps
327  future.set_result(comps)
328  return comps
329 
330  if isinstance(comps_or_future, asyncio.Future):
331  return await comps_or_future
332 
333  return comps_or_future
334 
335 
337  hass: HomeAssistant,
338  type_filter: Literal["device", "helper", "hub", "service"] | None = None,
339 ) -> set[str]:
340  """Return cached list of config flows."""
341  integrations = await async_get_custom_components(hass)
342  flows: set[str] = set()
343 
344  if type_filter is not None:
345  flows.update(FLOWS[type_filter])
346  else:
347  for type_flows in FLOWS.values():
348  flows.update(type_flows)
349 
350  flows.update(
351  integration.domain
352  for integration in integrations.values()
353  if integration.config_flow
354  and (type_filter is None or integration.integration_type == type_filter)
355  )
356 
357  return flows
358 
359 
360 class ComponentProtocol(Protocol):
361  """Define the format of an integration."""
362 
363  CONFIG_SCHEMA: vol.Schema
364  DOMAIN: str
365 
366  async def async_setup_entry(
367  self, hass: HomeAssistant, config_entry: ConfigEntry
368  ) -> bool:
369  """Set up a config entry."""
370 
371  async def async_unload_entry(
372  self, hass: HomeAssistant, config_entry: ConfigEntry
373  ) -> bool:
374  """Unload a config entry."""
375 
376  async def async_migrate_entry(
377  self, hass: HomeAssistant, config_entry: ConfigEntry
378  ) -> bool:
379  """Migrate an old config entry."""
380 
381  async def async_remove_entry(
382  self, hass: HomeAssistant, config_entry: ConfigEntry
383  ) -> None:
384  """Remove a config entry."""
385 
387  self,
388  hass: HomeAssistant,
389  config_entry: ConfigEntry,
390  device_entry: dr.DeviceEntry,
391  ) -> bool:
392  """Remove a config entry device."""
393 
394  async def async_reset_platform(
395  self, hass: HomeAssistant, integration_name: str
396  ) -> None:
397  """Release resources."""
398 
399  async def async_setup(self, hass: HomeAssistant, config: ConfigType) -> bool:
400  """Set up integration."""
401 
402  def setup(self, hass: HomeAssistant, config: ConfigType) -> bool:
403  """Set up integration."""
404 
405 
407  hass: HomeAssistant,
408 ) -> dict[str, Any]:
409  """Return cached list of integrations."""
410  base = generated.__path__[0]
411  config_flow_path = pathlib.Path(base) / "integrations.json"
412 
413  flow = await hass.async_add_executor_job(config_flow_path.read_text)
414  core_flows = cast(dict[str, Any], json_loads(flow))
415  custom_integrations = await async_get_custom_components(hass)
416  custom_flows: dict[str, Any] = {
417  "integration": {},
418  "helper": {},
419  }
420 
421  for integration in custom_integrations.values():
422  # Remove core integration with same domain as the custom integration
423  if integration.integration_type in ("entity", "system"):
424  continue
425 
426  for integration_type in ("integration", "helper"):
427  if integration.domain not in core_flows[integration_type]:
428  continue
429  del core_flows[integration_type][integration.domain]
430  if integration.domain in core_flows["translated_name"]:
431  core_flows["translated_name"].remove(integration.domain)
432 
433  if integration.integration_type == "helper":
434  integration_key: str = integration.integration_type
435  else:
436  integration_key = "integration"
437 
438  metadata = {
439  "config_flow": integration.config_flow,
440  "integration_type": integration.integration_type,
441  "iot_class": integration.iot_class,
442  "name": integration.name,
443  "single_config_entry": integration.manifest.get(
444  "single_config_entry", False
445  ),
446  "overwrites_built_in": integration.overwrites_built_in,
447  }
448  custom_flows[integration_key][integration.domain] = metadata
449 
450  return {"core": core_flows, "custom": custom_flows}
451 
452 
453 async def async_get_application_credentials(hass: HomeAssistant) -> list[str]:
454  """Return cached list of application credentials."""
455  integrations = await async_get_custom_components(hass)
456 
457  return [
458  *APPLICATION_CREDENTIALS,
459  *[
460  integration.domain
461  for integration in integrations.values()
462  if "application_credentials" in integration.dependencies
463  ],
464  ]
465 
466 
467 def async_process_zeroconf_match_dict(entry: dict[str, Any]) -> ZeroconfMatcher:
468  """Handle backwards compat with zeroconf matchers."""
469  entry_without_type: dict[str, Any] = entry.copy()
470  del entry_without_type["type"]
471  # These properties keys used to be at the top level, we relocate
472  # them for backwards compat
473  for moved_prop in MOVED_ZEROCONF_PROPS:
474  if value := entry_without_type.pop(moved_prop, None):
475  _LOGGER.warning(
476  (
477  'Matching the zeroconf property "%s" at top-level is deprecated and'
478  " should be moved into a properties dict; Check the developer"
479  " documentation"
480  ),
481  moved_prop,
482  )
483  if "properties" not in entry_without_type:
484  prop_dict: dict[str, str] = {}
485  entry_without_type["properties"] = prop_dict
486  else:
487  prop_dict = entry_without_type["properties"]
488  prop_dict[moved_prop] = value.lower()
489  return cast(ZeroconfMatcher, entry_without_type)
490 
491 
493  hass: HomeAssistant,
494 ) -> dict[str, list[ZeroconfMatcher]]:
495  """Return cached list of zeroconf types."""
496  zeroconf: dict[str, list[ZeroconfMatcher]] = ZEROCONF.copy() # type: ignore[assignment]
497 
498  integrations = await async_get_custom_components(hass)
499  for integration in integrations.values():
500  if not integration.zeroconf:
501  continue
502  for entry in integration.zeroconf:
503  data: ZeroconfMatcher = {"domain": integration.domain}
504  if isinstance(entry, dict):
505  typ = entry["type"]
506  data.update(async_process_zeroconf_match_dict(entry))
507  else:
508  typ = entry
509 
510  zeroconf.setdefault(typ, []).append(data)
511 
512  return zeroconf
513 
514 
515 async def async_get_bluetooth(hass: HomeAssistant) -> list[BluetoothMatcher]:
516  """Return cached list of bluetooth types."""
517  bluetooth = cast(list[BluetoothMatcher], BLUETOOTH.copy())
518 
519  integrations = await async_get_custom_components(hass)
520  for integration in integrations.values():
521  if not integration.bluetooth:
522  continue
523  for entry in integration.bluetooth:
524  bluetooth.append(
525  cast(BluetoothMatcher, {"domain": integration.domain, **entry})
526  )
527 
528  return bluetooth
529 
530 
531 async def async_get_dhcp(hass: HomeAssistant) -> list[DHCPMatcher]:
532  """Return cached list of dhcp types."""
533  dhcp = cast(list[DHCPMatcher], DHCP.copy())
534 
535  integrations = await async_get_custom_components(hass)
536  for integration in integrations.values():
537  if not integration.dhcp:
538  continue
539  for entry in integration.dhcp:
540  dhcp.append(cast(DHCPMatcher, {"domain": integration.domain, **entry}))
541 
542  return dhcp
543 
544 
545 async def async_get_usb(hass: HomeAssistant) -> list[USBMatcher]:
546  """Return cached list of usb types."""
547  usb = cast(list[USBMatcher], USB.copy())
548 
549  integrations = await async_get_custom_components(hass)
550  for integration in integrations.values():
551  if not integration.usb:
552  continue
553  for entry in integration.usb:
554  usb.append(
555  cast(
556  USBMatcher,
557  {
558  "domain": integration.domain,
559  **{k: v for k, v in entry.items() if k != "known_devices"},
560  },
561  )
562  )
563 
564  return usb
565 
566 
567 def homekit_always_discover(iot_class: str | None) -> bool:
568  """Return if we should always offer HomeKit control for a device."""
569  #
570  # Since we prefer local control, if the integration that is being
571  # discovered is cloud AND the HomeKit device is UNPAIRED we still
572  # want to discovery it.
573  #
574  # Additionally if the integration is polling, HKC offers a local
575  # push experience for the user to control the device so we want
576  # to offer that as well.
577  #
578  return not iot_class or (iot_class.startswith("cloud") or "polling" in iot_class)
579 
580 
582  hass: HomeAssistant,
583 ) -> dict[str, HomeKitDiscoveredIntegration]:
584  """Return cached list of homekit models."""
585  homekit: dict[str, HomeKitDiscoveredIntegration] = {
587  cast(str, details["domain"]), cast(bool, details["always_discover"])
588  )
589  for model, details in HOMEKIT.items()
590  }
591 
592  integrations = await async_get_custom_components(hass)
593  for integration in integrations.values():
594  if (
595  not integration.homekit
596  or "models" not in integration.homekit
597  or not integration.homekit["models"]
598  ):
599  continue
600  for model in integration.homekit["models"]:
601  homekit[model] = HomeKitDiscoveredIntegration(
602  integration.domain,
603  homekit_always_discover(integration.iot_class),
604  )
605 
606  return homekit
607 
608 
609 async def async_get_ssdp(hass: HomeAssistant) -> dict[str, list[dict[str, str]]]:
610  """Return cached list of ssdp mappings."""
611 
612  ssdp: dict[str, list[dict[str, str]]] = SSDP.copy()
613 
614  integrations = await async_get_custom_components(hass)
615  for integration in integrations.values():
616  if not integration.ssdp:
617  continue
618 
619  ssdp[integration.domain] = integration.ssdp
620 
621  return ssdp
622 
623 
624 async def async_get_mqtt(hass: HomeAssistant) -> dict[str, list[str]]:
625  """Return cached list of MQTT mappings."""
626 
627  mqtt: dict[str, list[str]] = MQTT.copy()
628 
629  integrations = await async_get_custom_components(hass)
630  for integration in integrations.values():
631  if not integration.mqtt:
632  continue
633 
634  mqtt[integration.domain] = integration.mqtt
635 
636  return mqtt
637 
638 
639 @callback
640 def async_register_preload_platform(hass: HomeAssistant, platform_name: str) -> None:
641  """Register a platform to be preloaded."""
642  preload_platforms = hass.data[DATA_PRELOAD_PLATFORMS]
643  if platform_name not in preload_platforms:
644  preload_platforms.append(platform_name)
645 
646 
648  """An integration in Home Assistant."""
649 
650  @classmethod
652  cls, hass: HomeAssistant, root_module: ModuleType, domain: str
653  ) -> Integration | None:
654  """Resolve an integration from a root module."""
655  for base in root_module.__path__:
656  manifest_path = pathlib.Path(base) / domain / "manifest.json"
657 
658  if not manifest_path.is_file():
659  continue
660 
661  try:
662  manifest = cast(Manifest, json_loads(manifest_path.read_text()))
663  except JSON_DECODE_EXCEPTIONS as err:
664  _LOGGER.error(
665  "Error parsing manifest.json file at %s: %s", manifest_path, err
666  )
667  continue
668 
669  file_path = manifest_path.parent
670  # Avoid the listdir for virtual integrations
671  # as they cannot have any platforms
672  is_virtual = manifest.get("integration_type") == "virtual"
673  integration = cls(
674  hass,
675  f"{root_module.__name__}.{domain}",
676  file_path,
677  manifest,
678  None if is_virtual else set(os.listdir(file_path)),
679  )
680 
681  if not integration.import_executor:
682  _LOGGER.warning(IMPORT_EVENT_LOOP_WARNING, integration.domain)
683 
684  if integration.is_built_in:
685  return integration
686 
687  _LOGGER.warning(CUSTOM_WARNING, integration.domain)
688 
689  if integration.version is None:
690  _LOGGER.error(
691  (
692  "The custom integration '%s' does not have a version key in the"
693  " manifest file and was blocked from loading. See"
694  " https://developers.home-assistant.io"
695  "/blog/2021/01/29/custom-integration-changes#versions"
696  " for more details"
697  ),
698  integration.domain,
699  )
700  return None
701  try:
702  AwesomeVersion(
703  integration.version,
704  ensure_strategy=[
705  AwesomeVersionStrategy.CALVER,
706  AwesomeVersionStrategy.SEMVER,
707  AwesomeVersionStrategy.SIMPLEVER,
708  AwesomeVersionStrategy.BUILDVER,
709  AwesomeVersionStrategy.PEP440,
710  ],
711  )
712  except AwesomeVersionException:
713  _LOGGER.error(
714  (
715  "The custom integration '%s' does not have a valid version key"
716  " (%s) in the manifest file and was blocked from loading. See"
717  " https://developers.home-assistant.io"
718  "/blog/2021/01/29/custom-integration-changes#versions"
719  " for more details"
720  ),
721  integration.domain,
722  integration.version,
723  )
724  return None
725 
726  if blocked := BLOCKED_CUSTOM_INTEGRATIONS.get(integration.domain):
727  if _version_blocked(integration.version, blocked):
728  _LOGGER.error(
729  (
730  "Version %s of custom integration '%s' %s and was blocked "
731  "from loading, please %s"
732  ),
733  integration.version,
734  integration.domain,
735  blocked.reason,
736  async_suggest_report_issue(None, integration=integration),
737  )
738  return None
739 
740  return integration
741 
742  return None
743 
744  def __init__(
745  self,
746  hass: HomeAssistant,
747  pkg_path: str,
748  file_path: pathlib.Path,
749  manifest: Manifest,
750  top_level_files: set[str] | None = None,
751  ) -> None:
752  """Initialize an integration."""
753  self.hasshass = hass
754  self.pkg_pathpkg_path = pkg_path
755  self.file_pathfile_path = file_path
756  self.manifestmanifest = manifest
757  manifest["is_built_in"] = self.is_built_inis_built_in
758  manifest["overwrites_built_in"] = self.overwrites_built_inoverwrites_built_in
759 
760  if self.dependenciesdependencies:
761  self._all_dependencies_resolved_all_dependencies_resolved: bool | None = None
762  self._all_dependencies_all_dependencies: set[str] | None = None
763  else:
764  self._all_dependencies_resolved_all_dependencies_resolved = True
765  self._all_dependencies_all_dependencies = set()
766 
767  self._platforms_to_preload_platforms_to_preload = hass.data[DATA_PRELOAD_PLATFORMS]
768  self._component_future_component_future: asyncio.Future[ComponentProtocol] | None = None
769  self._import_futures: dict[str, asyncio.Future[ModuleType]] = {}
770  self._cache_cache = hass.data[DATA_COMPONENTS]
771  self._missing_platforms_cache_missing_platforms_cache = hass.data[DATA_MISSING_PLATFORMS]
772  self._top_level_files_top_level_files = top_level_files or set()
773  _LOGGER.info("Loaded %s from %s", self.domaindomain, pkg_path)
774 
775  @cached_property
776  def manifest_json_fragment(self) -> json_fragment:
777  """Return manifest as a JSON fragment."""
778  return json_fragment(json_bytes(self.manifestmanifest))
779 
780  @cached_property
781  def name(self) -> str:
782  """Return name."""
783  return self.manifestmanifest["name"]
784 
785  @cached_property
786  def disabled(self) -> str | None:
787  """Return reason integration is disabled."""
788  return self.manifestmanifest.get("disabled")
789 
790  @cached_property
791  def domain(self) -> str:
792  """Return domain."""
793  return self.manifestmanifest["domain"]
794 
795  @cached_property
796  def dependencies(self) -> list[str]:
797  """Return dependencies."""
798  return self.manifestmanifest.get("dependencies", [])
799 
800  @cached_property
801  def after_dependencies(self) -> list[str]:
802  """Return after_dependencies."""
803  return self.manifestmanifest.get("after_dependencies", [])
804 
805  @cached_property
806  def requirements(self) -> list[str]:
807  """Return requirements."""
808  return self.manifestmanifest.get("requirements", [])
809 
810  @cached_property
811  def config_flow(self) -> bool:
812  """Return config_flow."""
813  return self.manifestmanifest.get("config_flow") or False
814 
815  @cached_property
816  def documentation(self) -> str | None:
817  """Return documentation."""
818  return self.manifestmanifest.get("documentation")
819 
820  @cached_property
821  def issue_tracker(self) -> str | None:
822  """Return issue tracker link."""
823  return self.manifestmanifest.get("issue_tracker")
824 
825  @cached_property
826  def loggers(self) -> list[str] | None:
827  """Return list of loggers used by the integration."""
828  return self.manifestmanifest.get("loggers")
829 
830  @cached_property
831  def quality_scale(self) -> str | None:
832  """Return Integration Quality Scale."""
833  # Custom integrations default to "custom" quality scale.
834  if not self.is_built_inis_built_in or self.overwrites_built_inoverwrites_built_in:
835  return "custom"
836  return self.manifestmanifest.get("quality_scale")
837 
838  @cached_property
839  def iot_class(self) -> str | None:
840  """Return the integration IoT Class."""
841  return self.manifestmanifest.get("iot_class")
842 
843  @cached_property
845  self,
846  ) -> Literal[
847  "entity", "device", "hardware", "helper", "hub", "service", "system", "virtual"
848  ]:
849  """Return the integration type."""
850  return self.manifestmanifest.get("integration_type", "hub")
851 
852  @cached_property
853  def import_executor(self) -> bool:
854  """Import integration in the executor."""
855  # If the integration does not explicitly set import_executor, we default to
856  # True.
857  return self.manifestmanifest.get("import_executor", True)
858 
859  @cached_property
860  def has_translations(self) -> bool:
861  """Return if the integration has translations."""
862  return "translations" in self._top_level_files_top_level_files
863 
864  @cached_property
865  def has_services(self) -> bool:
866  """Return if the integration has services."""
867  return "services.yaml" in self._top_level_files_top_level_files
868 
869  @property
870  def mqtt(self) -> list[str] | None:
871  """Return Integration MQTT entries."""
872  return self.manifestmanifest.get("mqtt")
873 
874  @property
875  def ssdp(self) -> list[dict[str, str]] | None:
876  """Return Integration SSDP entries."""
877  return self.manifestmanifest.get("ssdp")
878 
879  @property
880  def zeroconf(self) -> list[str | dict[str, str]] | None:
881  """Return Integration zeroconf entries."""
882  return self.manifestmanifest.get("zeroconf")
883 
884  @property
885  def bluetooth(self) -> list[dict[str, str | int]] | None:
886  """Return Integration bluetooth entries."""
887  return self.manifestmanifest.get("bluetooth")
888 
889  @property
890  def dhcp(self) -> list[dict[str, str | bool]] | None:
891  """Return Integration dhcp entries."""
892  return self.manifestmanifest.get("dhcp")
893 
894  @property
895  def usb(self) -> list[dict[str, str]] | None:
896  """Return Integration usb entries."""
897  return self.manifestmanifest.get("usb")
898 
899  @property
900  def homekit(self) -> dict[str, list[str]] | None:
901  """Return Integration homekit entries."""
902  return self.manifestmanifest.get("homekit")
903 
904  @property
905  def is_built_in(self) -> bool:
906  """Test if package is a built-in integration."""
907  return self.pkg_pathpkg_path.startswith(PACKAGE_BUILTIN)
908 
909  @property
910  def overwrites_built_in(self) -> bool:
911  """Return if package overwrites a built-in integration."""
912  if self.is_built_inis_built_in:
913  return False
914  core_comp_path = (
915  pathlib.Path(__file__).parent / "components" / self.domaindomain / "manifest.json"
916  )
917  return core_comp_path.is_file()
918 
919  @property
920  def version(self) -> AwesomeVersion | None:
921  """Return the version of the integration."""
922  if "version" not in self.manifestmanifest:
923  return None
924  return AwesomeVersion(self.manifestmanifest["version"])
925 
926  @cached_property
927  def single_config_entry(self) -> bool:
928  """Return if the integration supports a single config entry only."""
929  return self.manifestmanifest.get("single_config_entry", False)
930 
931  @property
932  def all_dependencies(self) -> set[str]:
933  """Return all dependencies including sub-dependencies."""
934  if self._all_dependencies_all_dependencies is None:
935  raise RuntimeError("Dependencies not resolved!")
936 
937  return self._all_dependencies_all_dependencies
938 
939  @property
940  def all_dependencies_resolved(self) -> bool:
941  """Return if all dependencies have been resolved."""
942  return self._all_dependencies_resolved_all_dependencies_resolved is not None
943 
944  async def resolve_dependencies(self) -> bool:
945  """Resolve all dependencies."""
946  if self._all_dependencies_resolved_all_dependencies_resolved is not None:
947  return self._all_dependencies_resolved_all_dependencies_resolved
948 
949  self._all_dependencies_resolved_all_dependencies_resolved = False
950  try:
951  dependencies = await _async_component_dependencies(self.hasshass, self)
952  except IntegrationNotFound as err:
953  _LOGGER.error(
954  (
955  "Unable to resolve dependencies for %s: unable to resolve"
956  " (sub)dependency %s"
957  ),
958  self.domaindomain,
959  err.domain,
960  )
961  except CircularDependency as err:
962  _LOGGER.error(
963  (
964  "Unable to resolve dependencies for %s: it contains a circular"
965  " dependency: %s -> %s"
966  ),
967  self.domaindomain,
968  err.from_domain,
969  err.to_domain,
970  )
971  else:
972  dependencies.discard(self.domaindomain)
973  self._all_dependencies_all_dependencies = dependencies
974  self._all_dependencies_resolved_all_dependencies_resolved = True
975 
976  return self._all_dependencies_resolved_all_dependencies_resolved
977 
978  async def async_get_component(self) -> ComponentProtocol:
979  """Return the component.
980 
981  This method will load the component if it's not already loaded
982  and will check if import_executor is set and load it in the executor,
983  otherwise it will load it in the event loop.
984  """
985  domain = self.domaindomain
986  if domain in (cache := self._cache_cache):
987  return cache[domain]
988 
989  if self._component_future_component_future:
990  return await self._component_future_component_future
991 
992  if debug := _LOGGER.isEnabledFor(logging.DEBUG):
993  start = time.perf_counter()
994 
995  # Some integrations fail on import because they call functions incorrectly.
996  # So we do it before validating config to catch these errors.
997  load_executor = self.import_executorimport_executor and (
998  self.pkg_pathpkg_path not in sys.modules
999  or (self.config_flowconfig_flow and f"{self.pkg_path}.config_flow" not in sys.modules)
1000  )
1001  if not load_executor:
1002  comp = self._get_component_get_component()
1003  if debug:
1004  _LOGGER.debug(
1005  "Component %s import took %.3f seconds (loaded_executor=False)",
1006  self.domaindomain,
1007  time.perf_counter() - start,
1008  )
1009  return comp
1010 
1011  self._component_future_component_future = self.hasshass.loop.create_future()
1012  try:
1013  try:
1014  comp = await self.hasshass.async_add_import_executor_job(
1015  self._get_component_get_component, True
1016  )
1017  except ModuleNotFoundError:
1018  raise
1019  except ImportError as ex:
1020  load_executor = False
1021  _LOGGER.debug(
1022  "Failed to import %s in executor", self.domaindomain, exc_info=ex
1023  )
1024  # If importing in the executor deadlocks because there is a circular
1025  # dependency, we fall back to the event loop.
1026  comp = self._get_component_get_component()
1027  self._component_future_component_future.set_result(comp)
1028  except BaseException as ex:
1029  self._component_future_component_future.set_exception(ex)
1030  with suppress(BaseException):
1031  # Set the exception retrieved flag on the future since
1032  # it will never be retrieved unless there
1033  # are concurrent calls to async_get_component
1034  self._component_future_component_future.result()
1035  raise
1036  finally:
1037  self._component_future_component_future = None
1038 
1039  if debug:
1040  _LOGGER.debug(
1041  "Component %s import took %.3f seconds (loaded_executor=%s)",
1042  self.domaindomain,
1043  time.perf_counter() - start,
1044  load_executor,
1045  )
1046 
1047  return comp
1048 
1049  def get_component(self) -> ComponentProtocol:
1050  """Return the component.
1051 
1052  This method must be thread-safe as it's called from the executor
1053  and the event loop.
1054 
1055  This method checks the cache and if the component is not loaded
1056  it will load it in the executor if import_executor is set, otherwise
1057  it will load it in the event loop.
1058 
1059  This is mostly a thin wrapper around importlib.import_module
1060  with a dict cache which is thread-safe since importlib has
1061  appropriate locks.
1062  """
1063  domain = self.domaindomain
1064  if domain in (cache := self._cache_cache):
1065  return cache[domain]
1066  return self._get_component_get_component()
1067 
1068  def _get_component(self, preload_platforms: bool = False) -> ComponentProtocol:
1069  """Return the component."""
1070  cache = self._cache_cache
1071  domain = self.domaindomain
1072  try:
1073  cache[domain] = cast(
1074  ComponentProtocol, importlib.import_module(self.pkg_pathpkg_path)
1075  )
1076  except ImportError:
1077  raise
1078  except RuntimeError as err:
1079  # _DeadlockError inherits from RuntimeError
1080  raise ImportError(f"RuntimeError importing {self.pkg_path}: {err}") from err
1081  except Exception as err:
1082  _LOGGER.exception(
1083  "Unexpected exception importing component %s", self.pkg_pathpkg_path
1084  )
1085  raise ImportError(f"Exception importing {self.pkg_path}") from err
1086 
1087  if preload_platforms:
1088  for platform_name in self.platforms_existsplatforms_exists(self._platforms_to_preload_platforms_to_preload):
1089  with suppress(ImportError):
1090  self.get_platformget_platform(platform_name)
1091 
1092  return cache[domain]
1093 
1094  def _load_platforms(self, platform_names: Iterable[str]) -> dict[str, ModuleType]:
1095  """Load platforms for an integration."""
1096  return {
1097  platform_name: self._load_platform_load_platform(platform_name)
1098  for platform_name in platform_names
1099  }
1100 
1101  async def async_get_platform(self, platform_name: str) -> ModuleType:
1102  """Return a platform for an integration."""
1103  # Fast path for a single platform when it is already cached.
1104  # This is the common case.
1105  if platform := self._cache_cache.get(f"{self.domain}.{platform_name}"):
1106  return platform # type: ignore[return-value]
1107  platforms = await self.async_get_platformsasync_get_platforms((platform_name,))
1108  return platforms[platform_name]
1109 
1111  self, platform_names: Iterable[Platform | str]
1112  ) -> dict[str, ModuleType]:
1113  """Return a platforms for an integration."""
1114  domain = self.domaindomain
1115  platforms: dict[str, ModuleType] = {}
1116 
1117  load_executor_platforms: list[str] = []
1118  load_event_loop_platforms: list[str] = []
1119  in_progress_imports: dict[str, asyncio.Future[ModuleType]] = {}
1120  import_futures: list[tuple[str, asyncio.Future[ModuleType]]] = []
1121 
1122  for platform_name in platform_names:
1123  if platform := self._get_platform_cached_or_raise_get_platform_cached_or_raise(platform_name):
1124  platforms[platform_name] = platform
1125  continue
1126 
1127  # Another call to async_get_platforms is already importing this platform
1128  if future := self._import_futures.get(platform_name):
1129  in_progress_imports[platform_name] = future
1130  continue
1131 
1132  full_name = f"{domain}.{platform_name}"
1133  if (
1134  self.import_executorimport_executor
1135  and full_name not in self.hasshass.config.components
1136  and f"{self.pkg_path}.{platform_name}" not in sys.modules
1137  ):
1138  load_executor_platforms.append(platform_name)
1139  else:
1140  load_event_loop_platforms.append(platform_name)
1141 
1142  import_future = self.hasshass.loop.create_future()
1143  self._import_futures[platform_name] = import_future
1144  import_futures.append((platform_name, import_future))
1145 
1146  if load_executor_platforms or load_event_loop_platforms:
1147  if debug := _LOGGER.isEnabledFor(logging.DEBUG):
1148  start = time.perf_counter()
1149 
1150  try:
1151  if load_executor_platforms:
1152  try:
1153  platforms.update(
1154  await self.hasshass.async_add_import_executor_job(
1155  self._load_platforms_load_platforms, platform_names
1156  )
1157  )
1158  except ModuleNotFoundError:
1159  raise
1160  except ImportError as ex:
1161  _LOGGER.debug(
1162  "Failed to import %s platforms %s in executor",
1163  domain,
1164  load_executor_platforms,
1165  exc_info=ex,
1166  )
1167  # If importing in the executor deadlocks because there is a circular
1168  # dependency, we fall back to the event loop.
1169  load_event_loop_platforms.extend(load_executor_platforms)
1170 
1171  if load_event_loop_platforms:
1172  platforms.update(self._load_platforms_load_platforms(platform_names))
1173 
1174  for platform_name, import_future in import_futures:
1175  import_future.set_result(platforms[platform_name])
1176 
1177  except BaseException as ex:
1178  for _, import_future in import_futures:
1179  import_future.set_exception(ex)
1180  with suppress(BaseException):
1181  # Set the exception retrieved flag on the future since
1182  # it will never be retrieved unless there
1183  # are concurrent calls to async_get_platforms
1184  import_future.result()
1185  raise
1186 
1187  finally:
1188  for platform_name, _ in import_futures:
1189  self._import_futures.pop(platform_name)
1190 
1191  if debug:
1192  _LOGGER.debug(
1193  "Importing platforms for %s executor=%s loop=%s took %.2fs",
1194  domain,
1195  load_executor_platforms,
1196  load_event_loop_platforms,
1197  time.perf_counter() - start,
1198  )
1199 
1200  if in_progress_imports:
1201  for platform_name, future in in_progress_imports.items():
1202  platforms[platform_name] = await future
1203 
1204  return platforms
1205 
1206  def _get_platform_cached_or_raise(self, platform_name: str) -> ModuleType | None:
1207  """Return a platform for an integration from cache."""
1208  full_name = f"{self.domain}.{platform_name}"
1209  if full_name in self._cache_cache:
1210  # the cache is either a ModuleType or a ComponentProtocol
1211  # but we only care about the ModuleType here
1212  return self._cache_cache[full_name] # type: ignore[return-value]
1213  if full_name in self._missing_platforms_cache_missing_platforms_cache:
1214  raise ModuleNotFoundError(
1215  f"Platform {full_name} not found",
1216  name=f"{self.pkg_path}.{platform_name}",
1217  )
1218  return None
1219 
1220  def platforms_are_loaded(self, platform_names: Iterable[str]) -> bool:
1221  """Check if a platforms are loaded for an integration."""
1222  return all(
1223  f"{self.domain}.{platform_name}" in self._cache_cache
1224  for platform_name in platform_names
1225  )
1226 
1227  def get_platform_cached(self, platform_name: str) -> ModuleType | None:
1228  """Return a platform for an integration from cache."""
1229  return self._cache_cache.get(f"{self.domain}.{platform_name}") # type: ignore[return-value]
1230 
1231  def get_platform(self, platform_name: str) -> ModuleType:
1232  """Return a platform for an integration."""
1233  if platform := self._get_platform_cached_or_raise_get_platform_cached_or_raise(platform_name):
1234  return platform
1235  return self._load_platform_load_platform(platform_name)
1236 
1237  def platforms_exists(self, platform_names: Iterable[str]) -> list[str]:
1238  """Check if a platforms exists for an integration.
1239 
1240  This method is thread-safe and can be called from the executor
1241  or event loop without doing blocking I/O.
1242  """
1243  files = self._top_level_files_top_level_files
1244  domain = self.domaindomain
1245  existing_platforms: list[str] = []
1246  missing_platforms = self._missing_platforms_cache_missing_platforms_cache
1247  for platform_name in platform_names:
1248  full_name = f"{domain}.{platform_name}"
1249  if full_name not in missing_platforms and (
1250  f"{platform_name}.py" in files or platform_name in files
1251  ):
1252  existing_platforms.append(platform_name)
1253  continue
1254  missing_platforms[full_name] = True
1255 
1256  return existing_platforms
1257 
1258  def _load_platform(self, platform_name: str) -> ModuleType:
1259  """Load a platform for an integration.
1260 
1261  This method must be thread-safe as it's called from the executor
1262  and the event loop.
1263 
1264  This is mostly a thin wrapper around importlib.import_module
1265  with a dict cache which is thread-safe since importlib has
1266  appropriate locks.
1267  """
1268  full_name = f"{self.domain}.{platform_name}"
1269  cache = self.hasshass.data[DATA_COMPONENTS]
1270  try:
1271  cache[full_name] = self._import_platform_import_platform(platform_name)
1272  except ModuleNotFoundError:
1273  if self.domaindomain in cache:
1274  # If the domain is loaded, cache that the platform
1275  # does not exist so we do not try to load it again
1276  self._missing_platforms_cache_missing_platforms_cache[full_name] = True
1277  raise
1278  except ImportError:
1279  raise
1280  except RuntimeError as err:
1281  # _DeadlockError inherits from RuntimeError
1282  raise ImportError(
1283  f"RuntimeError importing {self.pkg_path}.{platform_name}: {err}"
1284  ) from err
1285  except Exception as err:
1286  _LOGGER.exception(
1287  "Unexpected exception importing platform %s.%s",
1288  self.pkg_pathpkg_path,
1289  platform_name,
1290  )
1291  raise ImportError(
1292  f"Exception importing {self.pkg_path}.{platform_name}"
1293  ) from err
1294 
1295  return cast(ModuleType, cache[full_name])
1296 
1297  def _import_platform(self, platform_name: str) -> ModuleType:
1298  """Import the platform.
1299 
1300  This method must be thread-safe as it's called from the executor
1301  and the event loop.
1302  """
1303  return importlib.import_module(f"{self.pkg_path}.{platform_name}")
1304 
1305  def __repr__(self) -> str:
1306  """Text representation of class."""
1307  return f"<Integration {self.domain}: {self.pkg_path}>"
1308 
1309 
1311  integration_version: AwesomeVersion,
1312  blocked_integration: BlockedIntegration,
1313 ) -> bool:
1314  """Return True if the integration version is blocked."""
1315  if blocked_integration.lowest_good_version is None:
1316  return True
1317 
1318  if integration_version >= blocked_integration.lowest_good_version:
1319  return False
1320 
1321  return True
1322 
1323 
1325  hass: HomeAssistant, root_module: ModuleType, domains: Iterable[str]
1326 ) -> dict[str, Integration]:
1327  """Resolve multiple integrations from root."""
1328  integrations: dict[str, Integration] = {}
1329  for domain in domains:
1330  try:
1331  integration = Integration.resolve_from_root(hass, root_module, domain)
1332  except Exception:
1333  _LOGGER.exception("Error loading integration: %s", domain)
1334  else:
1335  if integration:
1336  integrations[domain] = integration
1337  return integrations
1338 
1339 
1340 @callback
1341 def async_get_loaded_integration(hass: HomeAssistant, domain: str) -> Integration:
1342  """Get an integration which is already loaded.
1343 
1344  Raises IntegrationNotLoaded if the integration is not loaded.
1345  """
1346  cache = hass.data[DATA_INTEGRATIONS]
1347  int_or_fut = cache.get(domain, UNDEFINED)
1348  # Integration is never subclassed, so we can check for type
1349  if type(int_or_fut) is Integration:
1350  return int_or_fut
1351  raise IntegrationNotLoaded(domain)
1352 
1353 
1354 async def async_get_integration(hass: HomeAssistant, domain: str) -> Integration:
1355  """Get integration."""
1356  cache = hass.data[DATA_INTEGRATIONS]
1357  if type(int_or_fut := cache.get(domain, UNDEFINED)) is Integration:
1358  return int_or_fut
1359  integrations_or_excs = await async_get_integrations(hass, [domain])
1360  int_or_exc = integrations_or_excs[domain]
1361  if isinstance(int_or_exc, Integration):
1362  return int_or_exc
1363  raise int_or_exc
1364 
1365 
1367  hass: HomeAssistant, domains: Iterable[str]
1368 ) -> dict[str, Integration | Exception]:
1369  """Get integrations."""
1370  cache = hass.data[DATA_INTEGRATIONS]
1371  results: dict[str, Integration | Exception] = {}
1372  needed: dict[str, asyncio.Future[None]] = {}
1373  in_progress: dict[str, asyncio.Future[None]] = {}
1374  for domain in domains:
1375  int_or_fut = cache.get(domain, UNDEFINED)
1376  # Integration is never subclassed, so we can check for type
1377  if type(int_or_fut) is Integration:
1378  results[domain] = int_or_fut
1379  elif int_or_fut is not UNDEFINED:
1380  in_progress[domain] = cast(asyncio.Future[None], int_or_fut)
1381  elif "." in domain:
1382  results[domain] = ValueError(f"Invalid domain {domain}")
1383  else:
1384  needed[domain] = cache[domain] = hass.loop.create_future()
1385 
1386  if in_progress:
1387  await asyncio.wait(in_progress.values())
1388  for domain in in_progress:
1389  # When we have waited and it's UNDEFINED, it doesn't exist
1390  # We don't cache that it doesn't exist, or else people can't fix it
1391  # and then restart, because their config will never be valid.
1392  if (int_or_fut := cache.get(domain, UNDEFINED)) is UNDEFINED:
1393  results[domain] = IntegrationNotFound(domain)
1394  else:
1395  results[domain] = cast(Integration, int_or_fut)
1396 
1397  if not needed:
1398  return results
1399 
1400  # First we look for custom components
1401  # Instead of using resolve_from_root we use the cache of custom
1402  # components to find the integration.
1403  custom = await async_get_custom_components(hass)
1404  for domain, future in needed.items():
1405  if integration := custom.get(domain):
1406  results[domain] = cache[domain] = integration
1407  future.set_result(None)
1408 
1409  for domain in results:
1410  if domain in needed:
1411  del needed[domain]
1412 
1413  # Now the rest use resolve_from_root
1414  if needed:
1415  from . import components # pylint: disable=import-outside-toplevel
1416 
1417  integrations = await hass.async_add_executor_job(
1418  _resolve_integrations_from_root, hass, components, needed
1419  )
1420  for domain, future in needed.items():
1421  int_or_exc = integrations.get(domain)
1422  if not int_or_exc:
1423  cache.pop(domain)
1424  results[domain] = IntegrationNotFound(domain)
1425  elif isinstance(int_or_exc, Exception):
1426  cache.pop(domain)
1427  exc = IntegrationNotFound(domain)
1428  exc.__cause__ = int_or_exc
1429  results[domain] = exc
1430  else:
1431  results[domain] = cache[domain] = int_or_exc
1432  future.set_result(None)
1433 
1434  return results
1435 
1436 
1437 class LoaderError(Exception):
1438  """Loader base error."""
1439 
1440 
1441 class IntegrationNotFound(LoaderError):
1442  """Raised when a component is not found."""
1443 
1444  def __init__(self, domain: str) -> None:
1445  """Initialize a component not found error."""
1446  super().__init__(f"Integration '{domain}' not found.")
1447  self.domaindomain = domain
1448 
1449 
1451  """Raised when a component is not loaded."""
1452 
1453  def __init__(self, domain: str) -> None:
1454  """Initialize a component not found error."""
1455  super().__init__(f"Integration '{domain}' not loaded.")
1456  self.domaindomain = domain
1457 
1458 
1460  """Raised when a circular dependency is found when resolving components."""
1461 
1462  def __init__(self, from_domain: str | set[str], to_domain: str) -> None:
1463  """Initialize circular dependency error."""
1464  super().__init__(f"Circular dependency detected: {from_domain} -> {to_domain}.")
1465  self.from_domainfrom_domain = from_domain
1466  self.to_domainto_domain = to_domain
1467 
1468 
1470  hass: HomeAssistant, comp_or_platform: str, base_paths: list[str]
1471 ) -> ComponentProtocol | None:
1472  """Try to load specified file.
1473 
1474  Looks in config dir first, then built-in components.
1475  Only returns it if also found to be valid.
1476  Async friendly.
1477  """
1478  cache = hass.data[DATA_COMPONENTS]
1479  if module := cache.get(comp_or_platform):
1480  return cast(ComponentProtocol, module)
1481 
1482  for path in (f"{base}.{comp_or_platform}" for base in base_paths):
1483  try:
1484  module = importlib.import_module(path)
1485 
1486  # In Python 3 you can import files from directories that do not
1487  # contain the file __init__.py. A directory is a valid module if
1488  # it contains a file with the .py extension. In this case Python
1489  # will succeed in importing the directory as a module and call it
1490  # a namespace. We do not care about namespaces.
1491  # This prevents that when only
1492  # custom_components/switch/some_platform.py exists,
1493  # the import custom_components.switch would succeed.
1494  # __file__ was unset for namespaces before Python 3.7
1495  if getattr(module, "__file__", None) is None:
1496  continue
1497 
1498  cache[comp_or_platform] = module
1499 
1500  return cast(ComponentProtocol, module)
1501 
1502  except ImportError as err:
1503  # This error happens if for example custom_components/switch
1504  # exists and we try to load switch.demo.
1505  # Ignore errors for custom_components, custom_components.switch
1506  # and custom_components.switch.demo.
1507  white_listed_errors = []
1508  parts = []
1509  for part in path.split("."):
1510  parts.append(part)
1511  white_listed_errors.append(f"No module named '{'.'.join(parts)}'")
1512 
1513  if str(err) not in white_listed_errors:
1514  _LOGGER.exception(
1515  "Error loading %s. Make sure all dependencies are installed", path
1516  )
1517 
1518  return None
1519 
1520 
1522  """Class to wrap a Python module and auto fill in hass argument."""
1523 
1524  def __init__(self, hass: HomeAssistant, module: ComponentProtocol) -> None:
1525  """Initialize the module wrapper."""
1526  self._hass_hass = hass
1527  self._module_module = module
1528 
1529  def __getattr__(self, attr: str) -> Any:
1530  """Fetch an attribute."""
1531  value = getattr(self._module_module, attr)
1532 
1533  if hasattr(value, "__bind_hass"):
1534  value = ft.partial(value, self._hass_hass)
1535 
1536  setattr(self, attr, value)
1537  return value
1538 
1539 
1541  """Helper to load components."""
1542 
1543  def __init__(self, hass: HomeAssistant) -> None:
1544  """Initialize the Components class."""
1545  self._hass_hass = hass
1546 
1547  def __getattr__(self, comp_name: str) -> ModuleWrapper:
1548  """Fetch a component."""
1549  # Test integration cache
1550  integration = self._hass_hass.data[DATA_INTEGRATIONS].get(comp_name)
1551 
1552  if isinstance(integration, Integration):
1553  component: ComponentProtocol | None = integration.get_component()
1554  else:
1555  # Fallback to importing old-school
1556  component = _load_file(self._hass_hass, comp_name, _lookup_path(self._hass_hass))
1557 
1558  if component is None:
1559  raise ImportError(f"Unable to load {comp_name}")
1560 
1561  # Local import to avoid circular dependencies
1562  # pylint: disable-next=import-outside-toplevel
1563  from .helpers.frame import ReportBehavior, report_usage
1564 
1565  report_usage(
1566  f"accesses hass.components.{comp_name}, which"
1567  f" should be updated to import functions used from {comp_name} directly",
1568  core_behavior=ReportBehavior.IGNORE,
1569  core_integration_behavior=ReportBehavior.IGNORE,
1570  custom_integration_behavior=ReportBehavior.LOG,
1571  breaks_in_ha_version="2025.3",
1572  )
1573 
1574  wrapped = ModuleWrapper(self._hass_hass, component)
1575  setattr(self, comp_name, wrapped)
1576  return wrapped
1577 
1578 
1579 class Helpers:
1580  """Helper to load helpers."""
1581 
1582  def __init__(self, hass: HomeAssistant) -> None:
1583  """Initialize the Helpers class."""
1584  self._hass_hass = hass
1585 
1586  def __getattr__(self, helper_name: str) -> ModuleWrapper:
1587  """Fetch a helper."""
1588  helper = importlib.import_module(f"homeassistant.helpers.{helper_name}")
1589 
1590  # Local import to avoid circular dependencies
1591  # pylint: disable-next=import-outside-toplevel
1592  from .helpers.frame import ReportBehavior, report_usage
1593 
1594  report_usage(
1595  (
1596  f"accesses hass.helpers.{helper_name}, which"
1597  f" should be updated to import functions used from {helper_name} directly"
1598  ),
1599  core_behavior=ReportBehavior.IGNORE,
1600  core_integration_behavior=ReportBehavior.IGNORE,
1601  custom_integration_behavior=ReportBehavior.LOG,
1602  breaks_in_ha_version="2025.5",
1603  )
1604 
1605  wrapped = ModuleWrapper(self._hass_hass, helper)
1606  setattr(self, helper_name, wrapped)
1607  return wrapped
1608 
1609 
1610 def bind_hass[_CallableT: Callable[..., Any]](func: _CallableT) -> _CallableT:
1611  """Decorate function to indicate that first argument is hass.
1612 
1613  The use of this decorator is discouraged, and it should not be used
1614  for new functions.
1615  """
1616  setattr(func, "__bind_hass", True)
1617  return func
1618 
1619 
1621  hass: HomeAssistant,
1622  integration: Integration,
1623 ) -> set[str]:
1624  """Get component dependencies."""
1625  loading: set[str] = set()
1626  loaded: set[str] = set()
1627 
1628  async def component_dependencies_impl(integration: Integration) -> None:
1629  """Recursively get component dependencies."""
1630  domain = integration.domain
1631  if not (dependencies := integration.dependencies):
1632  loaded.add(domain)
1633  return
1634 
1635  loading.add(domain)
1636  dep_integrations = await async_get_integrations(hass, dependencies)
1637  for dependency_domain, dep_integration in dep_integrations.items():
1638  if isinstance(dep_integration, Exception):
1639  raise dep_integration
1640 
1641  # If we are already loading it, we have a circular dependency.
1642  # We have to check it here to make sure that every integration that
1643  # depends on us, does not appear in our own after_dependencies.
1644  if conflict := loading.intersection(dep_integration.after_dependencies):
1645  raise CircularDependency(conflict, dependency_domain)
1646 
1647  # If we have already loaded it, no point doing it again.
1648  if dependency_domain in loaded:
1649  continue
1650 
1651  # If we are already loading it, we have a circular dependency.
1652  if dependency_domain in loading:
1653  raise CircularDependency(dependency_domain, domain)
1654 
1655  await component_dependencies_impl(dep_integration)
1656  loading.remove(domain)
1657  loaded.add(domain)
1658 
1659  await component_dependencies_impl(integration)
1660 
1661  return loaded
1662 
1663 
1664 def _async_mount_config_dir(hass: HomeAssistant) -> None:
1665  """Mount config dir in order to load custom_component.
1666 
1667  Async friendly but not a coroutine.
1668  """
1669 
1670  sys.path.insert(0, hass.config.config_dir)
1671  with suppress(ImportError):
1672  import custom_components # pylint: disable=import-outside-toplevel # noqa: F401
1673  sys.path.remove(hass.config.config_dir)
1674  sys.path_importer_cache.pop(hass.config.config_dir, None)
1675 
1676 
1677 def _lookup_path(hass: HomeAssistant) -> list[str]:
1678  """Return the lookup paths for legacy lookups."""
1679  if hass.config.recovery_mode or hass.config.safe_mode:
1680  return [PACKAGE_BUILTIN]
1681  return [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN]
1682 
1683 
1684 def is_component_module_loaded(hass: HomeAssistant, module: str) -> bool:
1685  """Test if a component module is loaded."""
1686  return module in hass.data[DATA_COMPONENTS]
1687 
1688 
1689 @callback
1691  hass: HomeAssistant | None,
1692  integration_domain: str | None,
1693 ) -> Integration | None:
1694  """Return details of an integration for issue reporting."""
1695  integration: Integration | None = None
1696  if not hass or not integration_domain:
1697  # We are unable to get the integration
1698  return None
1699 
1700  if (comps_or_future := hass.data.get(DATA_CUSTOM_COMPONENTS)) and not isinstance(
1701  comps_or_future, asyncio.Future
1702  ):
1703  integration = comps_or_future.get(integration_domain)
1704 
1705  if not integration:
1706  with suppress(IntegrationNotLoaded):
1707  integration = async_get_loaded_integration(hass, integration_domain)
1708 
1709  return integration
1710 
1711 
1712 @callback
1714  hass: HomeAssistant | None,
1715  *,
1716  integration: Integration | None = None,
1717  integration_domain: str | None = None,
1718  module: str | None = None,
1719 ) -> str | None:
1720  """Return a URL for an integration's issue tracker."""
1721  issue_tracker = (
1722  "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue"
1723  )
1724  if not integration and not integration_domain and not module:
1725  # If we know nothing about the integration, suggest opening an issue on HA core
1726  return issue_tracker
1727 
1728  if not integration:
1729  integration = async_get_issue_integration(hass, integration_domain)
1730 
1731  if integration and not integration.is_built_in:
1732  return integration.issue_tracker
1733 
1734  if module and "custom_components" in module:
1735  return None
1736 
1737  if integration:
1738  integration_domain = integration.domain
1739 
1740  if integration_domain:
1741  issue_tracker += f"+label%3A%22integration%3A+{integration_domain}%22"
1742  return issue_tracker
1743 
1744 
1745 @callback
1747  hass: HomeAssistant | None,
1748  *,
1749  integration: Integration | None = None,
1750  integration_domain: str | None = None,
1751  module: str | None = None,
1752 ) -> str:
1753  """Generate a blurb asking the user to file a bug report."""
1754  issue_tracker = async_get_issue_tracker(
1755  hass,
1756  integration=integration,
1757  integration_domain=integration_domain,
1758  module=module,
1759  )
1760 
1761  if not issue_tracker:
1762  if integration:
1763  integration_domain = integration.domain
1764  if not integration_domain:
1765  return "report it to the custom integration author"
1766  return (
1767  f"report it to the author of the '{integration_domain}' "
1768  "custom integration"
1769  )
1770 
1771  return f"create a bug report at {issue_tracker}"
None __init__(self, str|set[str] from_domain, str to_domain)
Definition: loader.py:1462
bool async_setup_entry(self, HomeAssistant hass, ConfigEntry config_entry)
Definition: loader.py:368
None __init__(self, HomeAssistant hass)
Definition: loader.py:1543
ModuleWrapper __getattr__(self, str comp_name)
Definition: loader.py:1547
None __init__(self, HomeAssistant hass)
Definition: loader.py:1582
ModuleWrapper __getattr__(self, str helper_name)
Definition: loader.py:1586
None __init__(self, str domain)
Definition: loader.py:1444
None __init__(self, str domain)
Definition: loader.py:1453
dict[str, list[str]]|None homekit(self)
Definition: loader.py:900
list[dict[str, str|bool]]|None dhcp(self)
Definition: loader.py:890
str|None issue_tracker(self)
Definition: loader.py:821
list[str] after_dependencies(self)
Definition: loader.py:801
list[str]|None loggers(self)
Definition: loader.py:826
list[dict[str, str]]|None ssdp(self)
Definition: loader.py:875
set[str] all_dependencies(self)
Definition: loader.py:932
ModuleType get_platform(self, str platform_name)
Definition: loader.py:1231
dict[str, ModuleType] async_get_platforms(self, Iterable[Platform|str] platform_names)
Definition: loader.py:1112
ModuleType _import_platform(self, str platform_name)
Definition: loader.py:1297
list[str]|None mqtt(self)
Definition: loader.py:870
ModuleType _load_platform(self, str platform_name)
Definition: loader.py:1258
None __init__(self, HomeAssistant hass, str pkg_path, pathlib.Path file_path, Manifest manifest, set[str]|None top_level_files=None)
Definition: loader.py:751
str|None documentation(self)
Definition: loader.py:816
bool platforms_are_loaded(self, Iterable[str] platform_names)
Definition: loader.py:1220
list[str] requirements(self)
Definition: loader.py:806
ComponentProtocol async_get_component(self)
Definition: loader.py:978
AwesomeVersion|None version(self)
Definition: loader.py:920
list[str] dependencies(self)
Definition: loader.py:796
list[dict[str, str]]|None usb(self)
Definition: loader.py:895
ComponentProtocol get_component(self)
Definition: loader.py:1049
list[str|dict[str, str]]|None zeroconf(self)
Definition: loader.py:880
json_fragment manifest_json_fragment(self)
Definition: loader.py:776
Integration|None resolve_from_root(cls, HomeAssistant hass, ModuleType root_module, str domain)
Definition: loader.py:653
Literal[ "entity", "device", "hardware", "helper", "hub", "service", "system", "virtual"] integration_type(self)
Definition: loader.py:848
ModuleType|None _get_platform_cached_or_raise(self, str platform_name)
Definition: loader.py:1206
list[str] platforms_exists(self, Iterable[str] platform_names)
Definition: loader.py:1237
ComponentProtocol _get_component(self, bool preload_platforms=False)
Definition: loader.py:1068
ModuleType async_get_platform(self, str platform_name)
Definition: loader.py:1101
ModuleType|None get_platform_cached(self, str platform_name)
Definition: loader.py:1227
str|None quality_scale(self)
Definition: loader.py:831
dict[str, ModuleType] _load_platforms(self, Iterable[str] platform_names)
Definition: loader.py:1094
bool all_dependencies_resolved(self)
Definition: loader.py:940
list[dict[str, str|int]]|None bluetooth(self)
Definition: loader.py:885
None __init__(self, HomeAssistant hass, ComponentProtocol module)
Definition: loader.py:1524
Any __getattr__(self, str attr)
Definition: loader.py:1529
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:128
bool async_migrate_entry(HomeAssistant hass, ConfigEntry config_entry)
Definition: __init__.py:23
bool setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:73
bool async_remove_config_entry_device(HomeAssistant hass, AugustConfigEntry config_entry, dr.DeviceEntry device_entry)
Definition: __init__.py:94
bool remove(self, _T matcher)
Definition: match.py:214
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_reset_platform(HomeAssistant hass, str integration_name)
Definition: __init__.py:460
None report_usage(str what, *str|None breaks_in_ha_version=None, ReportBehavior core_behavior=ReportBehavior.ERROR, ReportBehavior core_integration_behavior=ReportBehavior.LOG, ReportBehavior custom_integration_behavior=ReportBehavior.LOG, set[str]|None exclude_integrations=None, str|None integration_domain=None, int level=logging.WARNING)
Definition: frame.py:195
set[str] async_get_config_flows(HomeAssistant hass, Literal["device", "helper", "hub", "service"]|None type_filter=None)
Definition: loader.py:339
Manifest manifest_from_legacy_module(str domain, ModuleType module)
Definition: loader.py:275
Integration async_get_loaded_integration(HomeAssistant hass, str domain)
Definition: loader.py:1341
list[USBMatcher] async_get_usb(HomeAssistant hass)
Definition: loader.py:545
set[str] _async_component_dependencies(HomeAssistant hass, Integration integration)
Definition: loader.py:1623
dict[str, Integration|Exception] async_get_integrations(HomeAssistant hass, Iterable[str] domains)
Definition: loader.py:1368
bool homekit_always_discover(str|None iot_class)
Definition: loader.py:567
Integration|None async_get_issue_integration(HomeAssistant|None hass, str|None integration_domain)
Definition: loader.py:1693
None async_register_preload_platform(HomeAssistant hass, str platform_name)
Definition: loader.py:640
str|None async_get_issue_tracker(HomeAssistant|None hass, *Integration|None integration=None, str|None integration_domain=None, str|None module=None)
Definition: loader.py:1719
dict[str, list[str]] async_get_mqtt(HomeAssistant hass)
Definition: loader.py:624
dict[str, Any] async_get_integration_descriptions(HomeAssistant hass)
Definition: loader.py:408
ComponentProtocol|None _load_file(HomeAssistant hass, str comp_or_platform, list[str] base_paths)
Definition: loader.py:1471
list[str] _lookup_path(HomeAssistant hass)
Definition: loader.py:1677
list[str] async_get_application_credentials(HomeAssistant hass)
Definition: loader.py:453
str async_suggest_report_issue(HomeAssistant|None hass, *Integration|None integration=None, str|None integration_domain=None, str|None module=None)
Definition: loader.py:1752
bool is_component_module_loaded(HomeAssistant hass, str module)
Definition: loader.py:1684
None async_setup(HomeAssistant hass)
Definition: loader.py:266
Integration async_get_integration(HomeAssistant hass, str domain)
Definition: loader.py:1354
dict[str, list[dict[str, str]]] async_get_ssdp(HomeAssistant hass)
Definition: loader.py:609
None _async_mount_config_dir(HomeAssistant hass)
Definition: loader.py:1664
dict[str, Integration] _resolve_integrations_from_root(HomeAssistant hass, ModuleType root_module, Iterable[str] domains)
Definition: loader.py:1326
dict[str, Integration] _get_custom_components(HomeAssistant hass)
Definition: loader.py:286
ZeroconfMatcher async_process_zeroconf_match_dict(dict[str, Any] entry)
Definition: loader.py:467
dict[str, Integration] async_get_custom_components(HomeAssistant hass)
Definition: loader.py:317
bool _version_blocked(AwesomeVersion integration_version, BlockedIntegration blocked_integration)
Definition: loader.py:1313
dict[str, list[ZeroconfMatcher]] async_get_zeroconf(HomeAssistant hass)
Definition: loader.py:494
dict[str, HomeKitDiscoveredIntegration] async_get_homekit(HomeAssistant hass)
Definition: loader.py:583
list[DHCPMatcher] async_get_dhcp(HomeAssistant hass)
Definition: loader.py:531
list[BluetoothMatcher] async_get_bluetooth(HomeAssistant hass)
Definition: loader.py:515