How the Home Assistant Integration Loader Discovers and Loads Custom Components
The Home Assistant integration loader discovers custom components by importing the custom_components package, scanning its subdirectories for valid manifest.json files, and caching Integration objects that load Python code on demand via thread-pool executors.
The integration loader in the home-assistant/core repository manages both built-in and user-provided extensions. Understanding how it discovers and loads custom components is essential for developers debugging integration issues or building custom automation platforms.
The Three-Phase Discovery Process
The loader implements a three-phase pipeline to isolate custom code while maintaining fast lookup times. In homeassistant/loader.py, the process splits filesystem scanning from metadata resolution to prevent blocking the event loop during startup.
- Filesystem scan –
async_get_custom_components()delegates to_get_custom_components()to enumerate candidate domains. - Metadata resolution – Each candidate passes through
Integration.resolve_from_root()to validate manifests. - Caching and exposure – Valid integrations populate
hass.data[DATA_CUSTOM_COMPONENTS]for subsequent lookups.
Phase 1: Scanning the Filesystem with async_get_custom_components
The entry point async_get_custom_components() (lines 27-45 in homeassistant/loader.py) ensures the scan runs exactly once per Home Assistant instance. It creates a future in hass.data[DATA_CUSTOM_COMPONENTS] and executes the synchronous helper _get_custom_components() within the executor thread pool via hass.async_add_executor_job().
The synchronous scanner first checks for recovery mode or safe mode, returning an empty dictionary immediately if either is active. It then attempts import custom_components. If the import fails (indicating no custom components folder exists), the function returns an empty mapping. When the package exists, the code iterates over every subdirectory using pathlib.Path(entry).iterdir() to build a list of candidate domain names.
Phase 2: Resolving Integration Metadata
For each candidate domain, the loader invokes Integration.resolve_from_root() (lines 63-99) to transform a directory name into a validated Integration object. This method performs three critical validations:
- Manifest presence: The loader expects
custom_components/<domain>/manifest.jsonand parses it usingjson_loads()into a typedManifestdictionary. - Version validation: Lines 102-124 enforce strict version checking. If the manifest lacks a
versionkey or contains a malformed version string, the integration is rejected immediately. - Blocked integration check: The domain and version are validated against
BLOCKED_CUSTOM_INTEGRATIONS(lines 94-134). If the integration is known to break Home Assistant, the loader aborts loading before any Python code executes.
Upon validation, the Integration object stores the package path as custom_components.<domain>, the manifest data, and a set of top-level files used later to identify available platforms.
Phase 3: Caching and Lazy Loading
Valid Integration objects are stored in hass.data[DATA_CUSTOM_COMPONENTS], creating a persistent cache that eliminates repeated filesystem scans. When Home Assistant needs an integration, it calls async_get_integration() (lines 63-73), which first checks this custom component cache. If the domain is absent, the loader falls back to the built-in homeassistant.components package via _resolve_integrations_from_root().
Actual Python code loading occurs lazily through Integration.async_get_component(). If the integration’s import_executor flag is true (the default), the import executes in a thread-pool executor via hass.async_add_import_executor_job(). Otherwise, it loads directly in the event loop. Platform modules (e.g., sensor_platform.py) load on demand via async_get_platform() or async_get_platforms(), respecting the same executor flag to prevent blocking.
Loading Custom Integration Code
Once cached, integrating a custom component into the running system requires explicit module loading. The Integration object provides methods to load both the core module and specific platforms without manual import statements.
import homeassistant.loader
# 1. Populate the custom component cache
custom_integrations = await homeassistant.loader.async_get_custom_components(hass)
# 2. Retrieve the Integration metadata object
integration = await homeassistant.loader.async_get_integration(hass, "my_custom_sensor")
# 3. Load the component's __init__.py module
component = await integration.async_get_component()
# 4. Load a specific platform (e.g., sensor)
sensor_platform = await integration.async_get_platform("sensor")
This workflow mirrors the internal path Home Assistant uses during config flow discovery and entity platform setup.
Summary
- The integration loader scans
custom_componentsviaasync_get_custom_components()inhomeassistant/loader.py, skipping the scan in safe or recovery mode. - Each domain resolves through
Integration.resolve_from_root(), which validatesmanifest.json, enforces version requirements, and checks againstBLOCKED_CUSTOM_INTEGRATIONS. - Valid integrations cache in
hass.data[DATA_CUSTOM_COMPONENTS], withasync_get_integration()falling back to core components if no custom match exists. - Python code loads lazily via
async_get_component()andasync_get_platform(), respecting theimport_executorflag to manage thread safety.
Frequently Asked Questions
Where should custom components be placed for the loader to find them?
The loader expects a custom_components directory at the same level as the core Home Assistant installation. Each integration must reside in its own subdirectory (e.g., custom_components/my_integration/) containing a manifest.json file and an __init__.py module. If the custom_components package cannot be imported, the loader returns an empty dictionary and continues with core components only.
What happens if a custom component's manifest.json is invalid or missing?
The Integration.resolve_from_root() method rejects any domain lacking a valid manifest.json. Specifically, lines 102-124 in homeassistant/loader.py enforce that the manifest must contain a valid version key. Missing or malformed manifests cause the loader to skip that directory entirely without caching an Integration object, effectively excluding it from the available integration list.
How does Home Assistant handle naming conflicts between custom and core components?
The loader checks the custom component cache first via async_get_integration(). If a domain exists in both custom_components and the built-in homeassistant.components package, the custom version takes precedence. Only if the domain is absent from the custom cache does the loader fall back to resolving from the core package root.
Can custom components block the Home Assistant event loop during loading?
By default, no. The import_executor flag in the Integration object defaults to true, causing async_get_component() and async_get_platform() to execute imports within a dedicated thread-pool executor using hass.async_add_import_executor_job(). Developers can override this behavior, but the default design prevents long-running or poorly written custom integrations from freezing the event loop during startup.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →