Provide api to see which integrations are being loaded (#48274)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
J. Nick Koston 2021-04-04 22:11:44 -10:00 committed by GitHub
parent e925fd2228
commit 12e3bc8101
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 409 additions and 237 deletions

View file

@ -20,14 +20,17 @@ from homeassistant.components import http
from homeassistant.const import REQUIRED_NEXT_PYTHON_DATE, REQUIRED_NEXT_PYTHON_VER
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import area_registry, device_registry, entity_registry
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType
from homeassistant.setup import (
DATA_SETUP,
DATA_SETUP_STARTED,
DATA_SETUP_TIME,
async_set_domains_to_be_loaded,
async_setup_component,
)
from homeassistant.util.async_ import gather_with_concurrency
import homeassistant.util.dt as dt_util
from homeassistant.util.logging import async_activate_log_queue_handler
from homeassistant.util.package import async_get_user_site, is_virtual_env
@ -42,6 +45,8 @@ ERROR_LOG_FILENAME = "home-assistant.log"
DATA_LOGGING = "logging"
LOG_SLOW_STARTUP_INTERVAL = 60
SLOW_STARTUP_CHECK_INTERVAL = 1
SIGNAL_BOOTSTRAP_INTEGRATONS = "bootstrap_integrations"
STAGE_1_TIMEOUT = 120
STAGE_2_TIMEOUT = 300
@ -380,19 +385,29 @@ def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]:
return domains
async def _async_log_pending_setups(
hass: core.HomeAssistant, domains: set[str], setup_started: dict[str, datetime]
) -> None:
async def _async_watch_pending_setups(hass: core.HomeAssistant) -> None:
"""Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL."""
loop_count = 0
setup_started: dict[str, datetime] = hass.data[DATA_SETUP_STARTED]
while True:
await asyncio.sleep(LOG_SLOW_STARTUP_INTERVAL)
remaining = [domain for domain in domains if domain in setup_started]
now = dt_util.utcnow()
remaining_with_setup_started = {
domain: (now - setup_started[domain]).total_seconds()
for domain in setup_started
}
_LOGGER.debug("Integration remaining: %s", remaining_with_setup_started)
async_dispatcher_send(
hass, SIGNAL_BOOTSTRAP_INTEGRATONS, remaining_with_setup_started
)
await asyncio.sleep(SLOW_STARTUP_CHECK_INTERVAL)
loop_count += SLOW_STARTUP_CHECK_INTERVAL
if remaining:
if loop_count >= LOG_SLOW_STARTUP_INTERVAL and setup_started:
_LOGGER.warning(
"Waiting on integrations to complete setup: %s",
", ".join(remaining),
", ".join(setup_started),
)
loop_count = 0
_LOGGER.debug("Running timeout Zones: %s", hass.timeout.zones)
@ -400,18 +415,13 @@ async def async_setup_multi_components(
hass: core.HomeAssistant,
domains: set[str],
config: dict[str, Any],
setup_started: dict[str, datetime],
) -> None:
"""Set up multiple domains. Log on failure."""
futures = {
domain: hass.async_create_task(async_setup_component(hass, domain, config))
for domain in domains
}
log_task = asyncio.create_task(
_async_log_pending_setups(hass, domains, setup_started)
)
await asyncio.wait(futures.values())
log_task.cancel()
errors = [domain for domain in domains if futures[domain].exception()]
for domain in errors:
exception = futures[domain].exception()
@ -427,7 +437,11 @@ async def _async_set_up_integrations(
hass: core.HomeAssistant, config: dict[str, Any]
) -> None:
"""Set up all the integrations."""
setup_started = hass.data[DATA_SETUP_STARTED] = {}
hass.data[DATA_SETUP_STARTED] = {}
setup_time = hass.data[DATA_SETUP_TIME] = {}
log_task = asyncio.create_task(_async_watch_pending_setups(hass))
domains_to_setup = _get_domains(hass, config)
# Resolve all dependencies so we know all integrations
@ -476,14 +490,14 @@ async def _async_set_up_integrations(
# Load logging as soon as possible
if logging_domains:
_LOGGER.info("Setting up logging: %s", logging_domains)
await async_setup_multi_components(hass, logging_domains, config, setup_started)
await async_setup_multi_components(hass, logging_domains, config)
# Start up debuggers. Start these first in case they want to wait.
debuggers = domains_to_setup & DEBUGGER_INTEGRATIONS
if debuggers:
_LOGGER.debug("Setting up debuggers: %s", debuggers)
await async_setup_multi_components(hass, debuggers, config, setup_started)
await async_setup_multi_components(hass, debuggers, config)
# calculate what components to setup in what stage
stage_1_domains = set()
@ -524,9 +538,7 @@ async def _async_set_up_integrations(
async with hass.timeout.async_timeout(
STAGE_1_TIMEOUT, cool_down=COOLDOWN_TIME
):
await async_setup_multi_components(
hass, stage_1_domains, config, setup_started
)
await async_setup_multi_components(hass, stage_1_domains, config)
except asyncio.TimeoutError:
_LOGGER.warning("Setup timed out for stage 1 - moving forward")
@ -539,12 +551,21 @@ async def _async_set_up_integrations(
async with hass.timeout.async_timeout(
STAGE_2_TIMEOUT, cool_down=COOLDOWN_TIME
):
await async_setup_multi_components(
hass, stage_2_domains, config, setup_started
)
await async_setup_multi_components(hass, stage_2_domains, config)
except asyncio.TimeoutError:
_LOGGER.warning("Setup timed out for stage 2 - moving forward")
log_task.cancel()
_LOGGER.debug(
"Integration setup times: %s",
{
integration: timedelta.total_seconds()
for integration, timedelta in sorted(
setup_time.items(), key=lambda item: item[1].total_seconds() # type: ignore
)
},
)
# Wrap up startup
_LOGGER.debug("Waiting for startup to wrap up")
try:

View file

@ -38,7 +38,7 @@ from homeassistant.helpers.event import (
)
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, GPSType
from homeassistant.setup import async_prepare_setup_platform
from homeassistant.setup import async_prepare_setup_platform, async_start_setup
from homeassistant.util import dt as dt_util
from homeassistant.util.yaml import dump
@ -221,48 +221,54 @@ class DeviceTrackerPlatform:
async def async_setup_legacy(self, hass, tracker, discovery_info=None):
"""Set up a legacy platform."""
LOGGER.info("Setting up %s.%s", DOMAIN, self.name)
try:
scanner = None
setup = None
if hasattr(self.platform, "async_get_scanner"):
scanner = await self.platform.async_get_scanner(
hass, {DOMAIN: self.config}
)
elif hasattr(self.platform, "get_scanner"):
scanner = await hass.async_add_executor_job(
self.platform.get_scanner, hass, {DOMAIN: self.config}
)
elif hasattr(self.platform, "async_setup_scanner"):
setup = await self.platform.async_setup_scanner(
hass, self.config, tracker.async_see, discovery_info
)
elif hasattr(self.platform, "setup_scanner"):
setup = await hass.async_add_executor_job(
self.platform.setup_scanner,
hass,
self.config,
tracker.see,
discovery_info,
)
else:
raise HomeAssistantError("Invalid legacy device_tracker platform.")
full_name = f"{DOMAIN}.{self.name}"
LOGGER.info("Setting up %s", full_name)
with async_start_setup(hass, [full_name]):
try:
scanner = None
setup = None
if hasattr(self.platform, "async_get_scanner"):
scanner = await self.platform.async_get_scanner(
hass, {DOMAIN: self.config}
)
elif hasattr(self.platform, "get_scanner"):
scanner = await hass.async_add_executor_job(
self.platform.get_scanner, hass, {DOMAIN: self.config}
)
elif hasattr(self.platform, "async_setup_scanner"):
setup = await self.platform.async_setup_scanner(
hass, self.config, tracker.async_see, discovery_info
)
elif hasattr(self.platform, "setup_scanner"):
setup = await hass.async_add_executor_job(
self.platform.setup_scanner,
hass,
self.config,
tracker.see,
discovery_info,
)
else:
raise HomeAssistantError("Invalid legacy device_tracker platform.")
if setup:
hass.config.components.add(f"{DOMAIN}.{self.name}")
if setup:
hass.config.components.add(full_name)
if scanner:
async_setup_scanner_platform(
hass, self.config, scanner, tracker.async_see, self.type
if scanner:
async_setup_scanner_platform(
hass, self.config, scanner, tracker.async_see, self.type
)
return
if not setup:
LOGGER.error(
"Error setting up platform %s %s", self.type, self.name
)
return
except Exception: # pylint: disable=broad-except
LOGGER.exception(
"Error setting up platform %s %s", self.type, self.name
)
return
if not setup:
LOGGER.error("Error setting up platform %s %s", self.type, self.name)
return
except Exception: # pylint: disable=broad-except
LOGGER.exception("Error setting up platform %s %s", self.type, self.name)
async def async_extract_config(hass, config):

View file

@ -16,7 +16,7 @@ from homeassistant.helpers import config_per_platform, discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.service import async_set_service_schema
from homeassistant.loader import async_get_integration, bind_hass
from homeassistant.setup import async_prepare_setup_platform
from homeassistant.setup import async_prepare_setup_platform, async_start_setup
from homeassistant.util import slugify
from homeassistant.util.yaml import load_yaml
@ -289,47 +289,52 @@ async def async_setup(hass, config):
_LOGGER.error("Unknown notification service specified")
return
_LOGGER.info("Setting up %s.%s", DOMAIN, integration_name)
notify_service = None
try:
if hasattr(platform, "async_get_service"):
notify_service = await platform.async_get_service(
hass, p_config, discovery_info
)
elif hasattr(platform, "get_service"):
notify_service = await hass.async_add_executor_job(
platform.get_service, hass, p_config, discovery_info
)
else:
raise HomeAssistantError("Invalid notify platform.")
if notify_service is None:
# Platforms can decide not to create a service based
# on discovery data.
if discovery_info is None:
_LOGGER.error(
"Failed to initialize notification service %s", integration_name
full_name = f"{DOMAIN}.{integration_name}"
_LOGGER.info("Setting up %s", full_name)
with async_start_setup(hass, [full_name]):
notify_service = None
try:
if hasattr(platform, "async_get_service"):
notify_service = await platform.async_get_service(
hass, p_config, discovery_info
)
elif hasattr(platform, "get_service"):
notify_service = await hass.async_add_executor_job(
platform.get_service, hass, p_config, discovery_info
)
else:
raise HomeAssistantError("Invalid notify platform.")
if notify_service is None:
# Platforms can decide not to create a service based
# on discovery data.
if discovery_info is None:
_LOGGER.error(
"Failed to initialize notification service %s",
integration_name,
)
return
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error setting up platform %s", integration_name)
return
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error setting up platform %s", integration_name)
return
if discovery_info is None:
discovery_info = {}
if discovery_info is None:
discovery_info = {}
conf_name = p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME)
target_service_name_prefix = conf_name or integration_name
service_name = slugify(conf_name or SERVICE_NOTIFY)
conf_name = p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME)
target_service_name_prefix = conf_name or integration_name
service_name = slugify(conf_name or SERVICE_NOTIFY)
await notify_service.async_setup(
hass, service_name, target_service_name_prefix
)
await notify_service.async_register_services()
await notify_service.async_setup(hass, service_name, target_service_name_prefix)
await notify_service.async_register_services()
hass.data[NOTIFY_SERVICES].setdefault(integration_name, []).append(
notify_service
)
hass.config.components.add(f"{DOMAIN}.{integration_name}")
hass.data[NOTIFY_SERVICES].setdefault(integration_name, []).append(
notify_service
)
hass.config.components.add(f"{DOMAIN}.{integration_name}")
return True

View file

@ -4,6 +4,7 @@ import asyncio
import voluptuous as vol
from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_READ
from homeassistant.bootstrap import SIGNAL_BOOTSTRAP_INTEGRATONS
from homeassistant.components.websocket_api.const import ERR_NOT_FOUND
from homeassistant.const import EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL
from homeassistant.core import DOMAIN as HASS_DOMAIN, callback
@ -14,10 +15,11 @@ from homeassistant.exceptions import (
Unauthorized,
)
from homeassistant.helpers import config_validation as cv, entity, template
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.event import TrackTemplate, async_track_template_result
from homeassistant.helpers.service import async_get_all_descriptions
from homeassistant.loader import IntegrationNotFound, async_get_integration
from homeassistant.setup import async_get_loaded_integrations
from homeassistant.setup import DATA_SETUP_TIME, async_get_loaded_integrations
from . import const, decorators, messages
@ -34,9 +36,11 @@ def async_register_commands(hass, async_reg):
async_reg(hass, handle_get_services)
async_reg(hass, handle_get_states)
async_reg(hass, handle_manifest_get)
async_reg(hass, handle_integration_setup_info)
async_reg(hass, handle_manifest_list)
async_reg(hass, handle_ping)
async_reg(hass, handle_render_template)
async_reg(hass, handle_subscribe_bootstrap_integrations)
async_reg(hass, handle_subscribe_events)
async_reg(hass, handle_subscribe_trigger)
async_reg(hass, handle_test_condition)
@ -95,6 +99,27 @@ def handle_subscribe_events(hass, connection, msg):
connection.send_message(messages.result_message(msg["id"]))
@callback
@decorators.websocket_command(
{
vol.Required("type"): "subscribe_bootstrap_integrations",
}
)
def handle_subscribe_bootstrap_integrations(hass, connection, msg):
"""Handle subscribe bootstrap integrations command."""
@callback
def forward_bootstrap_integrations(message):
"""Forward bootstrap integrations to websocket."""
connection.send_message(messages.result_message(msg["id"], message))
connection.subscriptions[msg["id"]] = async_dispatcher_connect(
hass, SIGNAL_BOOTSTRAP_INTEGRATONS, forward_bootstrap_integrations
)
connection.send_message(messages.result_message(msg["id"]))
@callback
@decorators.websocket_command(
{
@ -238,6 +263,19 @@ async def handle_manifest_get(hass, connection, msg):
connection.send_error(msg["id"], const.ERR_NOT_FOUND, "Integration not found")
@decorators.websocket_command({vol.Required("type"): "integration/setup_info"})
@decorators.async_response
async def handle_integration_setup_info(hass, connection, msg):
"""Handle integrations command."""
connection.send_result(
msg["id"],
[
{"domain": integration, "seconds": timedelta.total_seconds()}
for integration, timedelta in hass.data[DATA_SETUP_TIME].items()
],
)
@callback
@decorators.websocket_command({vol.Required("type"): "ping"})
def handle_ping(hass, connection, msg):

View file

@ -4,6 +4,7 @@ from __future__ import annotations
import asyncio
from contextvars import ContextVar
from datetime import datetime, timedelta
import logging
from logging import Logger
from types import ModuleType
from typing import TYPE_CHECKING, Callable, Coroutine, Iterable
@ -30,6 +31,7 @@ from homeassistant.helpers import (
entity_registry as ent_reg,
service,
)
from homeassistant.setup import async_start_setup
from homeassistant.util.async_ import run_callback_threadsafe
from .entity_registry import DISABLED_INTEGRATION
@ -48,6 +50,8 @@ PLATFORM_NOT_READY_RETRIES = 10
DATA_ENTITY_PLATFORM = "entity_platform"
PLATFORM_NOT_READY_BASE_WAIT_TIME = 30 # seconds
_LOGGER = logging.getLogger(__name__)
class EntityPlatform:
"""Manage the entities for a single platform."""
@ -202,77 +206,77 @@ class EntityPlatform:
self.platform_name,
SLOW_SETUP_WARNING,
)
with async_start_setup(hass, [full_name]):
try:
task = async_create_setup_task()
try:
task = async_create_setup_task()
async with hass.timeout.async_timeout(SLOW_SETUP_MAX_WAIT, self.domain):
await asyncio.shield(task)
async with hass.timeout.async_timeout(SLOW_SETUP_MAX_WAIT, self.domain):
await asyncio.shield(task)
# Block till all entities are done
while self._tasks:
pending = [task for task in self._tasks if not task.done()]
self._tasks.clear()
# Block till all entities are done
while self._tasks:
pending = [task for task in self._tasks if not task.done()]
self._tasks.clear()
if pending:
await asyncio.gather(*pending)
if pending:
await asyncio.gather(*pending)
hass.config.components.add(full_name)
self._setup_complete = True
return True
except PlatformNotReady as ex:
tries += 1
wait_time = min(tries, 6) * PLATFORM_NOT_READY_BASE_WAIT_TIME
message = str(ex)
if not message and ex.__cause__:
message = str(ex.__cause__)
ready_message = f"ready yet: {message}" if message else "ready yet"
if tries == 1:
logger.warning(
"Platform %s not %s; Retrying in background in %d seconds",
self.platform_name,
ready_message,
wait_time,
)
else:
logger.debug(
"Platform %s not %s; Retrying in %d seconds",
self.platform_name,
ready_message,
wait_time,
)
hass.config.components.add(full_name)
self._setup_complete = True
return True
except PlatformNotReady as ex:
tries += 1
wait_time = min(tries, 6) * PLATFORM_NOT_READY_BASE_WAIT_TIME
message = str(ex)
if not message and ex.__cause__:
message = str(ex.__cause__)
ready_message = f"ready yet: {message}" if message else "ready yet"
if tries == 1:
logger.warning(
"Platform %s not %s; Retrying in background in %d seconds",
async def setup_again(*_): # type: ignore[no-untyped-def]
"""Run setup again."""
self._async_cancel_retry_setup = None
await self._async_setup_platform(async_create_setup_task, tries)
if hass.state == CoreState.running:
self._async_cancel_retry_setup = async_call_later(
hass, wait_time, setup_again
)
else:
self._async_cancel_retry_setup = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STARTED, setup_again
)
return False
except asyncio.TimeoutError:
logger.error(
"Setup of platform %s is taking longer than %s seconds."
" Startup will proceed without waiting any longer.",
self.platform_name,
ready_message,
wait_time,
SLOW_SETUP_MAX_WAIT,
)
else:
logger.debug(
"Platform %s not %s; Retrying in %d seconds",
return False
except Exception: # pylint: disable=broad-except
logger.exception(
"Error while setting up %s platform for %s",
self.platform_name,
ready_message,
wait_time,
self.domain,
)
async def setup_again(*_): # type: ignore[no-untyped-def]
"""Run setup again."""
self._async_cancel_retry_setup = None
await self._async_setup_platform(async_create_setup_task, tries)
if hass.state == CoreState.running:
self._async_cancel_retry_setup = async_call_later(
hass, wait_time, setup_again
)
else:
self._async_cancel_retry_setup = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STARTED, setup_again
)
return False
except asyncio.TimeoutError:
logger.error(
"Setup of platform %s is taking longer than %s seconds."
" Startup will proceed without waiting any longer.",
self.platform_name,
SLOW_SETUP_MAX_WAIT,
)
return False
except Exception: # pylint: disable=broad-except
logger.exception(
"Error while setting up %s platform for %s",
self.platform_name,
self.domain,
)
return False
finally:
warn_task.cancel()
return False
finally:
warn_task.cancel()
def _schedule_add_entities(
self, new_entities: Iterable[Entity], update_before_add: bool = False

View file

@ -2,17 +2,18 @@
from __future__ import annotations
import asyncio
import contextlib
import logging.handlers
from timeit import default_timer as timer
from types import ModuleType
from typing import Awaitable, Callable
from typing import Awaitable, Callable, Generator, Iterable
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
from homeassistant.util import dt as dt_util, ensure_unique_string
_LOGGER = logging.getLogger(__name__)
@ -42,6 +43,8 @@ BASE_PLATFORMS = {
DATA_SETUP_DONE = "setup_done"
DATA_SETUP_STARTED = "setup_started"
DATA_SETUP_TIME = "setup_time"
DATA_SETUP = "setup_tasks"
DATA_DEPS_REQS = "deps_reqs_processed"
@ -205,84 +208,77 @@ async def _async_setup_component(
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,
)
task = None
result = True
try:
if hasattr(component, "async_setup"):
task = component.async_setup(hass, processed_config) # type: ignore
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
with async_start_setup(hass, [domain]):
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,
)
task = None
result = True
try:
if hasattr(component, "async_setup"):
task = component.async_setup(hass, processed_config) # type: ignore
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
)
elif not hasattr(component, "async_setup_entry"):
log_error("No setup or config entry setup function defined.")
return False
if task:
async with hass.timeout.async_timeout(SLOW_SETUP_MAX_WAIT, domain):
result = await task
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,
)
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)
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.")
return False
if result is not True:
log_error(
f"Integration {domain!r} did not return boolean if setup was "
"successful. Disabling component."
)
elif not hasattr(component, "async_setup_entry"):
log_error("No setup or config entry setup function defined.")
hass.data[DATA_SETUP_STARTED].pop(domain)
return False
if task:
async with hass.timeout.async_timeout(SLOW_SETUP_MAX_WAIT, domain):
result = await task
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,
# 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.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)
hass.config.components.add(domain)
# Cleanup
if domain in hass.data[DATA_SETUP]:
@ -420,3 +416,30 @@ def async_get_loaded_integrations(hass: core.HomeAssistant) -> set:
if domain in BASE_PLATFORMS:
integrations.add(platform)
return integrations
@contextlib.contextmanager
def async_start_setup(hass: core.HomeAssistant, components: Iterable) -> Generator:
"""Keep track of when setup starts and finishes."""
setup_started = hass.data.setdefault(DATA_SETUP_STARTED, {})
started = dt_util.utcnow()
unique_components = {}
for domain in components:
unique = ensure_unique_string(domain, setup_started)
unique_components[unique] = domain
setup_started[unique] = started
yield
setup_time = hass.data.setdefault(DATA_SETUP_TIME, {})
time_taken = dt_util.utcnow() - started
for unique, domain in unique_components.items():
del setup_started[unique]
if "." in domain:
_, integration = domain.split(".", 1)
else:
integration = domain
if integration in setup_time:
setup_time[integration] += time_taken
else:
setup_time[integration] = time_taken

View file

@ -1,10 +1,12 @@
"""Tests for WebSocket API commands."""
import datetime
from unittest.mock import ANY, patch
from async_timeout import timeout
import pytest
import voluptuous as vol
from homeassistant.bootstrap import SIGNAL_BOOTSTRAP_INTEGRATONS
from homeassistant.components.websocket_api import const
from homeassistant.components.websocket_api.auth import (
TYPE_AUTH,
@ -15,9 +17,10 @@ from homeassistant.components.websocket_api.const import URL
from homeassistant.core import Context, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.loader import async_get_integration
from homeassistant.setup import async_setup_component
from homeassistant.setup import DATA_SETUP_TIME, async_setup_component
from tests.common import MockEntity, MockEntityPlatform, async_mock_service
@ -1124,3 +1127,44 @@ async def test_execute_script(hass, websocket_client):
assert call.service == "test_service"
assert call.data == {"hello": "From variable"}
assert call.context.as_dict() == msg_var["result"]["context"]
async def test_subscribe_unsubscribe_bootstrap_integrations(
hass, websocket_client, hass_admin_user
):
"""Test subscribe/unsubscribe bootstrap_integrations."""
await websocket_client.send_json(
{"id": 7, "type": "subscribe_bootstrap_integrations"}
)
msg = await websocket_client.receive_json()
assert msg["id"] == 7
assert msg["type"] == const.TYPE_RESULT
assert msg["success"]
message = {"august": 12.5, "isy994": 12.8}
async_dispatcher_send(hass, SIGNAL_BOOTSTRAP_INTEGRATONS, message)
msg = await websocket_client.receive_json()
assert msg["id"] == 7
assert msg["success"] is True
assert msg["type"] == "result"
assert msg["result"] == message
async def test_integration_setup_info(hass, websocket_client, hass_admin_user):
"""Test subscribe/unsubscribe bootstrap_integrations."""
hass.data[DATA_SETUP_TIME] = {
"august": datetime.timedelta(seconds=12.5),
"isy994": datetime.timedelta(seconds=12.8),
}
await websocket_client.send_json({"id": 7, "type": "integration/setup_info"})
msg = await websocket_client.receive_json()
assert msg["id"] == 7
assert msg["type"] == const.TYPE_RESULT
assert msg["success"]
assert msg["result"] == [
{"domain": "august", "seconds": 12.5},
{"domain": "isy994", "seconds": 12.8},
]

View file

@ -431,7 +431,9 @@ async def test_setup_hass_takes_longer_than_log_slow_startup(
with patch(
"homeassistant.config.async_hass_config_yaml",
return_value={"browser": {}, "frontend": {}},
), patch.object(bootstrap, "LOG_SLOW_STARTUP_INTERVAL", 0.3), patch(
), patch.object(bootstrap, "LOG_SLOW_STARTUP_INTERVAL", 0.3), patch.object(
bootstrap, "SLOW_STARTUP_CHECK_INTERVAL", 0.05
), patch(
"homeassistant.components.frontend.async_setup",
side_effect=_async_setup_that_blocks_startup,
):

View file

@ -1,6 +1,7 @@
"""Test component/platform setup."""
# pylint: disable=protected-access
import asyncio
import datetime
import os
import threading
from unittest.mock import AsyncMock, Mock, patch
@ -644,3 +645,31 @@ async def test_integration_only_setup_entry(hass):
),
)
assert await setup.async_setup_component(hass, "test_integration_only_entry", {})
async def test_async_start_setup(hass):
"""Test setup started context manager keeps track of setup times."""
with setup.async_start_setup(hass, ["august"]):
assert isinstance(
hass.data[setup.DATA_SETUP_STARTED]["august"], datetime.datetime
)
with setup.async_start_setup(hass, ["august"]):
assert isinstance(
hass.data[setup.DATA_SETUP_STARTED]["august_2"], datetime.datetime
)
assert "august" not in hass.data[setup.DATA_SETUP_STARTED]
assert isinstance(hass.data[setup.DATA_SETUP_TIME]["august"], datetime.timedelta)
assert "august_2" not in hass.data[setup.DATA_SETUP_TIME]
async def test_async_start_setup_platforms(hass):
"""Test setup started context manager keeps track of setup times for platforms."""
with setup.async_start_setup(hass, ["sensor.august"]):
assert isinstance(
hass.data[setup.DATA_SETUP_STARTED]["sensor.august"], datetime.datetime
)
assert "august" not in hass.data[setup.DATA_SETUP_STARTED]
assert isinstance(hass.data[setup.DATA_SETUP_TIME]["august"], datetime.timedelta)
assert "sensor" not in hass.data[setup.DATA_SETUP_TIME]