"""All methods needed to bootstrap a Home Assistant instance.""" import asyncio import logging.handlers from timeit import default_timer as timer from types import ModuleType from typing import Awaitable, Callable, List, Optional from homeassistant import config as conf_util, core, loader, requirements from homeassistant.config import async_notify_setup_error from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) ATTR_COMPONENT = "component" DATA_SETUP_STARTED = "setup_started" DATA_SETUP = "setup_tasks" DATA_DEPS_REQS = "deps_reqs_processed" SLOW_SETUP_WARNING = 10 # Since a pip install can run, we wait # 30 minutes to timeout SLOW_SETUP_MAX_WAIT = 1800 def setup_component(hass: core.HomeAssistant, domain: str, config: ConfigType) -> bool: """Set up a component and all its dependencies.""" return asyncio.run_coroutine_threadsafe( async_setup_component(hass, domain, config), hass.loop ).result() async def async_setup_component( hass: core.HomeAssistant, domain: str, config: ConfigType ) -> bool: """Set up a component and all its dependencies. This method is a coroutine. """ if domain in hass.config.components: return True setup_tasks = hass.data.setdefault(DATA_SETUP, {}) if domain in setup_tasks: return await setup_tasks[domain] # type: ignore task = setup_tasks[domain] = hass.async_create_task( _async_setup_component(hass, domain, config) ) return await task # type: ignore async def _async_process_dependencies( hass: core.HomeAssistant, config: ConfigType, name: str, dependencies: List[str] ) -> bool: """Ensure all dependencies are set up.""" tasks = [async_setup_component(hass, dep, config) for dep in dependencies] if not tasks: return True results = await asyncio.gather(*tasks) failed = [dependencies[idx] for idx, res in enumerate(results) if not res] if failed: _LOGGER.error( "Unable to set up dependencies of %s. Setup failed for dependencies: %s", name, ", ".join(failed), ) return False return True async def _async_setup_component( hass: core.HomeAssistant, domain: str, config: ConfigType ) -> bool: """Set up a component for Home Assistant. This method is a coroutine. """ def log_error(msg: str, link: Optional[str] = None) -> None: """Log helper.""" _LOGGER.error("Setup failed for %s: %s", domain, msg) async_notify_setup_error(hass, domain, link) try: integration = await loader.async_get_integration(hass, domain) except loader.IntegrationNotFound: log_error("Integration not found.") return False # Validate all dependencies exist and there are no circular dependencies try: await loader.async_component_dependencies(hass, domain) except loader.IntegrationNotFound as err: _LOGGER.error( "Not setting up %s because we are unable to resolve (sub)dependency %s", domain, err.domain, ) return False except loader.CircularDependency as err: _LOGGER.error( "Not setting up %s because it contains a circular dependency: %s -> %s", domain, err.from_domain, err.to_domain, ) return False # Process requirements as soon as possible, so we can import the component # without requiring imports to be in functions. try: await async_process_deps_reqs(hass, config, integration) except HomeAssistantError as err: log_error(str(err), integration.documentation) return False # Some integrations fail on import because they call functions incorrectly. # So we do it before validating config to catch these errors. try: component = integration.get_component() except ImportError as err: log_error(f"Unable to import component: {err}", integration.documentation) return False except Exception: # pylint: disable=broad-except _LOGGER.exception("Setup failed for %s: unknown error", domain) return False processed_config = await conf_util.async_process_component_config( hass, config, integration ) if processed_config is None: log_error("Invalid config.", integration.documentation) return False start = timer() _LOGGER.info("Setting up %s", domain) hass.data.setdefault(DATA_SETUP_STARTED, {})[domain] = dt_util.utcnow() if hasattr(component, "PLATFORM_SCHEMA"): # Entity components have their own warning warn_task = None else: warn_task = hass.loop.call_later( SLOW_SETUP_WARNING, _LOGGER.warning, "Setup of %s is taking over %s seconds.", domain, SLOW_SETUP_WARNING, ) try: if hasattr(component, "async_setup"): task = component.async_setup( # type: ignore hass, processed_config ) elif hasattr(component, "setup"): # This should not be replaced with hass.async_add_executor_job because # we don't want to track this task in case it blocks startup. task = hass.loop.run_in_executor( None, component.setup, hass, processed_config # type: ignore ) else: log_error("No setup function defined.") hass.data[DATA_SETUP_STARTED].pop(domain) return False result = await asyncio.wait_for(task, SLOW_SETUP_MAX_WAIT) except asyncio.TimeoutError: _LOGGER.error( "Setup of %s is taking longer than %s seconds." " Startup will proceed without waiting any longer.", domain, SLOW_SETUP_MAX_WAIT, ) hass.data[DATA_SETUP_STARTED].pop(domain) return False except Exception: # pylint: disable=broad-except _LOGGER.exception("Error during setup of component %s", domain) async_notify_setup_error(hass, domain, integration.documentation) hass.data[DATA_SETUP_STARTED].pop(domain) return False finally: end = timer() if warn_task: warn_task.cancel() _LOGGER.info("Setup of domain %s took %.1f seconds.", domain, end - start) if result is False: log_error("Integration failed to initialize.") hass.data[DATA_SETUP_STARTED].pop(domain) return False if result is not True: log_error( f"Integration {domain!r} did not return boolean if setup was " "successful. Disabling component." ) hass.data[DATA_SETUP_STARTED].pop(domain) return False # Flush out async_setup calling create_task. Fragile but covered by test. await asyncio.sleep(0) await hass.config_entries.flow.async_wait_init_flow_finish(domain) await asyncio.gather( *[ entry.async_setup(hass, integration=integration) for entry in hass.config_entries.async_entries(domain) ] ) hass.config.components.add(domain) hass.data[DATA_SETUP_STARTED].pop(domain) # Cleanup if domain in hass.data[DATA_SETUP]: hass.data[DATA_SETUP].pop(domain) hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: domain}) return True async def async_prepare_setup_platform( hass: core.HomeAssistant, hass_config: ConfigType, domain: str, platform_name: str ) -> Optional[ModuleType]: """Load a platform and makes sure dependencies are setup. This method is a coroutine. """ platform_path = PLATFORM_FORMAT.format(domain=domain, platform=platform_name) def log_error(msg: str) -> None: """Log helper.""" _LOGGER.error("Unable to prepare setup for platform %s: %s", platform_path, msg) async_notify_setup_error(hass, platform_path) try: integration = await loader.async_get_integration(hass, platform_name) except loader.IntegrationNotFound: log_error("Integration not found") return None # Process deps and reqs as soon as possible, so that requirements are # available when we import the platform. try: await async_process_deps_reqs(hass, hass_config, integration) except HomeAssistantError as err: log_error(str(err)) return None try: platform = integration.get_platform(domain) except ImportError as exc: log_error(f"Platform not found ({exc}).") return None # Already loaded if platform_path in hass.config.components: return platform # Platforms cannot exist on their own, they are part of their integration. # If the integration is not set up yet, and can be set up, set it up. if integration.domain not in hass.config.components: try: component = integration.get_component() except ImportError as exc: log_error(f"Unable to import the component ({exc}).") return None if hasattr(component, "setup") or hasattr(component, "async_setup"): if not await async_setup_component(hass, integration.domain, hass_config): log_error("Unable to set up component.") return None return platform async def async_process_deps_reqs( hass: core.HomeAssistant, config: ConfigType, integration: loader.Integration ) -> None: """Process all dependencies and requirements for a module. Module is a Python module of either a component or platform. """ processed = hass.data.get(DATA_DEPS_REQS) if processed is None: processed = hass.data[DATA_DEPS_REQS] = set() elif integration.domain in processed: return if integration.dependencies and not await _async_process_dependencies( hass, config, integration.domain, integration.dependencies ): raise HomeAssistantError("Could not set up all dependencies.") if not hass.config.skip_pip and integration.requirements: await requirements.async_get_integration_with_requirements( hass, integration.domain ) processed.add(integration.domain) @core.callback def async_when_setup( hass: core.HomeAssistant, component: str, when_setup_cb: Callable[[core.HomeAssistant, str], Awaitable[None]], ) -> None: """Call a method when a component is setup.""" async def when_setup() -> None: """Call the callback.""" try: await when_setup_cb(hass, component) except Exception: # pylint: disable=broad-except _LOGGER.exception("Error handling when_setup callback for %s", component) # Running it in a new task so that it always runs after if component in hass.config.components: hass.async_create_task(when_setup()) return unsub = None async def loaded_event(event: core.Event) -> None: """Call the callback.""" if event.data[ATTR_COMPONENT] != component: return unsub() # type: ignore await when_setup() unsub = hass.bus.async_listen(EVENT_COMPONENT_LOADED, loaded_event)