1 """The methods for loading Home Assistant integrations.
3 This module has quite some complex parts. I have tried to add as much
4 documentation as possible to keep it understandable.
7 from __future__
import annotations
10 from collections.abc
import Callable, Iterable
11 from contextlib
import suppress
12 from dataclasses
import dataclass
13 import functools
as ft
20 from types
import ModuleType
21 from typing
import TYPE_CHECKING, Any, Literal, Protocol, TypedDict, cast
23 from awesomeversion
import (
25 AwesomeVersionException,
26 AwesomeVersionStrategy,
28 from propcache
import cached_property
29 import voluptuous
as vol
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
50 from .config_entries
import ConfigEntry
51 from .helpers
import device_registry
as dr
52 from .helpers.typing
import ConfigType
54 _LOGGER = logging.getLogger(__name__)
67 BASE_PRELOAD_PLATFORMS = [
86 """Blocked custom integration details."""
88 lowest_good_version: AwesomeVersion |
None
92 BLOCKED_CUSTOM_INTEGRATIONS: dict[str, BlockedIntegration] = {
98 AwesomeVersion(
"1.0.4"),
"crashes Home Assistant"
103 AwesomeVersion(
"1.0.13"),
"crashes Home Assistant"
110 AwesomeVersion(
"3.0.5.3"),
"prevents recorder from working"
115 AwesomeVersion(
"2.7.1"),
"prevents recorder from working"
120 AwesomeVersion(
"3.4.4"),
"prevents recorder from working"
124 DATA_COMPONENTS: HassKey[dict[str, ModuleType | ComponentProtocol]] =
HassKey(
127 DATA_INTEGRATIONS: HassKey[dict[str, Integration | asyncio.Future[
None]]] =
HassKey(
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"
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"
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"
150 MOVED_ZEROCONF_PROPS = (
"macaddress",
"model",
"manufacturer")
154 """Matcher for the dhcp integration for required fields."""
160 """Matcher for the dhcp integration for optional fields."""
164 registered_devices: bool
168 """Matcher for the dhcp integration."""
171 class BluetoothMatcherRequired(TypedDict,
total=
True):
172 """Matcher for the bluetooth integration for required fields."""
178 """Matcher for the bluetooth integration for optional fields."""
182 service_data_uuid: str
184 manufacturer_data_start: list[int]
189 """Matcher for the bluetooth integration."""
193 """Matcher for the usb integration for required fields."""
199 """Matcher for the usb integration for optional fields."""
209 """Matcher for the USB integration."""
212 @dataclass(slots=
True)
217 always_discover: bool
221 """Matcher for zeroconf."""
225 properties: dict[str, str]
229 """Integration manifest.
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.
239 integration_type: Literal[
240 "entity",
"device",
"hardware",
"helper",
"hub",
"service",
"system",
"virtual"
242 dependencies: list[str]
243 after_dependencies: list[str]
244 requirements: list[str]
250 bluetooth: list[dict[str, int | 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]]
258 overwrites_built_in: bool
260 codeowners: list[str]
262 import_executor: bool
263 single_config_entry: bool
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()
276 """Generate a manifest from a legacy module."""
280 "requirements": getattr(module,
"REQUIREMENTS", []),
281 "dependencies": getattr(module,
"DEPENDENCIES", []),
287 """Return list of custom integrations."""
288 if hass.config.recovery_mode
or hass.config.safe_mode:
292 import custom_components
298 for path
in custom_components.__path__
299 for entry
in pathlib.Path(path).iterdir()
306 [comp.name
for comp
in dirs],
309 integration.domain: integration
310 for integration
in integrations.values()
311 if integration
is not None
317 ) -> dict[str, Integration]:
318 """Return cached list of custom integrations."""
319 comps_or_future = hass.data.get(DATA_CUSTOM_COMPONENTS)
321 if comps_or_future
is None:
322 future = hass.data[DATA_CUSTOM_COMPONENTS] = hass.loop.create_future()
324 comps = await hass.async_add_executor_job(_get_custom_components, hass)
326 hass.data[DATA_CUSTOM_COMPONENTS] = comps
327 future.set_result(comps)
330 if isinstance(comps_or_future, asyncio.Future):
331 return await comps_or_future
333 return comps_or_future
338 type_filter: Literal[
"device",
"helper",
"hub",
"service"] |
None =
None,
340 """Return cached list of config flows."""
342 flows: set[str] = set()
344 if type_filter
is not None:
345 flows.update(FLOWS[type_filter])
347 for type_flows
in FLOWS.values():
348 flows.update(type_flows)
352 for integration
in integrations.values()
353 if integration.config_flow
354 and (type_filter
is None or integration.integration_type == type_filter)
361 """Define the format of an integration."""
363 CONFIG_SCHEMA: vol.Schema
367 self, hass: HomeAssistant, config_entry: ConfigEntry
369 """Set up a config entry."""
372 self, hass: HomeAssistant, config_entry: ConfigEntry
374 """Unload a config entry."""
377 self, hass: HomeAssistant, config_entry: ConfigEntry
379 """Migrate an old config entry."""
382 self, hass: HomeAssistant, config_entry: ConfigEntry
384 """Remove a config entry."""
389 config_entry: ConfigEntry,
390 device_entry: dr.DeviceEntry,
392 """Remove a config entry device."""
395 self, hass: HomeAssistant, integration_name: str
397 """Release resources."""
399 async
def async_setup(self, hass: HomeAssistant, config: ConfigType) -> bool:
400 """Set up integration."""
402 def setup(self, hass: HomeAssistant, config: ConfigType) -> bool:
403 """Set up integration."""
409 """Return cached list of integrations."""
410 base = generated.__path__[0]
411 config_flow_path = pathlib.Path(base) /
"integrations.json"
413 flow = await hass.async_add_executor_job(config_flow_path.read_text)
414 core_flows = cast(dict[str, Any],
json_loads(flow))
416 custom_flows: dict[str, Any] = {
421 for integration
in custom_integrations.values():
423 if integration.integration_type
in (
"entity",
"system"):
426 for integration_type
in (
"integration",
"helper"):
427 if integration.domain
not in core_flows[integration_type]:
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)
433 if integration.integration_type ==
"helper":
434 integration_key: str = integration.integration_type
436 integration_key =
"integration"
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
446 "overwrites_built_in": integration.overwrites_built_in,
448 custom_flows[integration_key][integration.domain] = metadata
450 return {
"core": core_flows,
"custom": custom_flows}
454 """Return cached list of application credentials."""
458 *APPLICATION_CREDENTIALS,
461 for integration
in integrations.values()
462 if "application_credentials" in integration.dependencies
468 """Handle backwards compat with zeroconf matchers."""
469 entry_without_type: dict[str, Any] = entry.copy()
470 del entry_without_type[
"type"]
473 for moved_prop
in MOVED_ZEROCONF_PROPS:
474 if value := entry_without_type.pop(moved_prop,
None):
477 'Matching the zeroconf property "%s" at top-level is deprecated and'
478 " should be moved into a properties dict; Check the developer"
483 if "properties" not in entry_without_type:
484 prop_dict: dict[str, str] = {}
485 entry_without_type[
"properties"] = prop_dict
487 prop_dict = entry_without_type[
"properties"]
488 prop_dict[moved_prop] = value.lower()
489 return cast(ZeroconfMatcher, entry_without_type)
494 ) -> dict[str, list[ZeroconfMatcher]]:
495 """Return cached list of zeroconf types."""
496 zeroconf: dict[str, list[ZeroconfMatcher]] = ZEROCONF.copy()
499 for integration
in integrations.values():
500 if not integration.zeroconf:
502 for entry
in integration.zeroconf:
503 data: ZeroconfMatcher = {
"domain": integration.domain}
504 if isinstance(entry, dict):
510 zeroconf.setdefault(typ, []).append(data)
516 """Return cached list of bluetooth types."""
517 bluetooth = cast(list[BluetoothMatcher], BLUETOOTH.copy())
520 for integration
in integrations.values():
521 if not integration.bluetooth:
523 for entry
in integration.bluetooth:
525 cast(BluetoothMatcher, {
"domain": integration.domain, **entry})
532 """Return cached list of dhcp types."""
533 dhcp = cast(list[DHCPMatcher], DHCP.copy())
536 for integration
in integrations.values():
537 if not integration.dhcp:
539 for entry
in integration.dhcp:
540 dhcp.append(cast(DHCPMatcher, {
"domain": integration.domain, **entry}))
546 """Return cached list of usb types."""
547 usb = cast(list[USBMatcher], USB.copy())
550 for integration
in integrations.values():
551 if not integration.usb:
553 for entry
in integration.usb:
558 "domain": integration.domain,
559 **{k: v
for k, v
in entry.items()
if k !=
"known_devices"},
568 """Return if we should always offer HomeKit control for a device."""
578 return not iot_class
or (iot_class.startswith(
"cloud")
or "polling" in iot_class)
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"])
589 for model, details
in HOMEKIT.items()
593 for integration
in integrations.values():
595 not integration.homekit
596 or "models" not in integration.homekit
597 or not integration.homekit[
"models"]
600 for model
in integration.homekit[
"models"]:
609 async
def async_get_ssdp(hass: HomeAssistant) -> dict[str, list[dict[str, str]]]:
610 """Return cached list of ssdp mappings."""
612 ssdp: dict[str, list[dict[str, str]]] = SSDP.copy()
615 for integration
in integrations.values():
616 if not integration.ssdp:
619 ssdp[integration.domain] = integration.ssdp
625 """Return cached list of MQTT mappings."""
627 mqtt: dict[str, list[str]] = MQTT.copy()
630 for integration
in integrations.values():
631 if not integration.mqtt:
634 mqtt[integration.domain] = integration.mqtt
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)
648 """An integration in Home Assistant."""
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"
658 if not manifest_path.is_file():
662 manifest = cast(Manifest,
json_loads(manifest_path.read_text()))
663 except JSON_DECODE_EXCEPTIONS
as err:
665 "Error parsing manifest.json file at %s: %s", manifest_path, err
669 file_path = manifest_path.parent
672 is_virtual = manifest.get(
"integration_type") ==
"virtual"
675 f
"{root_module.__name__}.{domain}",
678 None if is_virtual
else set(os.listdir(file_path)),
681 if not integration.import_executor:
682 _LOGGER.warning(IMPORT_EVENT_LOOP_WARNING, integration.domain)
684 if integration.is_built_in:
687 _LOGGER.warning(CUSTOM_WARNING, integration.domain)
689 if integration.version
is None:
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"
705 AwesomeVersionStrategy.CALVER,
706 AwesomeVersionStrategy.SEMVER,
707 AwesomeVersionStrategy.SIMPLEVER,
708 AwesomeVersionStrategy.BUILDVER,
709 AwesomeVersionStrategy.PEP440,
712 except AwesomeVersionException:
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"
726 if blocked := BLOCKED_CUSTOM_INTEGRATIONS.get(integration.domain):
730 "Version %s of custom integration '%s' %s and was blocked "
731 "from loading, please %s"
748 file_path: pathlib.Path,
750 top_level_files: set[str] |
None =
None,
752 """Initialize an integration."""
757 manifest[
"is_built_in"] = self.
is_built_inis_built_in
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]
773 _LOGGER.info(
"Loaded %s from %s", self.
domaindomain, pkg_path)
777 """Return manifest as a JSON fragment."""
783 return self.
manifestmanifest[
"name"]
787 """Return reason integration is disabled."""
793 return self.
manifestmanifest[
"domain"]
797 """Return dependencies."""
798 return self.
manifestmanifest.
get(
"dependencies", [])
802 """Return after_dependencies."""
803 return self.
manifestmanifest.
get(
"after_dependencies", [])
807 """Return requirements."""
808 return self.
manifestmanifest.
get(
"requirements", [])
812 """Return config_flow."""
813 return self.
manifestmanifest.
get(
"config_flow")
or False
817 """Return documentation."""
822 """Return issue tracker link."""
827 """Return list of loggers used by the integration."""
832 """Return Integration Quality Scale."""
840 """Return the integration IoT Class."""
847 "entity", "device", "hardware", "helper", "hub", "service", "system", "virtual"
849 """Return the integration type."""
850 return self.
manifestmanifest.
get(
"integration_type",
"hub")
854 """Import integration in the executor."""
857 return self.
manifestmanifest.
get(
"import_executor",
True)
861 """Return if the integration has translations."""
866 """Return if the integration has services."""
870 def mqtt(self) -> list[str] | None:
871 """Return Integration MQTT entries."""
875 def ssdp(self) -> list[dict[str, str]] | None:
876 """Return Integration SSDP entries."""
880 def zeroconf(self) -> list[str | dict[str, str]] | None:
881 """Return Integration zeroconf entries."""
885 def bluetooth(self) -> list[dict[str, str | int]] | None:
886 """Return Integration bluetooth entries."""
890 def dhcp(self) -> list[dict[str, str | bool]] | None:
891 """Return Integration dhcp entries."""
895 def usb(self) -> list[dict[str, str]] | None:
896 """Return Integration usb entries."""
900 def homekit(self) -> dict[str, list[str]] | None:
901 """Return Integration homekit entries."""
906 """Test if package is a built-in integration."""
907 return self.
pkg_pathpkg_path.startswith(PACKAGE_BUILTIN)
911 """Return if package overwrites a built-in integration."""
915 pathlib.Path(__file__).parent /
"components" / self.
domaindomain /
"manifest.json"
917 return core_comp_path.is_file()
921 """Return the version of the integration."""
922 if "version" not in self.
manifestmanifest:
924 return AwesomeVersion(self.
manifestmanifest[
"version"])
928 """Return if the integration supports a single config entry only."""
929 return self.
manifestmanifest.
get(
"single_config_entry",
False)
933 """Return all dependencies including sub-dependencies."""
935 raise RuntimeError(
"Dependencies not resolved!")
941 """Return if all dependencies have been resolved."""
945 """Resolve all dependencies."""
952 except IntegrationNotFound
as err:
955 "Unable to resolve dependencies for %s: unable to resolve"
956 " (sub)dependency %s"
961 except CircularDependency
as err:
964 "Unable to resolve dependencies for %s: it contains a circular"
965 " dependency: %s -> %s"
972 dependencies.discard(self.
domaindomain)
979 """Return the component.
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.
985 domain = self.
domaindomain
986 if domain
in (cache := self.
_cache_cache):
992 if debug := _LOGGER.isEnabledFor(logging.DEBUG):
993 start = time.perf_counter()
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)
1001 if not load_executor:
1005 "Component %s import took %.3f seconds (loaded_executor=False)",
1007 time.perf_counter() - start,
1014 comp = await self.
hasshass.async_add_import_executor_job(
1017 except ModuleNotFoundError:
1019 except ImportError
as ex:
1020 load_executor =
False
1022 "Failed to import %s in executor", self.
domaindomain, exc_info=ex
1028 except BaseException
as ex:
1030 with suppress(BaseException):
1041 "Component %s import took %.3f seconds (loaded_executor=%s)",
1043 time.perf_counter() - start,
1050 """Return the component.
1052 This method must be thread-safe as it's called from the executor
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.
1059 This is mostly a thin wrapper around importlib.import_module
1060 with a dict cache which is thread-safe since importlib has
1063 domain = self.
domaindomain
1064 if domain
in (cache := self.
_cache_cache):
1065 return cache[domain]
1069 """Return the component."""
1070 cache = self.
_cache_cache
1071 domain = self.
domaindomain
1073 cache[domain] = cast(
1074 ComponentProtocol, importlib.import_module(self.
pkg_pathpkg_path)
1078 except RuntimeError
as err:
1080 raise ImportError(f
"RuntimeError importing {self.pkg_path}: {err}")
from err
1081 except Exception
as err:
1083 "Unexpected exception importing component %s", self.
pkg_pathpkg_path
1085 raise ImportError(f
"Exception importing {self.pkg_path}")
from err
1087 if preload_platforms:
1089 with suppress(ImportError):
1092 return cache[domain]
1095 """Load platforms for an integration."""
1098 for platform_name
in platform_names
1102 """Return a platform for an integration."""
1105 if platform := self.
_cache_cache.
get(f
"{self.domain}.{platform_name}"):
1108 return platforms[platform_name]
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] = {}
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]]] = []
1122 for platform_name
in platform_names:
1124 platforms[platform_name] = platform
1128 if future := self._import_futures.
get(platform_name):
1129 in_progress_imports[platform_name] = future
1132 full_name = f
"{domain}.{platform_name}"
1135 and full_name
not in self.
hasshass.config.components
1136 and f
"{self.pkg_path}.{platform_name}" not in sys.modules
1138 load_executor_platforms.append(platform_name)
1140 load_event_loop_platforms.append(platform_name)
1142 import_future = self.
hasshass.loop.create_future()
1143 self._import_futures[platform_name] = import_future
1144 import_futures.append((platform_name, import_future))
1146 if load_executor_platforms
or load_event_loop_platforms:
1147 if debug := _LOGGER.isEnabledFor(logging.DEBUG):
1148 start = time.perf_counter()
1151 if load_executor_platforms:
1154 await self.
hasshass.async_add_import_executor_job(
1158 except ModuleNotFoundError:
1160 except ImportError
as ex:
1162 "Failed to import %s platforms %s in executor",
1164 load_executor_platforms,
1169 load_event_loop_platforms.extend(load_executor_platforms)
1171 if load_event_loop_platforms:
1174 for platform_name, import_future
in import_futures:
1175 import_future.set_result(platforms[platform_name])
1177 except BaseException
as ex:
1178 for _, import_future
in import_futures:
1179 import_future.set_exception(ex)
1180 with suppress(BaseException):
1184 import_future.result()
1188 for platform_name, _
in import_futures:
1189 self._import_futures.pop(platform_name)
1193 "Importing platforms for %s executor=%s loop=%s took %.2fs",
1195 load_executor_platforms,
1196 load_event_loop_platforms,
1197 time.perf_counter() - start,
1200 if in_progress_imports:
1201 for platform_name, future
in in_progress_imports.items():
1202 platforms[platform_name] = await future
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:
1212 return self.
_cache_cache[full_name]
1214 raise ModuleNotFoundError(
1215 f
"Platform {full_name} not found",
1216 name=f
"{self.pkg_path}.{platform_name}",
1221 """Check if a platforms are loaded for an integration."""
1223 f
"{self.domain}.{platform_name}" in self.
_cache_cache
1224 for platform_name
in platform_names
1228 """Return a platform for an integration from cache."""
1229 return self.
_cache_cache.
get(f
"{self.domain}.{platform_name}")
1232 """Return a platform for an integration."""
1238 """Check if a platforms exists for an integration.
1240 This method is thread-safe and can be called from the executor
1241 or event loop without doing blocking I/O.
1244 domain = self.
domaindomain
1245 existing_platforms: list[str] = []
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
1252 existing_platforms.append(platform_name)
1254 missing_platforms[full_name] =
True
1256 return existing_platforms
1259 """Load a platform for an integration.
1261 This method must be thread-safe as it's called from the executor
1264 This is mostly a thin wrapper around importlib.import_module
1265 with a dict cache which is thread-safe since importlib has
1268 full_name = f
"{self.domain}.{platform_name}"
1269 cache = self.
hasshass.data[DATA_COMPONENTS]
1272 except ModuleNotFoundError:
1273 if self.
domaindomain
in cache:
1280 except RuntimeError
as err:
1283 f
"RuntimeError importing {self.pkg_path}.{platform_name}: {err}"
1285 except Exception
as err:
1287 "Unexpected exception importing platform %s.%s",
1292 f
"Exception importing {self.pkg_path}.{platform_name}"
1295 return cast(ModuleType, cache[full_name])
1298 """Import the platform.
1300 This method must be thread-safe as it's called from the executor
1303 return importlib.import_module(f
"{self.pkg_path}.{platform_name}")
1306 """Text representation of class."""
1307 return f
"<Integration {self.domain}: {self.pkg_path}>"
1311 integration_version: AwesomeVersion,
1312 blocked_integration: BlockedIntegration,
1314 """Return True if the integration version is blocked."""
1315 if blocked_integration.lowest_good_version
is None:
1318 if integration_version >= blocked_integration.lowest_good_version:
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:
1331 integration = Integration.resolve_from_root(hass, root_module, domain)
1333 _LOGGER.exception(
"Error loading integration: %s", domain)
1336 integrations[domain] = integration
1342 """Get an integration which is already loaded.
1344 Raises IntegrationNotLoaded if the integration is not loaded.
1346 cache = hass.data[DATA_INTEGRATIONS]
1347 int_or_fut = cache.get(domain, UNDEFINED)
1349 if type(int_or_fut)
is Integration:
1355 """Get integration."""
1356 cache = hass.data[DATA_INTEGRATIONS]
1357 if type(int_or_fut := cache.get(domain, UNDEFINED))
is Integration:
1360 int_or_exc = integrations_or_excs[domain]
1361 if isinstance(int_or_exc, Integration):
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)
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)
1382 results[domain] = ValueError(f
"Invalid domain {domain}")
1384 needed[domain] = cache[domain] = hass.loop.create_future()
1387 await asyncio.wait(in_progress.values())
1388 for domain
in in_progress:
1392 if (int_or_fut := cache.get(domain, UNDEFINED))
is UNDEFINED:
1395 results[domain] = cast(Integration, int_or_fut)
1404 for domain, future
in needed.items():
1405 if integration := custom.get(domain):
1406 results[domain] = cache[domain] = integration
1407 future.set_result(
None)
1409 for domain
in results:
1410 if domain
in needed:
1415 from .
import components
1417 integrations = await hass.async_add_executor_job(
1418 _resolve_integrations_from_root, hass, components, needed
1420 for domain, future
in needed.items():
1421 int_or_exc = integrations.get(domain)
1425 elif isinstance(int_or_exc, Exception):
1428 exc.__cause__ = int_or_exc
1429 results[domain] = exc
1431 results[domain] = cache[domain] = int_or_exc
1432 future.set_result(
None)
1438 """Loader base error."""
1441 class IntegrationNotFound(LoaderError):
1442 """Raised when a component is not found."""
1445 """Initialize a component not found error."""
1446 super().
__init__(f
"Integration '{domain}' not found.")
1451 """Raised when a component is not loaded."""
1454 """Initialize a component not found error."""
1455 super().
__init__(f
"Integration '{domain}' not loaded.")
1460 """Raised when a circular dependency is found when resolving components."""
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}.")
1470 hass: HomeAssistant, comp_or_platform: str, base_paths: list[str]
1471 ) -> ComponentProtocol |
None:
1472 """Try to load specified file.
1474 Looks in config dir first, then built-in components.
1475 Only returns it if also found to be valid.
1478 cache = hass.data[DATA_COMPONENTS]
1479 if module := cache.get(comp_or_platform):
1480 return cast(ComponentProtocol, module)
1482 for path
in (f
"{base}.{comp_or_platform}" for base
in base_paths):
1484 module = importlib.import_module(path)
1495 if getattr(module,
"__file__",
None)
is None:
1498 cache[comp_or_platform] = module
1500 return cast(ComponentProtocol, module)
1502 except ImportError
as err:
1507 white_listed_errors = []
1509 for part
in path.split(
"."):
1511 white_listed_errors.append(f
"No module named '{'.'.join(parts)}'")
1513 if str(err)
not in white_listed_errors:
1515 "Error loading %s. Make sure all dependencies are installed", path
1522 """Class to wrap a Python module and auto fill in hass argument."""
1524 def __init__(self, hass: HomeAssistant, module: ComponentProtocol) ->
None:
1525 """Initialize the module wrapper."""
1530 """Fetch an attribute."""
1531 value = getattr(self.
_module_module, attr)
1533 if hasattr(value,
"__bind_hass"):
1534 value = ft.partial(value, self.
_hass_hass)
1536 setattr(self, attr, value)
1541 """Helper to load components."""
1544 """Initialize the Components class."""
1548 """Fetch a component."""
1550 integration = self.
_hass_hass.data[DATA_INTEGRATIONS].
get(comp_name)
1552 if isinstance(integration, Integration):
1553 component: ComponentProtocol |
None = integration.get_component()
1558 if component
is None:
1559 raise ImportError(f
"Unable to load {comp_name}")
1563 from .helpers.frame
import ReportBehavior, 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",
1575 setattr(self, comp_name, wrapped)
1580 """Helper to load helpers."""
1583 """Initialize the Helpers class."""
1587 """Fetch a helper."""
1588 helper = importlib.import_module(f
"homeassistant.helpers.{helper_name}")
1592 from .helpers.frame
import ReportBehavior, report_usage
1596 f
"accesses hass.helpers.{helper_name}, which"
1597 f
" should be updated to import functions used from {helper_name} directly"
1599 core_behavior=ReportBehavior.IGNORE,
1600 core_integration_behavior=ReportBehavior.IGNORE,
1601 custom_integration_behavior=ReportBehavior.LOG,
1602 breaks_in_ha_version=
"2025.5",
1606 setattr(self, helper_name, wrapped)
1610 def bind_hass[_CallableT: Callable[..., Any]](func: _CallableT) -> _CallableT:
1611 """Decorate function to indicate that first argument is hass.
1613 The use of this decorator is discouraged, and it should not be used
1616 setattr(func,
"__bind_hass",
True)
1621 hass: HomeAssistant,
1622 integration: Integration,
1624 """Get component dependencies."""
1625 loading: set[str] = set()
1626 loaded: set[str] = set()
1628 async
def component_dependencies_impl(integration: Integration) ->
None:
1629 """Recursively get component dependencies."""
1630 domain = integration.domain
1631 if not (dependencies := integration.dependencies):
1637 for dependency_domain, dep_integration
in dep_integrations.items():
1638 if isinstance(dep_integration, Exception):
1639 raise dep_integration
1644 if conflict := loading.intersection(dep_integration.after_dependencies):
1648 if dependency_domain
in loaded:
1652 if dependency_domain
in loading:
1655 await component_dependencies_impl(dep_integration)
1656 loading.remove(domain)
1659 await component_dependencies_impl(integration)
1665 """Mount config dir in order to load custom_component.
1667 Async friendly but not a coroutine.
1670 sys.path.insert(0, hass.config.config_dir)
1671 with suppress(ImportError):
1672 import custom_components
1673 sys.path.remove(hass.config.config_dir)
1674 sys.path_importer_cache.pop(hass.config.config_dir,
None)
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]
1685 """Test if a component module is loaded."""
1686 return module
in hass.data[DATA_COMPONENTS]
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:
1700 if (comps_or_future := hass.data.get(DATA_CUSTOM_COMPONENTS))
and not isinstance(
1701 comps_or_future, asyncio.Future
1703 integration = comps_or_future.get(integration_domain)
1706 with suppress(IntegrationNotLoaded):
1714 hass: HomeAssistant |
None,
1716 integration: Integration |
None =
None,
1717 integration_domain: str |
None =
None,
1718 module: str |
None =
None,
1720 """Return a URL for an integration's issue tracker."""
1722 "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue"
1724 if not integration
and not integration_domain
and not module:
1726 return issue_tracker
1731 if integration
and not integration.is_built_in:
1732 return integration.issue_tracker
1734 if module
and "custom_components" in module:
1738 integration_domain = integration.domain
1740 if integration_domain:
1741 issue_tracker += f
"+label%3A%22integration%3A+{integration_domain}%22"
1742 return issue_tracker
1747 hass: HomeAssistant |
None,
1749 integration: Integration |
None =
None,
1750 integration_domain: str |
None =
None,
1751 module: str |
None =
None,
1753 """Generate a blurb asking the user to file a bug report."""
1756 integration=integration,
1757 integration_domain=integration_domain,
1761 if not issue_tracker:
1763 integration_domain = integration.domain
1764 if not integration_domain:
1765 return "report it to the custom integration author"
1767 f
"report it to the author of the '{integration_domain}' "
1768 "custom integration"
1771 return f
"create a bug report at {issue_tracker}"
None __init__(self, str|set[str] from_domain, str to_domain)
bool async_setup_entry(self, HomeAssistant hass, ConfigEntry config_entry)
None __init__(self, HomeAssistant hass)
ModuleWrapper __getattr__(self, str comp_name)
None __init__(self, HomeAssistant hass)
ModuleWrapper __getattr__(self, str helper_name)
None __init__(self, str domain)
None __init__(self, str domain)
dict[str, list[str]]|None homekit(self)
list[dict[str, str|bool]]|None dhcp(self)
str|None issue_tracker(self)
list[str] after_dependencies(self)
list[str]|None loggers(self)
list[dict[str, str]]|None ssdp(self)
set[str] all_dependencies(self)
ModuleType get_platform(self, str platform_name)
bool overwrites_built_in(self)
dict[str, ModuleType] async_get_platforms(self, Iterable[Platform|str] platform_names)
ModuleType _import_platform(self, str platform_name)
list[str]|None mqtt(self)
ModuleType _load_platform(self, str platform_name)
None __init__(self, HomeAssistant hass, str pkg_path, pathlib.Path file_path, Manifest manifest, set[str]|None top_level_files=None)
str|None documentation(self)
bool platforms_are_loaded(self, Iterable[str] platform_names)
_all_dependencies_resolved
list[str] requirements(self)
ComponentProtocol async_get_component(self)
AwesomeVersion|None version(self)
list[str] dependencies(self)
list[dict[str, str]]|None usb(self)
ComponentProtocol get_component(self)
list[str|dict[str, str]]|None zeroconf(self)
json_fragment manifest_json_fragment(self)
Integration|None resolve_from_root(cls, HomeAssistant hass, ModuleType root_module, str domain)
Literal[ "entity", "device", "hardware", "helper", "hub", "service", "system", "virtual"] integration_type(self)
ModuleType|None _get_platform_cached_or_raise(self, str platform_name)
list[str] platforms_exists(self, Iterable[str] platform_names)
ComponentProtocol _get_component(self, bool preload_platforms=False)
bool has_translations(self)
ModuleType async_get_platform(self, str platform_name)
ModuleType|None get_platform_cached(self, str platform_name)
str|None quality_scale(self)
bool single_config_entry(self)
dict[str, ModuleType] _load_platforms(self, Iterable[str] platform_names)
bool import_executor(self)
bool resolve_dependencies(self)
bool all_dependencies_resolved(self)
list[dict[str, str|int]]|None bluetooth(self)
None __init__(self, HomeAssistant hass, ComponentProtocol module)
Any __getattr__(self, str attr)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
bool async_migrate_entry(HomeAssistant hass, ConfigEntry config_entry)
bool setup(HomeAssistant hass, ConfigType config)
bool async_remove_config_entry_device(HomeAssistant hass, AugustConfigEntry config_entry, dr.DeviceEntry device_entry)
bool remove(self, _T matcher)
web.Response get(self, web.Request request, str config_key)
None async_reset_platform(HomeAssistant hass, str integration_name)
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)
set[str] async_get_config_flows(HomeAssistant hass, Literal["device", "helper", "hub", "service"]|None type_filter=None)
Manifest manifest_from_legacy_module(str domain, ModuleType module)
Integration async_get_loaded_integration(HomeAssistant hass, str domain)
list[USBMatcher] async_get_usb(HomeAssistant hass)
set[str] _async_component_dependencies(HomeAssistant hass, Integration integration)
dict[str, Integration|Exception] async_get_integrations(HomeAssistant hass, Iterable[str] domains)
bool homekit_always_discover(str|None iot_class)
Integration|None async_get_issue_integration(HomeAssistant|None hass, str|None integration_domain)
None async_register_preload_platform(HomeAssistant hass, str platform_name)
str|None async_get_issue_tracker(HomeAssistant|None hass, *Integration|None integration=None, str|None integration_domain=None, str|None module=None)
dict[str, list[str]] async_get_mqtt(HomeAssistant hass)
dict[str, Any] async_get_integration_descriptions(HomeAssistant hass)
ComponentProtocol|None _load_file(HomeAssistant hass, str comp_or_platform, list[str] base_paths)
list[str] _lookup_path(HomeAssistant hass)
list[str] async_get_application_credentials(HomeAssistant hass)
str async_suggest_report_issue(HomeAssistant|None hass, *Integration|None integration=None, str|None integration_domain=None, str|None module=None)
bool is_component_module_loaded(HomeAssistant hass, str module)
None async_setup(HomeAssistant hass)
Integration async_get_integration(HomeAssistant hass, str domain)
dict[str, list[dict[str, str]]] async_get_ssdp(HomeAssistant hass)
None _async_mount_config_dir(HomeAssistant hass)
dict[str, Integration] _resolve_integrations_from_root(HomeAssistant hass, ModuleType root_module, Iterable[str] domains)
dict[str, Integration] _get_custom_components(HomeAssistant hass)
ZeroconfMatcher async_process_zeroconf_match_dict(dict[str, Any] entry)
dict[str, Integration] async_get_custom_components(HomeAssistant hass)
bool _version_blocked(AwesomeVersion integration_version, BlockedIntegration blocked_integration)
dict[str, list[ZeroconfMatcher]] async_get_zeroconf(HomeAssistant hass)
dict[str, HomeKitDiscoveredIntegration] async_get_homekit(HomeAssistant hass)
list[DHCPMatcher] async_get_dhcp(HomeAssistant hass)
list[BluetoothMatcher] async_get_bluetooth(HomeAssistant hass)