mirror of
https://github.com/home-assistant/core.git
synced 2026-07-09 07:45:11 +01:00
8198aa263b
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
378 lines
13 KiB
Python
378 lines
13 KiB
Python
"""Helpers to help with integration platforms."""
|
|
|
|
import asyncio
|
|
from collections.abc import Awaitable, Callable
|
|
from dataclasses import dataclass
|
|
from functools import partial
|
|
import logging
|
|
from types import ModuleType
|
|
from typing import Any
|
|
|
|
from homeassistant.const import EVENT_COMPONENT_LOADED
|
|
from homeassistant.core import Event, HassJob, HomeAssistant, callback
|
|
from homeassistant.loader import (
|
|
Integration,
|
|
async_get_integration,
|
|
async_get_integrations,
|
|
async_get_loaded_integration,
|
|
async_register_preload_platform,
|
|
)
|
|
from homeassistant.setup import ATTR_COMPONENT, EventComponentLoaded
|
|
from homeassistant.util.hass_dict import HassKey
|
|
from homeassistant.util.logging import catch_log_exception
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
DATA_INTEGRATION_PLATFORMS: HassKey[list[IntegrationPlatform]] = HassKey(
|
|
"integration_platforms"
|
|
)
|
|
|
|
|
|
@dataclass(slots=True, frozen=True)
|
|
class IntegrationPlatform:
|
|
"""An integration platform."""
|
|
|
|
platform_name: str
|
|
process_job: HassJob[[HomeAssistant, str, Any], Awaitable[None] | None]
|
|
seen_components: set[str]
|
|
|
|
|
|
@callback
|
|
def _async_integration_platform_component_loaded(
|
|
hass: HomeAssistant,
|
|
integration_platforms: list[IntegrationPlatform],
|
|
event: Event[EventComponentLoaded],
|
|
) -> None:
|
|
"""Process integration platforms for a component."""
|
|
if "." in (component_name := event.data[ATTR_COMPONENT]):
|
|
return
|
|
|
|
integration = async_get_loaded_integration(hass, component_name)
|
|
# First filter out platforms that the integration already processed.
|
|
integration_platforms_by_name: dict[str, IntegrationPlatform] = {}
|
|
for integration_platform in integration_platforms:
|
|
if component_name in integration_platform.seen_components:
|
|
continue
|
|
integration_platform.seen_components.add(component_name)
|
|
integration_platforms_by_name[integration_platform.platform_name] = (
|
|
integration_platform
|
|
)
|
|
|
|
if not integration_platforms_by_name:
|
|
return
|
|
|
|
# Next, check which platforms exist for this integration.
|
|
platforms_that_exist = integration.platforms_exists(integration_platforms_by_name)
|
|
if not platforms_that_exist:
|
|
return
|
|
|
|
# If everything is already loaded, we can avoid creating a task.
|
|
can_use_cache = True
|
|
platforms: dict[str, ModuleType] = {}
|
|
for platform_name in platforms_that_exist:
|
|
if platform := integration.get_platform_cached(platform_name):
|
|
platforms[platform_name] = platform
|
|
else:
|
|
can_use_cache = False
|
|
break
|
|
|
|
if can_use_cache:
|
|
_process_integration_platforms(
|
|
hass,
|
|
integration,
|
|
platforms,
|
|
integration_platforms_by_name,
|
|
)
|
|
return
|
|
|
|
# At least one of the platforms is not loaded, we need to load them
|
|
# so we have to fall back to creating a task.
|
|
hass.async_create_task_internal(
|
|
_async_process_integration_platforms_for_component(
|
|
hass, integration, platforms_that_exist, integration_platforms_by_name
|
|
),
|
|
eager_start=True,
|
|
)
|
|
|
|
|
|
async def _async_process_integration_platforms_for_component(
|
|
hass: HomeAssistant,
|
|
integration: Integration,
|
|
platforms_that_exist: list[str],
|
|
integration_platforms_by_name: dict[str, IntegrationPlatform],
|
|
) -> None:
|
|
"""Process integration platforms for a component."""
|
|
# Now we know which platforms to load, let's load them.
|
|
try:
|
|
platforms = await integration.async_get_platforms(platforms_that_exist)
|
|
except ImportError:
|
|
_LOGGER.debug(
|
|
"Unexpected error importing integration platforms for %s",
|
|
integration.domain,
|
|
)
|
|
return
|
|
|
|
if futures := _process_integration_platforms(
|
|
hass,
|
|
integration,
|
|
platforms,
|
|
integration_platforms_by_name,
|
|
):
|
|
await asyncio.gather(*futures)
|
|
|
|
|
|
@callback
|
|
def _process_integration_platforms(
|
|
hass: HomeAssistant,
|
|
integration: Integration,
|
|
platforms: dict[str, ModuleType],
|
|
integration_platforms_by_name: dict[str, IntegrationPlatform],
|
|
) -> list[asyncio.Future[Awaitable[None] | None]]:
|
|
"""Process integration platforms for a component.
|
|
|
|
Only the platforms that are passed in will be processed.
|
|
"""
|
|
return [
|
|
future
|
|
for platform_name, platform in platforms.items()
|
|
if (integration_platform := integration_platforms_by_name[platform_name])
|
|
and (
|
|
future := hass.async_run_hass_job(
|
|
integration_platform.process_job,
|
|
hass,
|
|
integration.domain,
|
|
platform,
|
|
)
|
|
)
|
|
]
|
|
|
|
|
|
def _format_err(name: str, platform_name: str, *args: Any) -> str:
|
|
"""Format error message."""
|
|
return f"Exception in {name} when processing platform '{platform_name}': {args}"
|
|
|
|
|
|
async def _async_import_platform(
|
|
integration: Integration, platform_name: str
|
|
) -> ModuleType | None:
|
|
"""Import a single platform for an integration.
|
|
|
|
Returns None if the integration does not provide the platform or it could
|
|
not be imported.
|
|
"""
|
|
if not integration.platforms_exists((platform_name,)):
|
|
return None
|
|
try:
|
|
return await integration.async_get_platform(platform_name)
|
|
except ImportError:
|
|
_LOGGER.debug(
|
|
"Error importing %s platform for %s", platform_name, integration.domain
|
|
)
|
|
return None
|
|
|
|
|
|
async def async_process_integration_platforms(
|
|
hass: HomeAssistant,
|
|
platform_name: str,
|
|
# Any = platform.
|
|
process_platform: Callable[[HomeAssistant, str, Any], Awaitable[None] | None],
|
|
wait_for_platforms: bool = False,
|
|
) -> None:
|
|
"""Process a specific platform for all current and future loaded integrations."""
|
|
if DATA_INTEGRATION_PLATFORMS not in hass.data:
|
|
integration_platforms = hass.data[DATA_INTEGRATION_PLATFORMS] = []
|
|
hass.bus.async_listen(
|
|
EVENT_COMPONENT_LOADED,
|
|
partial(
|
|
_async_integration_platform_component_loaded,
|
|
hass,
|
|
integration_platforms,
|
|
),
|
|
)
|
|
else:
|
|
integration_platforms = hass.data[DATA_INTEGRATION_PLATFORMS]
|
|
|
|
# Tell the loader that it should try to pre-load the integration
|
|
# for any future components that are loaded so we can reduce the
|
|
# amount of import executor usage.
|
|
async_register_preload_platform(hass, platform_name)
|
|
top_level_components = hass.config.top_level_components.copy()
|
|
process_job = HassJob(
|
|
catch_log_exception(
|
|
process_platform,
|
|
partial(_format_err, str(process_platform), platform_name),
|
|
),
|
|
f"process_platform {platform_name}",
|
|
)
|
|
integration_platform = IntegrationPlatform(
|
|
platform_name, process_job, top_level_components
|
|
)
|
|
integration_platforms.append(integration_platform)
|
|
if not top_level_components:
|
|
return
|
|
|
|
# We create a task here for two reasons:
|
|
#
|
|
# 1. We want the integration that provides the integration platform to
|
|
# not be delayed by waiting on each individual platform to be processed
|
|
# since the import or the integration platforms themselves may have to
|
|
# schedule I/O or executor jobs.
|
|
#
|
|
# 2. We want the behavior to be the same as if the integration that has
|
|
# the integration platform is loaded after the platform is processed.
|
|
#
|
|
# We use hass.async_create_task instead of asyncio.create_task because
|
|
# we want to make sure that startup waits for the task to complete.
|
|
#
|
|
future = hass.async_create_task_internal(
|
|
_async_process_integration_platforms(
|
|
hass, platform_name, top_level_components.copy(), process_job
|
|
),
|
|
eager_start=True,
|
|
)
|
|
if wait_for_platforms:
|
|
await future
|
|
|
|
|
|
async def _async_process_integration_platforms(
|
|
hass: HomeAssistant,
|
|
platform_name: str,
|
|
top_level_components: set[str],
|
|
process_job: HassJob,
|
|
) -> None:
|
|
"""Process integration platforms for a component."""
|
|
integrations = await async_get_integrations(hass, top_level_components)
|
|
loaded_integrations: list[Integration] = [
|
|
integration
|
|
for integration in integrations.values()
|
|
if not isinstance(integration, Exception)
|
|
]
|
|
# Finally, fetch the platforms for each integration and process them.
|
|
# This uses the import executor in a loop. If there are a lot
|
|
# of integration with the integration platform to process,
|
|
# this could be a bottleneck.
|
|
futures: list[asyncio.Future[None]] = []
|
|
for integration in loaded_integrations:
|
|
if (
|
|
platform := await _async_import_platform(integration, platform_name)
|
|
) is None:
|
|
continue
|
|
if future := hass.async_run_hass_job(
|
|
process_job, hass, integration.domain, platform
|
|
):
|
|
futures.append(future)
|
|
|
|
if futures:
|
|
await asyncio.gather(*futures)
|
|
|
|
|
|
# Any = platform.
|
|
type ProcessPlatform[_R] = Callable[[HomeAssistant, str, Any], _R]
|
|
|
|
|
|
class LazyIntegrationPlatforms[_R]:
|
|
"""Lazily load and process an integration platform on demand.
|
|
|
|
Unlike async_process_integration_platforms, which imports and processes the
|
|
platform for every loaded integration up front (and as integrations load),
|
|
this only imports and processes the platform for an integration the first
|
|
time it is requested, and only for integrations that are loaded.
|
|
|
|
The platform is intentionally not registered for preloading, since for a
|
|
rarely used platform that would import it for every integration during
|
|
loading, defeating the point of loading it lazily.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
hass: HomeAssistant,
|
|
platform_name: str,
|
|
process_platform: ProcessPlatform[_R],
|
|
) -> None:
|
|
"""Initialize the lazy integration platforms."""
|
|
self._hass = hass
|
|
self._platform_name = platform_name
|
|
self._process_platform = process_platform
|
|
# A cached value of None means the integration does not provide the
|
|
# platform (or it failed to import).
|
|
self._processed: dict[str, _R | None] = {}
|
|
# In-flight processing per domain, so concurrent callers share the work.
|
|
self._processing: dict[str, asyncio.Future[_R | None]] = {}
|
|
|
|
async def async_get_platform(self, domain: str) -> _R | None:
|
|
"""Return the processed platform for a loaded integration.
|
|
|
|
Returns None if the integration is not loaded or does not provide the
|
|
platform. The result for a loaded integration is cached.
|
|
"""
|
|
if domain in self._processed:
|
|
return self._processed[domain]
|
|
# Only process integrations whose component is loaded, matching
|
|
# async_process_integration_platforms.
|
|
if domain not in self._hass.config.top_level_components:
|
|
# Don't cache, the integration may be loaded later.
|
|
return None
|
|
integration = await async_get_integration(self._hass, domain)
|
|
return await self._async_process(integration)
|
|
|
|
async def async_get_platforms(self) -> dict[str, _R]:
|
|
"""Return the processed platform for all loaded integrations that have it."""
|
|
integrations = await async_get_integrations(
|
|
self._hass, self._hass.config.top_level_components
|
|
)
|
|
to_process = [
|
|
integration
|
|
for integration in integrations.values()
|
|
if not isinstance(integration, Exception)
|
|
and integration.platforms_exists((self._platform_name,))
|
|
]
|
|
if missing := [
|
|
integration
|
|
for integration in to_process
|
|
if integration.domain not in self._processed
|
|
]:
|
|
await asyncio.gather(
|
|
*(self._async_process(integration) for integration in missing)
|
|
)
|
|
return {
|
|
integration.domain: result
|
|
for integration in to_process
|
|
if (result := self._processed[integration.domain]) is not None
|
|
}
|
|
|
|
async def _async_process(self, integration: Integration) -> _R | None:
|
|
"""Import, process and cache the platform for a loaded integration.
|
|
|
|
Concurrent callers for the same domain wait on a shared future so the
|
|
platform is imported and processed at most once.
|
|
"""
|
|
domain = integration.domain
|
|
if domain in self._processed:
|
|
return self._processed[domain]
|
|
if (processing := self._processing.get(domain)) is not None:
|
|
return await processing
|
|
|
|
future: asyncio.Future[_R | None] = self._hass.loop.create_future()
|
|
self._processing[domain] = future
|
|
try:
|
|
platform = await _async_import_platform(integration, self._platform_name)
|
|
result: _R | None = None
|
|
if platform is not None:
|
|
try:
|
|
result = self._process_platform(self._hass, domain, platform)
|
|
except Exception:
|
|
_LOGGER.exception(
|
|
"Error processing %s platform for %s",
|
|
self._platform_name,
|
|
domain,
|
|
)
|
|
self._processed[domain] = result
|
|
except BaseException as err:
|
|
future.set_exception(err)
|
|
# Retrieve so an unawaited future does not log the exception.
|
|
future.exception()
|
|
raise
|
|
finally:
|
|
del self._processing[domain]
|
|
future.set_result(result)
|
|
return result
|