diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 4b694d2b97a..258970378b2 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -194,7 +194,9 @@ async def async_setup_platform( integration = await async_get_integration(hass, SCENE_DOMAIN) - conf = await conf_util.async_process_component_config(hass, config, integration) + conf = await conf_util.async_process_component_and_handle_errors( + hass, config, integration + ) if not (conf and platform): return diff --git a/homeassistant/components/homeassistant/strings.json b/homeassistant/components/homeassistant/strings.json index f14d9f8148c..6981bdfe685 100644 --- a/homeassistant/components/homeassistant/strings.json +++ b/homeassistant/components/homeassistant/strings.json @@ -138,6 +138,36 @@ } }, "exceptions": { + "component_import_err": { + "message": "Unable to import {domain}: {error}" + }, + "config_platform_import_err": { + "message": "Error importing config platform {domain}: {error}" + }, + "config_validation_err": { + "message": "Invalid config for integration {domain} at {config_file}, line {line}: {error}. Check the logs for more information." + }, + "config_validator_unknown_err": { + "message": "Unknown error calling {domain} config validator. Check the logs for more information." + }, + "config_schema_unknown_err": { + "message": "Unknown error calling {domain} CONFIG_SCHEMA. Check the logs for more information." + }, + "integration_config_error": { + "message": "Failed to process config for integration {domain} due to multiple ({errors}) errors. Check the logs for more information." + }, + "platform_component_load_err": { + "message": "Platform error: {domain} - {error}. Check the logs for more information." + }, + "platform_component_load_exc": { + "message": "Platform error: {domain} - {error}. Check the logs for more information." + }, + "platform_config_validation_err": { + "message": "Invalid config for {domain} from integration {p_name} at file {config_file}, line {line}: {error}. Check the logs for more information." + }, + "platform_schema_validator_err": { + "message": "Unknown error when validating config for {domain} from integration {p_name}" + }, "service_not_found": { "message": "Service {domain}.{service} not found." } diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 2c425bec785..daa44bf60be 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -4,7 +4,10 @@ import logging import voluptuous as vol from homeassistant.components import frontend, websocket_api -from homeassistant.config import async_hass_config_yaml, async_process_component_config +from homeassistant.config import ( + async_hass_config_yaml, + async_process_component_and_handle_errors, +) from homeassistant.const import CONF_FILENAME, CONF_MODE, CONF_RESOURCES from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError @@ -85,7 +88,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: integration = await async_get_integration(hass, DOMAIN) - config = await async_process_component_config(hass, conf, integration) + config = await async_process_component_and_handle_errors( + hass, conf, integration + ) if config is None: raise HomeAssistantError("Config validation failed") diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 83e6dae55b1..dd51b276715 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -25,7 +25,7 @@ from homeassistant.const import ( ) from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ( - HomeAssistantError, + ConfigValidationError, ServiceValidationError, TemplateError, Unauthorized, @@ -417,14 +417,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _reload_config(call: ServiceCall) -> None: """Reload the platforms.""" # Fetch updated manually configured items and validate - if ( - config_yaml := await async_integration_yaml_config(hass, DOMAIN) - ) is None: - # Raise in case we have an invalid configuration - raise HomeAssistantError( - "Error reloading manually configured MQTT items, " - "check your configuration.yaml" + try: + config_yaml = await async_integration_yaml_config( + hass, DOMAIN, raise_on_failure=True ) + except ConfigValidationError as ex: + raise ServiceValidationError( + str(ex), + translation_domain=ex.translation_domain, + translation_key=ex.translation_key, + translation_placeholders=ex.translation_placeholders, + ) from ex + # Check the schema before continuing reload await async_check_config_schema(hass, config_yaml) diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index 22919ac9e70..d52dc0cf166 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -34,8 +34,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.error(err) return - conf = await conf_util.async_process_component_config( - hass, unprocessed_conf, await async_get_integration(hass, DOMAIN) + integration = await async_get_integration(hass, DOMAIN) + conf = await conf_util.async_process_component_and_handle_errors( + hass, unprocessed_conf, integration ) if conf is None: diff --git a/homeassistant/config.py b/homeassistant/config.py index 6a840b01714..a9c505b0a68 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -4,6 +4,8 @@ from __future__ import annotations from collections import OrderedDict from collections.abc import Callable, Sequence from contextlib import suppress +from dataclasses import dataclass +from enum import StrEnum from functools import reduce import logging import operator @@ -12,7 +14,7 @@ from pathlib import Path import re import shutil from types import ModuleType -from typing import Any +from typing import TYPE_CHECKING, Any from urllib.parse import urlparse from awesomeversion import AwesomeVersion @@ -54,7 +56,7 @@ from .const import ( __version__, ) from .core import DOMAIN as CONF_CORE, ConfigSource, HomeAssistant, callback -from .exceptions import HomeAssistantError +from .exceptions import ConfigValidationError, HomeAssistantError from .generated.currencies import HISTORIC_CURRENCIES from .helpers import ( config_per_platform, @@ -66,13 +68,13 @@ from .helpers.entity_values import EntityValues from .helpers.typing import ConfigType from .loader import ComponentProtocol, Integration, IntegrationNotFound from .requirements import RequirementsNotFound, async_get_integration_with_requirements +from .setup import async_notify_setup_error from .util.package import is_docker_env from .util.unit_system import get_unit_system, validate_unit_system from .util.yaml import SECRET_YAML, Secrets, load_yaml _LOGGER = logging.getLogger(__name__) -DATA_PERSISTENT_ERRORS = "bootstrap_persistent_errors" RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml") RE_ASCII = re.compile(r"\033\[[^m]*m") YAML_CONFIG_FILE = "configuration.yaml" @@ -117,6 +119,46 @@ tts: """ +class ConfigErrorTranslationKey(StrEnum): + """Config error translation keys for config errors.""" + + # translation keys with a generated config related message text + CONFIG_VALIDATION_ERR = "config_validation_err" + PLATFORM_CONFIG_VALIDATION_ERR = "platform_config_validation_err" + + # translation keys with a general static message text + COMPONENT_IMPORT_ERR = "component_import_err" + CONFIG_PLATFORM_IMPORT_ERR = "config_platform_import_err" + CONFIG_VALIDATOR_UNKNOWN_ERR = "config_validator_unknown_err" + CONFIG_SCHEMA_UNKNOWN_ERR = "config_schema_unknown_err" + PLATFORM_VALIDATOR_UNKNOWN_ERR = "platform_validator_unknown_err" + PLATFORM_COMPONENT_LOAD_ERR = "platform_component_load_err" + PLATFORM_COMPONENT_LOAD_EXC = "platform_component_load_exc" + PLATFORM_SCHEMA_VALIDATOR_ERR = "platform_schema_validator_err" + + # translation key in case multiple errors occurred + INTEGRATION_CONFIG_ERROR = "integration_config_error" + + +@dataclass +class ConfigExceptionInfo: + """Configuration exception info class.""" + + exception: Exception + translation_key: ConfigErrorTranslationKey + platform_name: str + config: ConfigType + integration_link: str | None + + +@dataclass +class IntegrationConfigInfo: + """Configuration for an integration and exception information.""" + + config: ConfigType | None + exception_info_list: list[ConfigExceptionInfo] + + def _no_duplicate_auth_provider( configs: Sequence[dict[str, Any]] ) -> Sequence[dict[str, Any]]: @@ -1025,21 +1067,193 @@ async def merge_packages_config( return config -async def async_process_component_config( # noqa: C901 - hass: HomeAssistant, config: ConfigType, integration: Integration -) -> ConfigType | None: - """Check component configuration and return processed configuration. +@callback +def _get_log_message_and_stack_print_pref( + hass: HomeAssistant, domain: str, platform_exception: ConfigExceptionInfo +) -> tuple[str | None, bool, dict[str, str]]: + """Get message to log and print stack trace preference.""" + exception = platform_exception.exception + platform_name = platform_exception.platform_name + platform_config = platform_exception.config + link = platform_exception.integration_link - Returns None on error. + placeholders: dict[str, str] = {"domain": domain, "error": str(exception)} + + log_message_mapping: dict[ConfigErrorTranslationKey, tuple[str, bool]] = { + ConfigErrorTranslationKey.COMPONENT_IMPORT_ERR: ( + f"Unable to import {domain}: {exception}", + False, + ), + ConfigErrorTranslationKey.CONFIG_PLATFORM_IMPORT_ERR: ( + f"Error importing config platform {domain}: {exception}", + False, + ), + ConfigErrorTranslationKey.CONFIG_VALIDATOR_UNKNOWN_ERR: ( + f"Unknown error calling {domain} config validator", + True, + ), + ConfigErrorTranslationKey.CONFIG_SCHEMA_UNKNOWN_ERR: ( + f"Unknown error calling {domain} CONFIG_SCHEMA", + True, + ), + ConfigErrorTranslationKey.PLATFORM_VALIDATOR_UNKNOWN_ERR: ( + f"Unknown error validating {platform_name} platform config with {domain} " + "component platform schema", + True, + ), + ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_ERR: ( + f"Platform error: {domain} - {exception}", + False, + ), + ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_EXC: ( + f"Platform error: {domain} - {exception}", + True, + ), + ConfigErrorTranslationKey.PLATFORM_SCHEMA_VALIDATOR_ERR: ( + f"Unknown error validating config for {platform_name} platform " + f"for {domain} component with PLATFORM_SCHEMA", + True, + ), + } + log_message_show_stack_trace = log_message_mapping.get( + platform_exception.translation_key + ) + if log_message_show_stack_trace is None: + # If no pre defined log_message is set, we generate an enriched error + # message, so we can notify about it during setup + show_stack_trace = False + if isinstance(exception, vol.Invalid): + log_message = format_schema_error( + hass, exception, platform_name, platform_config, link + ) + if annotation := find_annotation(platform_config, exception.path): + placeholders["config_file"], line = annotation + placeholders["line"] = str(line) + else: + if TYPE_CHECKING: + assert isinstance(exception, HomeAssistantError) + log_message = format_homeassistant_error( + hass, exception, platform_name, platform_config, link + ) + if annotation := find_annotation(platform_config, [platform_name]): + placeholders["config_file"], line = annotation + placeholders["line"] = str(line) + show_stack_trace = True + return (log_message, show_stack_trace, placeholders) + + assert isinstance(log_message_show_stack_trace, tuple) + + return (*log_message_show_stack_trace, placeholders) + + +async def async_process_component_and_handle_errors( + hass: HomeAssistant, + config: ConfigType, + integration: Integration, + raise_on_failure: bool = False, +) -> ConfigType | None: + """Process and component configuration and handle errors. + + In case of errors: + - Print the error messages to the log. + - Raise a ConfigValidationError if raise_on_failure is set. + + Returns the integration config or `None`. + """ + integration_config_info = await async_process_component_config( + hass, config, integration + ) + return async_handle_component_errors( + hass, integration_config_info, integration, raise_on_failure + ) + + +@callback +def async_handle_component_errors( + hass: HomeAssistant, + integration_config_info: IntegrationConfigInfo, + integration: Integration, + raise_on_failure: bool = False, +) -> ConfigType | None: + """Handle component configuration errors from async_process_component_config. + + In case of errors: + - Print the error messages to the log. + - Raise a ConfigValidationError if raise_on_failure is set. + + Returns the integration config or `None`. + """ + + if not (config_exception_info := integration_config_info.exception_info_list): + return integration_config_info.config + + platform_exception: ConfigExceptionInfo + domain = integration.domain + placeholders: dict[str, str] + for platform_exception in config_exception_info: + exception = platform_exception.exception + ( + log_message, + show_stack_trace, + placeholders, + ) = _get_log_message_and_stack_print_pref(hass, domain, platform_exception) + _LOGGER.error( + log_message, + exc_info=exception if show_stack_trace else None, + ) + + if not raise_on_failure: + return integration_config_info.config + + if len(config_exception_info) == 1: + translation_key = platform_exception.translation_key + else: + translation_key = ConfigErrorTranslationKey.INTEGRATION_CONFIG_ERROR + errors = str(len(config_exception_info)) + log_message = ( + f"Failed to process component config for integration {domain} " + f"due to multiple errors ({errors}), check the logs for more information." + ) + placeholders = { + "domain": domain, + "errors": errors, + } + raise ConfigValidationError( + str(log_message), + [platform_exception.exception for platform_exception in config_exception_info], + translation_domain="homeassistant", + translation_key=translation_key, + translation_placeholders=placeholders, + ) + + +async def async_process_component_config( # noqa: C901 + hass: HomeAssistant, + config: ConfigType, + integration: Integration, +) -> IntegrationConfigInfo: + """Check component configuration. + + Returns processed configuration and exception information. This method must be run in the event loop. """ domain = integration.domain + integration_docs = integration.documentation + config_exceptions: list[ConfigExceptionInfo] = [] + try: component = integration.get_component() - except LOAD_EXCEPTIONS as ex: - _LOGGER.error("Unable to import %s: %s", domain, ex) - return None + except LOAD_EXCEPTIONS as exc: + exc_info = ConfigExceptionInfo( + exc, + ConfigErrorTranslationKey.COMPONENT_IMPORT_ERR, + domain, + config, + integration_docs, + ) + config_exceptions.append(exc_info) + return IntegrationConfigInfo(None, config_exceptions) # Check if the integration has a custom config validator config_validator = None @@ -1050,62 +1264,101 @@ async def async_process_component_config( # noqa: C901 # If the config platform contains bad imports, make sure # that still fails. if err.name != f"{integration.pkg_path}.config": - _LOGGER.error("Error importing config platform %s: %s", domain, err) - return None + exc_info = ConfigExceptionInfo( + err, + ConfigErrorTranslationKey.CONFIG_PLATFORM_IMPORT_ERR, + domain, + config, + integration_docs, + ) + config_exceptions.append(exc_info) + return IntegrationConfigInfo(None, config_exceptions) if config_validator is not None and hasattr( config_validator, "async_validate_config" ): try: - return ( # type: ignore[no-any-return] - await config_validator.async_validate_config(hass, config) + return IntegrationConfigInfo( + await config_validator.async_validate_config(hass, config), [] ) - except (vol.Invalid, HomeAssistantError) as ex: - async_log_config_validator_error( - ex, domain, config, hass, integration.documentation + except (vol.Invalid, HomeAssistantError) as exc: + exc_info = ConfigExceptionInfo( + exc, + ConfigErrorTranslationKey.CONFIG_VALIDATION_ERR, + domain, + config, + integration_docs, ) - return None - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unknown error calling %s config validator", domain) - return None + config_exceptions.append(exc_info) + return IntegrationConfigInfo(None, config_exceptions) + except Exception as exc: # pylint: disable=broad-except + exc_info = ConfigExceptionInfo( + exc, + ConfigErrorTranslationKey.CONFIG_VALIDATOR_UNKNOWN_ERR, + domain, + config, + integration_docs, + ) + config_exceptions.append(exc_info) + return IntegrationConfigInfo(None, config_exceptions) # No custom config validator, proceed with schema validation if hasattr(component, "CONFIG_SCHEMA"): try: - return component.CONFIG_SCHEMA(config) # type: ignore[no-any-return] - except vol.Invalid as ex: - async_log_schema_error(ex, domain, config, hass, integration.documentation) - return None - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unknown error calling %s CONFIG_SCHEMA", domain) - return None + return IntegrationConfigInfo(component.CONFIG_SCHEMA(config), []) + except vol.Invalid as exc: + exc_info = ConfigExceptionInfo( + exc, + ConfigErrorTranslationKey.CONFIG_VALIDATION_ERR, + domain, + config, + integration_docs, + ) + config_exceptions.append(exc_info) + return IntegrationConfigInfo(None, config_exceptions) + except Exception as exc: # pylint: disable=broad-except + exc_info = ConfigExceptionInfo( + exc, + ConfigErrorTranslationKey.CONFIG_SCHEMA_UNKNOWN_ERR, + domain, + config, + integration_docs, + ) + config_exceptions.append(exc_info) + return IntegrationConfigInfo(None, config_exceptions) component_platform_schema = getattr( component, "PLATFORM_SCHEMA_BASE", getattr(component, "PLATFORM_SCHEMA", None) ) if component_platform_schema is None: - return config + return IntegrationConfigInfo(config, []) - platforms = [] + platforms: list[ConfigType] = [] for p_name, p_config in config_per_platform(config, domain): # Validate component specific platform schema + platform_name = f"{domain}.{p_name}" try: p_validated = component_platform_schema(p_config) - except vol.Invalid as ex: - async_log_schema_error( - ex, domain, p_config, hass, integration.documentation - ) - continue - except Exception: # pylint: disable=broad-except - _LOGGER.exception( - ( - "Unknown error validating %s platform config with %s component" - " platform schema" - ), - p_name, + except vol.Invalid as exc: + exc_info = ConfigExceptionInfo( + exc, + ConfigErrorTranslationKey.PLATFORM_CONFIG_VALIDATION_ERR, domain, + p_config, + integration_docs, ) + config_exceptions.append(exc_info) + continue + except Exception as exc: # pylint: disable=broad-except + exc_info = ConfigExceptionInfo( + exc, + ConfigErrorTranslationKey.PLATFORM_SCHEMA_VALIDATOR_ERR, + str(p_name), + config, + integration_docs, + ) + config_exceptions.append(exc_info) continue # Not all platform components follow same pattern for platforms @@ -1117,38 +1370,53 @@ async def async_process_component_config( # noqa: C901 try: p_integration = await async_get_integration_with_requirements(hass, p_name) - except (RequirementsNotFound, IntegrationNotFound) as ex: - _LOGGER.error("Platform error: %s - %s", domain, ex) + except (RequirementsNotFound, IntegrationNotFound) as exc: + exc_info = ConfigExceptionInfo( + exc, + ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_ERR, + platform_name, + p_config, + integration_docs, + ) + config_exceptions.append(exc_info) continue try: platform = p_integration.get_platform(domain) - except LOAD_EXCEPTIONS: - _LOGGER.exception("Platform error: %s", domain) + except LOAD_EXCEPTIONS as exc: + exc_info = ConfigExceptionInfo( + exc, + ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_EXC, + platform_name, + p_config, + integration_docs, + ) + config_exceptions.append(exc_info) continue # Validate platform specific schema if hasattr(platform, "PLATFORM_SCHEMA"): try: p_validated = platform.PLATFORM_SCHEMA(p_config) - except vol.Invalid as ex: - async_log_schema_error( - ex, - f"{domain}.{p_name}", + except vol.Invalid as exc: + exc_info = ConfigExceptionInfo( + exc, + ConfigErrorTranslationKey.PLATFORM_CONFIG_VALIDATION_ERR, + platform_name, p_config, - hass, p_integration.documentation, ) + config_exceptions.append(exc_info) continue - except Exception: # pylint: disable=broad-except - _LOGGER.exception( - ( - "Unknown error validating config for %s platform for %s" - " component with PLATFORM_SCHEMA" - ), + except Exception as exc: # pylint: disable=broad-except + exc_info = ConfigExceptionInfo( + exc, + ConfigErrorTranslationKey.PLATFORM_SCHEMA_VALIDATOR_ERR, p_name, - domain, + p_config, + p_integration.documentation, ) + config_exceptions.append(exc_info) continue platforms.append(p_validated) @@ -1158,7 +1426,7 @@ async def async_process_component_config( # noqa: C901 config = config_without_domain(config, domain) config[domain] = platforms - return config + return IntegrationConfigInfo(config, config_exceptions) @callback @@ -1183,36 +1451,6 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> str | None: return res.error_str -@callback -def async_notify_setup_error( - hass: HomeAssistant, component: str, display_link: str | None = None -) -> None: - """Print a persistent notification. - - This method must be run in the event loop. - """ - # pylint: disable-next=import-outside-toplevel - from .components import persistent_notification - - if (errors := hass.data.get(DATA_PERSISTENT_ERRORS)) is None: - errors = hass.data[DATA_PERSISTENT_ERRORS] = {} - - errors[component] = errors.get(component) or display_link - - message = "The following integrations and platforms could not be set up:\n\n" - - for name, link in errors.items(): - show_logs = f"[Show logs](/config/logs?filter={name})" - part = f"[{name}]({link})" if link else name - message += f" - {part} ({show_logs})\n" - - message += "\nPlease check your config and [logs](/config/logs)." - - persistent_notification.async_create( - hass, message, "Invalid config", "invalid_config" - ) - - def safe_mode_enabled(config_dir: str) -> bool: """Return if safe mode is enabled. diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 262b0e338ff..8d5e2bbde95 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -26,6 +26,31 @@ class HomeAssistantError(Exception): self.translation_placeholders = translation_placeholders +class ConfigValidationError(HomeAssistantError, ExceptionGroup[Exception]): + """A validation exception occurred when validating the configuration.""" + + def __init__( + self, + message: str, + exceptions: list[Exception], + translation_domain: str | None = None, + translation_key: str | None = None, + translation_placeholders: dict[str, str] | None = None, + ) -> None: + """Initialize exception.""" + super().__init__( + *(message, exceptions), + translation_domain=translation_domain, + translation_key=translation_key, + translation_placeholders=translation_placeholders, + ) + self._message = message + + def __str__(self) -> str: + """Return exception message string.""" + return self._message + + class ServiceValidationError(HomeAssistantError): """A validation exception occurred when calling a service.""" diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index ddd46759259..775d0934c36 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -355,7 +355,7 @@ class EntityComponent(Generic[_EntityT]): integration = await async_get_integration(self.hass, self.domain) - processed_conf = await conf_util.async_process_component_config( + processed_conf = await conf_util.async_process_component_and_handle_errors( self.hass, conf, integration ) diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index 6e719cdac24..42ebc2d0869 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Iterable import logging -from typing import Any +from typing import Any, Literal, overload from homeassistant import config as conf_util from homeassistant.const import SERVICE_RELOAD @@ -60,7 +60,7 @@ async def _resetup_platform( """Resetup a platform.""" integration = await async_get_integration(hass, platform_domain) - conf = await conf_util.async_process_component_config( + conf = await conf_util.async_process_component_and_handle_errors( hass, unprocessed_config, integration ) @@ -136,14 +136,41 @@ async def _async_reconfig_platform( await asyncio.gather(*tasks) +@overload async def async_integration_yaml_config( hass: HomeAssistant, integration_name: str +) -> ConfigType | None: + ... + + +@overload +async def async_integration_yaml_config( + hass: HomeAssistant, + integration_name: str, + *, + raise_on_failure: Literal[True], +) -> ConfigType: + ... + + +@overload +async def async_integration_yaml_config( + hass: HomeAssistant, + integration_name: str, + *, + raise_on_failure: Literal[False] | bool, +) -> ConfigType | None: + ... + + +async def async_integration_yaml_config( + hass: HomeAssistant, integration_name: str, *, raise_on_failure: bool = False ) -> ConfigType | None: """Fetch the latest yaml configuration for an integration.""" integration = await async_get_integration(hass, integration_name) - - return await conf_util.async_process_component_config( - hass, await conf_util.async_hass_config_yaml(hass), integration + config = await conf_util.async_hass_config_yaml(hass) + return await conf_util.async_process_component_and_handle_errors( + hass, config, integration, raise_on_failure=raise_on_failure ) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 9b705b4735e..679042bc4e9 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -11,14 +11,13 @@ from types import ModuleType from typing import Any from . import config as conf_util, core, loader, requirements -from .config import async_notify_setup_error from .const import ( EVENT_COMPONENT_LOADED, EVENT_HOMEASSISTANT_START, PLATFORM_FORMAT, Platform, ) -from .core import CALLBACK_TYPE, DOMAIN as HOMEASSISTANT_DOMAIN +from .core import CALLBACK_TYPE, DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback from .exceptions import DependencyError, HomeAssistantError from .helpers.issue_registry import IssueSeverity, async_create_issue from .helpers.typing import ConfigType @@ -56,10 +55,47 @@ DATA_SETUP_TIME = "setup_time" DATA_DEPS_REQS = "deps_reqs_processed" +DATA_PERSISTENT_ERRORS = "bootstrap_persistent_errors" + +NOTIFY_FOR_TRANSLATION_KEYS = [ + "config_validation_err", + "platform_config_validation_err", +] + SLOW_SETUP_WARNING = 10 SLOW_SETUP_MAX_WAIT = 300 +@callback +def async_notify_setup_error( + hass: HomeAssistant, component: str, display_link: str | None = None +) -> None: + """Print a persistent notification. + + This method must be run in the event loop. + """ + # pylint: disable-next=import-outside-toplevel + from .components import persistent_notification + + if (errors := hass.data.get(DATA_PERSISTENT_ERRORS)) is None: + errors = hass.data[DATA_PERSISTENT_ERRORS] = {} + + errors[component] = errors.get(component) or display_link + + message = "The following integrations and platforms could not be set up:\n\n" + + for name, link in errors.items(): + show_logs = f"[Show logs](/config/logs?filter={name})" + part = f"[{name}]({link})" if link else name + message += f" - {part} ({show_logs})\n" + + message += "\nPlease check your config and [logs](/config/logs)." + + persistent_notification.async_create( + hass, message, "Invalid config", "invalid_config" + ) + + @core.callback def async_set_domains_to_be_loaded(hass: core.HomeAssistant, domains: set[str]) -> None: """Set domains that are going to be loaded from the config. @@ -217,10 +253,18 @@ async def _async_setup_component( log_error(f"Unable to import component: {err}", err) return False - processed_config = await conf_util.async_process_component_config( + integration_config_info = await conf_util.async_process_component_config( hass, config, integration ) - + processed_config = conf_util.async_handle_component_errors( + hass, integration_config_info, integration + ) + for platform_exception in integration_config_info.exception_info_list: + if platform_exception.translation_key not in NOTIFY_FOR_TRANSLATION_KEYS: + continue + async_notify_setup_error( + hass, platform_exception.platform_name, platform_exception.integration_link + ) if processed_config is None: log_error("Invalid config.") return False diff --git a/tests/common.py b/tests/common.py index bc770fae2fe..06cdee06ec9 100644 --- a/tests/common.py +++ b/tests/common.py @@ -984,7 +984,10 @@ def assert_setup_component(count, domain=None): async def mock_psc(hass, config_input, integration): """Mock the prepare_setup_component to capture config.""" domain_input = integration.domain - res = await async_process_component_config(hass, config_input, integration) + integration_config_info = await async_process_component_config( + hass, config_input, integration + ) + res = integration_config_info.config config[domain_input] = None if res is None else res.get(domain_input) _LOGGER.debug( "Configuration for %s, Validated: %s, Original %s", @@ -992,7 +995,7 @@ def assert_setup_component(count, domain=None): config[domain_input], config_input.get(domain_input), ) - return res + return integration_config_info assert isinstance(config, dict) with patch("homeassistant.config.async_process_component_config", mock_psc): diff --git a/tests/helpers/test_reload.py b/tests/helpers/test_reload.py index 9c3789a3553..586dbc19eb8 100644 --- a/tests/helpers/test_reload.py +++ b/tests/helpers/test_reload.py @@ -3,10 +3,12 @@ import logging from unittest.mock import AsyncMock, Mock, patch import pytest +import voluptuous as vol from homeassistant import config from homeassistant.const import SERVICE_RELOAD from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigValidationError, HomeAssistantError from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_platform import async_get_platforms from homeassistant.helpers.reload import ( @@ -139,7 +141,9 @@ async def test_setup_reload_service_when_async_process_component_config_fails( yaml_path = get_fixture_path("helpers/reload_configuration.yaml") with patch.object(config, "YAML_CONFIG_FILE", yaml_path), patch.object( - config, "async_process_component_config", return_value=None + config, + "async_process_component_config", + return_value=config.IntegrationConfigInfo(None, []), ): await hass.services.async_call( PLATFORM, @@ -208,8 +212,49 @@ async def test_async_integration_yaml_config(hass: HomeAssistant) -> None: yaml_path = get_fixture_path(f"helpers/{DOMAIN}_configuration.yaml") with patch.object(config, "YAML_CONFIG_FILE", yaml_path): processed_config = await async_integration_yaml_config(hass, DOMAIN) + assert processed_config == {DOMAIN: [{"name": "one"}, {"name": "two"}]} + # Test fetching yaml config does not raise when the raise_on_failure option is set + processed_config = await async_integration_yaml_config( + hass, DOMAIN, raise_on_failure=True + ) + assert processed_config == {DOMAIN: [{"name": "one"}, {"name": "two"}]} - assert processed_config == {DOMAIN: [{"name": "one"}, {"name": "two"}]} + +async def test_async_integration_failing_yaml_config(hass: HomeAssistant) -> None: + """Test reloading yaml config for an integration fails. + + In case an integration reloads its yaml configuration it should throw when + the new config failed to load and raise_on_failure is set to True. + """ + schema_without_name_attr = vol.Schema({vol.Required("some_option"): str}) + + mock_integration(hass, MockModule(DOMAIN, config_schema=schema_without_name_attr)) + + yaml_path = get_fixture_path(f"helpers/{DOMAIN}_configuration.yaml") + with patch.object(config, "YAML_CONFIG_FILE", yaml_path): + # Test fetching yaml config does not raise without raise_on_failure option + processed_config = await async_integration_yaml_config(hass, DOMAIN) + assert processed_config is None + # Test fetching yaml config does not raise when the raise_on_failure option is set + with pytest.raises(ConfigValidationError): + await async_integration_yaml_config(hass, DOMAIN, raise_on_failure=True) + + +async def test_async_integration_failing_on_reload(hass: HomeAssistant) -> None: + """Test reloading yaml config for an integration fails with an other exception. + + In case an integration reloads its yaml configuration it should throw when + the new config failed to load and raise_on_failure is set to True. + """ + mock_integration(hass, MockModule(DOMAIN)) + + yaml_path = get_fixture_path(f"helpers/{DOMAIN}_configuration.yaml") + with patch.object(config, "YAML_CONFIG_FILE", yaml_path), patch( + "homeassistant.config.async_process_component_config", + side_effect=HomeAssistantError(), + ), pytest.raises(HomeAssistantError): + # Test fetching yaml config does raise when the raise_on_failure option is set + await async_integration_yaml_config(hass, DOMAIN, raise_on_failure=True) async def test_async_integration_missing_yaml_config(hass: HomeAssistant) -> None: diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index c3e25219369..f6d3b92bb4a 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1013,7 +1013,10 @@ async def test_bootstrap_dependencies( with patch( "homeassistant.setup.loader.async_get_integrations", side_effect=mock_async_get_integrations, - ), patch("homeassistant.config.async_process_component_config", return_value={}): + ), patch( + "homeassistant.config.async_process_component_config", + return_value=config_util.IntegrationConfigInfo({}, []), + ): bootstrap.async_set_domains_to_be_loaded(hass, {integration}) await bootstrap.async_setup_multi_components(hass, {integration}, {}) await hass.async_block_till_done() diff --git a/tests/test_config.py b/tests/test_config.py index 448990429a1..de5e7e0581d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -30,6 +30,7 @@ from homeassistant.const import ( __version__, ) from homeassistant.core import ConfigSource, HomeAssistant, HomeAssistantError +from homeassistant.exceptions import ConfigValidationError from homeassistant.helpers import config_validation as cv, issue_registry as ir import homeassistant.helpers.check_config as check_config from homeassistant.helpers.entity import Entity @@ -1427,71 +1428,132 @@ async def test_component_config_exceptions( ) -> None: """Test unexpected exceptions validating component config.""" # Config validator + test_integration = Mock( + domain="test_domain", + get_platform=Mock( + return_value=Mock( + async_validate_config=AsyncMock(side_effect=ValueError("broken")) + ) + ), + ) assert ( - await config_util.async_process_component_config( - hass, - {}, - integration=Mock( - domain="test_domain", - get_platform=Mock( - return_value=Mock( - async_validate_config=AsyncMock( - side_effect=ValueError("broken") - ) - ) - ), - ), + await config_util.async_process_component_and_handle_errors( + hass, {}, integration=test_integration ) is None ) assert "ValueError: broken" in caplog.text assert "Unknown error calling test_domain config validator" in caplog.text + caplog.clear() + with pytest.raises(HomeAssistantError) as ex: + await config_util.async_process_component_and_handle_errors( + hass, {}, integration=test_integration, raise_on_failure=True + ) + assert "ValueError: broken" in caplog.text + assert "Unknown error calling test_domain config validator" in caplog.text + assert str(ex.value) == "Unknown error calling test_domain config validator" - # component.CONFIG_SCHEMA + test_integration = Mock( + domain="test_domain", + get_platform=Mock( + return_value=Mock( + async_validate_config=AsyncMock( + side_effect=HomeAssistantError("broken") + ) + ) + ), + get_component=Mock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])), + ) caplog.clear() assert ( - await config_util.async_process_component_config( - hass, - {}, - integration=Mock( - domain="test_domain", - get_platform=Mock(return_value=None), - get_component=Mock( - return_value=Mock( - CONFIG_SCHEMA=Mock(side_effect=ValueError("broken")) - ) - ), - ), + await config_util.async_process_component_and_handle_errors( + hass, {}, integration=test_integration, raise_on_failure=False + ) + is None + ) + assert "Invalid config for 'test_domain': broken" in caplog.text + with pytest.raises(HomeAssistantError) as ex: + await config_util.async_process_component_and_handle_errors( + hass, {}, integration=test_integration, raise_on_failure=True + ) + assert "Invalid config for 'test_domain': broken" in str(ex.value) + + # component.CONFIG_SCHEMA + caplog.clear() + test_integration = Mock( + domain="test_domain", + get_platform=Mock(return_value=None), + get_component=Mock( + return_value=Mock(CONFIG_SCHEMA=Mock(side_effect=ValueError("broken"))) + ), + ) + assert ( + await config_util.async_process_component_and_handle_errors( + hass, + {}, + integration=test_integration, + raise_on_failure=False, ) is None ) - assert "ValueError: broken" in caplog.text assert "Unknown error calling test_domain CONFIG_SCHEMA" in caplog.text + with pytest.raises(HomeAssistantError) as ex: + await config_util.async_process_component_and_handle_errors( + hass, + {}, + integration=test_integration, + raise_on_failure=True, + ) + assert "Unknown error calling test_domain CONFIG_SCHEMA" in caplog.text + assert str(ex.value) == "Unknown error calling test_domain CONFIG_SCHEMA" # component.PLATFORM_SCHEMA caplog.clear() - assert await config_util.async_process_component_config( + test_integration = Mock( + domain="test_domain", + get_platform=Mock(return_value=None), + get_component=Mock( + return_value=Mock( + spec=["PLATFORM_SCHEMA_BASE"], + PLATFORM_SCHEMA_BASE=Mock(side_effect=ValueError("broken")), + ) + ), + ) + assert await config_util.async_process_component_and_handle_errors( hass, {"test_domain": {"platform": "test_platform"}}, - integration=Mock( - domain="test_domain", - get_platform=Mock(return_value=None), - get_component=Mock( - return_value=Mock( - spec=["PLATFORM_SCHEMA_BASE"], - PLATFORM_SCHEMA_BASE=Mock(side_effect=ValueError("broken")), - ) - ), - ), + integration=test_integration, + raise_on_failure=False, ) == {"test_domain": []} assert "ValueError: broken" in caplog.text assert ( - "Unknown error validating test_platform platform config " - "with test_domain component platform schema" + "Unknown error validating config for test_platform platform " + "for test_domain component with PLATFORM_SCHEMA" ) in caplog.text + caplog.clear() + with pytest.raises(HomeAssistantError) as ex: + await config_util.async_process_component_and_handle_errors( + hass, + {"test_domain": {"platform": "test_platform"}}, + integration=test_integration, + raise_on_failure=True, + ) + assert ( + "Unknown error validating config for test_platform platform " + "for test_domain component with PLATFORM_SCHEMA" + ) in caplog.text + assert str(ex.value) == ( + "Unknown error validating config for test_platform platform " + "for test_domain component with PLATFORM_SCHEMA" + ) # platform.PLATFORM_SCHEMA caplog.clear() + test_integration = Mock( + domain="test_domain", + get_platform=Mock(return_value=None), + get_component=Mock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])), + ) with patch( "homeassistant.config.async_get_integration_with_requirements", return_value=Mock( # integration that owns platform @@ -1502,67 +1564,337 @@ async def test_component_config_exceptions( ) ), ): - assert await config_util.async_process_component_config( + assert await config_util.async_process_component_and_handle_errors( hass, {"test_domain": {"platform": "test_platform"}}, - integration=Mock( - domain="test_domain", - get_platform=Mock(return_value=None), - get_component=Mock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])), - ), + integration=test_integration, + raise_on_failure=False, ) == {"test_domain": []} assert "ValueError: broken" in caplog.text + assert ( + "Unknown error validating config for test_platform platform for test_domain" + " component with PLATFORM_SCHEMA" + ) in caplog.text + caplog.clear() + with pytest.raises(HomeAssistantError) as ex: + assert await config_util.async_process_component_and_handle_errors( + hass, + {"test_domain": {"platform": "test_platform"}}, + integration=test_integration, + raise_on_failure=True, + ) + assert ( + "Unknown error validating config for test_platform platform for test_domain" + " component with PLATFORM_SCHEMA" + ) in str(ex.value) + assert "ValueError: broken" in caplog.text assert ( "Unknown error validating config for test_platform platform for test_domain" " component with PLATFORM_SCHEMA" in caplog.text ) + # Test multiple platform failures + assert await config_util.async_process_component_and_handle_errors( + hass, + { + "test_domain": [ + {"platform": "test_platform1"}, + {"platform": "test_platform2"}, + ] + }, + integration=test_integration, + raise_on_failure=False, + ) == {"test_domain": []} + assert "ValueError: broken" in caplog.text + assert ( + "Unknown error validating config for test_platform1 platform " + "for test_domain component with PLATFORM_SCHEMA" + ) in caplog.text + assert ( + "Unknown error validating config for test_platform2 platform " + "for test_domain component with PLATFORM_SCHEMA" + ) in caplog.text + caplog.clear() + with pytest.raises(HomeAssistantError) as ex: + assert await config_util.async_process_component_and_handle_errors( + hass, + { + "test_domain": [ + {"platform": "test_platform1"}, + {"platform": "test_platform2"}, + ] + }, + integration=test_integration, + raise_on_failure=True, + ) + assert ( + "Failed to process component config for integration test_domain" + " due to multiple errors (2), check the logs for more information." + ) in str(ex.value) + assert "ValueError: broken" in caplog.text + assert ( + "Unknown error validating config for test_platform1 platform " + "for test_domain component with PLATFORM_SCHEMA" + ) in caplog.text + assert ( + "Unknown error validating config for test_platform2 platform " + "for test_domain component with PLATFORM_SCHEMA" + ) in caplog.text + + # get_platform("domain") raising on ImportError + caplog.clear() + test_integration = Mock( + domain="test_domain", + get_platform=Mock(return_value=None), + get_component=Mock(return_value=Mock(spec=["PLATFORM_SCHEMA_BASE"])), + ) + import_error = ImportError( + ("ModuleNotFoundError: No module named 'not_installed_something'"), + name="not_installed_something", + ) + with patch( + "homeassistant.config.async_get_integration_with_requirements", + return_value=Mock( # integration that owns platform + get_platform=Mock(side_effect=import_error) + ), + ): + assert await config_util.async_process_component_and_handle_errors( + hass, + {"test_domain": {"platform": "test_platform"}}, + integration=test_integration, + raise_on_failure=False, + ) == {"test_domain": []} + assert ( + "ImportError: ModuleNotFoundError: No module named " + "'not_installed_something'" in caplog.text + ) + caplog.clear() + with pytest.raises(HomeAssistantError) as ex: + assert await config_util.async_process_component_and_handle_errors( + hass, + {"test_domain": {"platform": "test_platform"}}, + integration=test_integration, + raise_on_failure=True, + ) + assert ( + "ImportError: ModuleNotFoundError: No module named " + "'not_installed_something'" in caplog.text + ) + assert ( + "Platform error: test_domain - ModuleNotFoundError: " + "No module named 'not_installed_something'" + ) in caplog.text + assert ( + "Platform error: test_domain - ModuleNotFoundError: " + "No module named 'not_installed_something'" + ) in str(ex.value) # get_platform("config") raising caplog.clear() + test_integration = Mock( + pkg_path="homeassistant.components.test_domain", + domain="test_domain", + get_platform=Mock( + side_effect=ImportError( + ("ModuleNotFoundError: No module named 'not_installed_something'"), + name="not_installed_something", + ) + ), + ) assert ( - await config_util.async_process_component_config( + await config_util.async_process_component_and_handle_errors( hass, {"test_domain": {}}, - integration=Mock( - pkg_path="homeassistant.components.test_domain", - domain="test_domain", - get_platform=Mock( - side_effect=ImportError( - ( - "ModuleNotFoundError: No module named" - " 'not_installed_something'" - ), - name="not_installed_something", - ) - ), - ), + integration=test_integration, + raise_on_failure=False, ) is None ) assert ( - "Error importing config platform test_domain: ModuleNotFoundError: No module" - " named 'not_installed_something'" in caplog.text + "Error importing config platform test_domain: ModuleNotFoundError: " + "No module named 'not_installed_something'" in caplog.text + ) + with pytest.raises(HomeAssistantError) as ex: + await config_util.async_process_component_and_handle_errors( + hass, + {"test_domain": {}}, + integration=test_integration, + raise_on_failure=True, + ) + assert ( + "Error importing config platform test_domain: ModuleNotFoundError: " + "No module named 'not_installed_something'" in caplog.text + ) + assert ( + "Error importing config platform test_domain: ModuleNotFoundError: " + "No module named 'not_installed_something'" in str(ex.value) ) # get_component raising caplog.clear() + test_integration = Mock( + pkg_path="homeassistant.components.test_domain", + domain="test_domain", + get_component=Mock( + side_effect=FileNotFoundError("No such file or directory: b'liblibc.a'") + ), + ) assert ( - await config_util.async_process_component_config( + await config_util.async_process_component_and_handle_errors( hass, {"test_domain": {}}, - integration=Mock( - pkg_path="homeassistant.components.test_domain", - domain="test_domain", - get_component=Mock( - side_effect=FileNotFoundError( - "No such file or directory: b'liblibc.a'" - ) - ), - ), + integration=test_integration, + raise_on_failure=False, ) is None ) assert "Unable to import test_domain: No such file or directory" in caplog.text + with pytest.raises(HomeAssistantError) as ex: + await config_util.async_process_component_and_handle_errors( + hass, + {"test_domain": {}}, + integration=test_integration, + raise_on_failure=True, + ) + assert "Unable to import test_domain: No such file or directory" in caplog.text + assert "Unable to import test_domain: No such file or directory" in str(ex.value) + + +@pytest.mark.parametrize( + ("exception_info_list", "error", "messages", "show_stack_trace", "translation_key"), + [ + ( + [ + config_util.ConfigExceptionInfo( + ImportError("bla"), + "component_import_err", + "test_domain", + {"test_domain": []}, + "https://example.com", + ) + ], + "bla", + ["Unable to import test_domain: bla", "bla"], + False, + "component_import_err", + ), + ( + [ + config_util.ConfigExceptionInfo( + HomeAssistantError("bla"), + "config_validation_err", + "test_domain", + {"test_domain": []}, + "https://example.com", + ) + ], + "bla", + [ + "Invalid config for 'test_domain': bla, " + "please check the docs at https://example.com", + "bla", + ], + True, + "config_validation_err", + ), + ( + [ + config_util.ConfigExceptionInfo( + vol.Invalid("bla", ["path"]), + "config_validation_err", + "test_domain", + {"test_domain": []}, + "https://example.com", + ) + ], + "bla @ data['path']", + [ + "Invalid config for 'test_domain': bla 'path', got None, " + "please check the docs at https://example.com", + "bla", + ], + False, + "config_validation_err", + ), + ( + [ + config_util.ConfigExceptionInfo( + vol.Invalid("bla", ["path"]), + "platform_config_validation_err", + "test_domain", + {"test_domain": []}, + "https://alt.example.com", + ) + ], + "bla @ data['path']", + [ + "Invalid config for 'test_domain': bla 'path', got None, " + "please check the docs at https://alt.example.com", + "bla", + ], + False, + "platform_config_validation_err", + ), + ( + [ + config_util.ConfigExceptionInfo( + ImportError("bla"), + "platform_component_load_err", + "test_domain", + {"test_domain": []}, + "https://example.com", + ) + ], + "bla", + ["Platform error: test_domain - bla", "bla"], + False, + "platform_component_load_err", + ), + ], +) +async def test_component_config_error_processing( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + error: str, + exception_info_list: list[config_util.ConfigExceptionInfo], + messages: list[str], + show_stack_trace: bool, + translation_key: str, +) -> None: + """Test component config error processing.""" + test_integration = Mock( + domain="test_domain", + documentation="https://example.com", + get_platform=Mock( + return_value=Mock( + async_validate_config=AsyncMock(side_effect=ValueError("broken")) + ) + ), + ) + with patch( + "homeassistant.config.async_process_component_config", + return_value=config_util.IntegrationConfigInfo(None, exception_info_list), + ), pytest.raises(ConfigValidationError) as ex: + await config_util.async_process_component_and_handle_errors( + hass, {}, test_integration, raise_on_failure=True + ) + records = [record for record in caplog.records if record.msg == messages[0]] + assert len(records) == 1 + assert (records[0].exc_info is not None) == show_stack_trace + assert str(ex.value) == messages[0] + assert ex.value.translation_key == translation_key + assert ex.value.translation_domain == "homeassistant" + assert ex.value.translation_placeholders["domain"] == "test_domain" + assert all(message in caplog.text for message in messages) + + caplog.clear() + with patch( + "homeassistant.config.async_process_component_config", + return_value=config_util.IntegrationConfigInfo(None, exception_info_list), + ): + await config_util.async_process_component_and_handle_errors( + hass, {}, test_integration + ) + assert all(message in caplog.text for message in messages) @pytest.mark.parametrize( @@ -1713,7 +2045,7 @@ async def test_component_config_validation_error( integration = await async_get_integration( hass, domain_with_label.partition(" ")[0] ) - await config_util.async_process_component_config( + await config_util.async_process_component_and_handle_errors( hass, config, integration=integration, @@ -1758,7 +2090,7 @@ async def test_component_config_validation_error_with_docs( integration = await async_get_integration( hass, domain_with_label.partition(" ")[0] ) - await config_util.async_process_component_config( + await config_util.async_process_component_and_handle_errors( hass, config, integration=integration, diff --git a/tests/test_setup.py b/tests/test_setup.py index 0f480198c11..00bb3fa2a2d 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -374,7 +374,7 @@ async def test_platform_specific_config_validation(hass: HomeAssistant) -> None: ) with assert_setup_component(0, "switch"), patch( - "homeassistant.config.async_notify_setup_error" + "homeassistant.setup.async_notify_setup_error" ) as mock_notify: assert await setup.async_setup_component( hass, @@ -389,7 +389,7 @@ async def test_platform_specific_config_validation(hass: HomeAssistant) -> None: hass.config.components.remove("switch") with assert_setup_component(0), patch( - "homeassistant.config.async_notify_setup_error" + "homeassistant.setup.async_notify_setup_error" ) as mock_notify: assert await setup.async_setup_component( hass, @@ -410,7 +410,7 @@ async def test_platform_specific_config_validation(hass: HomeAssistant) -> None: hass.config.components.remove("switch") with assert_setup_component(1, "switch"), patch( - "homeassistant.config.async_notify_setup_error" + "homeassistant.setup.async_notify_setup_error" ) as mock_notify: assert await setup.async_setup_component( hass,