2019-07-10 18:56:50 +00:00
|
|
|
"""Helper to check the configuration file."""
|
2021-02-12 09:58:20 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
from collections import OrderedDict
|
2020-11-26 21:25:21 +00:00
|
|
|
import logging
|
2020-01-14 21:03:02 +00:00
|
|
|
import os
|
2021-03-02 20:58:53 +00:00
|
|
|
from pathlib import Path
|
2023-07-22 22:03:44 +00:00
|
|
|
from typing import NamedTuple, Self
|
2019-07-10 18:56:50 +00:00
|
|
|
|
|
|
|
import voluptuous as vol
|
|
|
|
|
2019-08-07 22:35:50 +00:00
|
|
|
from homeassistant import loader
|
2022-01-26 09:55:06 +00:00
|
|
|
from homeassistant.config import ( # type: ignore[attr-defined]
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_CORE,
|
|
|
|
CONF_PACKAGES,
|
2019-12-09 15:42:10 +00:00
|
|
|
CORE_CONFIG_SCHEMA,
|
2020-01-14 21:03:02 +00:00
|
|
|
YAML_CONFIG_FILE,
|
2019-07-31 19:25:30 +00:00
|
|
|
_format_config_error,
|
2019-12-09 15:42:10 +00:00
|
|
|
config_per_platform,
|
|
|
|
extract_domain_configs,
|
2019-07-31 19:25:30 +00:00
|
|
|
load_yaml_config_file,
|
2019-12-09 15:42:10 +00:00
|
|
|
merge_packages_config,
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2019-12-09 15:42:10 +00:00
|
|
|
from homeassistant.core import HomeAssistant
|
|
|
|
from homeassistant.exceptions import HomeAssistantError
|
2019-08-07 22:35:50 +00:00
|
|
|
from homeassistant.requirements import (
|
|
|
|
RequirementsNotFound,
|
2021-09-26 19:47:03 +00:00
|
|
|
async_clear_install_history,
|
2019-12-09 15:42:10 +00:00
|
|
|
async_get_integration_with_requirements,
|
2019-08-07 22:35:50 +00:00
|
|
|
)
|
2019-07-10 18:56:50 +00:00
|
|
|
import homeassistant.util.yaml.loader as yaml_loader
|
|
|
|
|
2021-12-23 19:14:47 +00:00
|
|
|
from .typing import ConfigType
|
|
|
|
|
2019-07-21 16:59:02 +00:00
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
class CheckConfigError(NamedTuple):
|
|
|
|
"""Configuration check error."""
|
|
|
|
|
|
|
|
message: str
|
2021-03-17 17:34:19 +00:00
|
|
|
domain: str | None
|
|
|
|
config: ConfigType | None
|
2019-07-10 18:56:50 +00:00
|
|
|
|
|
|
|
|
|
|
|
class HomeAssistantConfig(OrderedDict):
|
|
|
|
"""Configuration result with errors attribute."""
|
|
|
|
|
2020-11-26 21:25:21 +00:00
|
|
|
def __init__(self) -> None:
|
|
|
|
"""Initialize HA config."""
|
|
|
|
super().__init__()
|
2021-03-17 17:34:19 +00:00
|
|
|
self.errors: list[CheckConfigError] = []
|
2019-07-10 18:56:50 +00:00
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
def add_error(
|
|
|
|
self,
|
|
|
|
message: str,
|
2021-03-17 17:34:19 +00:00
|
|
|
domain: str | None = None,
|
|
|
|
config: ConfigType | None = None,
|
2023-02-07 04:30:22 +00:00
|
|
|
) -> Self:
|
2019-07-10 18:56:50 +00:00
|
|
|
"""Add a single error."""
|
|
|
|
self.errors.append(CheckConfigError(str(message), domain, config))
|
|
|
|
return self
|
|
|
|
|
|
|
|
@property
|
|
|
|
def error_str(self) -> str:
|
|
|
|
"""Return errors as a string."""
|
2019-07-31 19:25:30 +00:00
|
|
|
return "\n".join([err.message for err in self.errors])
|
2019-07-10 18:56:50 +00:00
|
|
|
|
|
|
|
|
2021-04-25 10:38:40 +00:00
|
|
|
async def async_check_ha_config_file( # noqa: C901
|
|
|
|
hass: HomeAssistant,
|
|
|
|
) -> HomeAssistantConfig:
|
2019-07-10 18:56:50 +00:00
|
|
|
"""Load and check if Home Assistant configuration file is valid.
|
|
|
|
|
|
|
|
This method is a coroutine.
|
|
|
|
"""
|
|
|
|
result = HomeAssistantConfig()
|
2021-09-26 19:47:03 +00:00
|
|
|
async_clear_install_history(hass)
|
2019-07-10 18:56:50 +00:00
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
def _pack_error(
|
|
|
|
package: str, component: str, config: ConfigType, message: str
|
|
|
|
) -> None:
|
2019-07-10 18:56:50 +00:00
|
|
|
"""Handle errors from packages: _log_pkg_error."""
|
2020-01-03 13:47:06 +00:00
|
|
|
message = f"Package {package} setup failed. Component {component} {message}"
|
2019-08-23 16:53:33 +00:00
|
|
|
domain = f"homeassistant.packages.{package}.{component}"
|
2019-07-10 18:56:50 +00:00
|
|
|
pack_config = core_config[CONF_PACKAGES].get(package, config)
|
|
|
|
result.add_error(message, domain, pack_config)
|
|
|
|
|
2019-12-22 18:51:39 +00:00
|
|
|
def _comp_error(ex: Exception, domain: str, config: ConfigType) -> None:
|
2019-07-10 18:56:50 +00:00
|
|
|
"""Handle errors from components: async_log_exception."""
|
2021-01-05 12:55:38 +00:00
|
|
|
result.add_error(_format_config_error(ex, domain, config)[0], domain, config)
|
2019-07-10 18:56:50 +00:00
|
|
|
|
|
|
|
# Load configuration.yaml
|
2020-01-14 21:03:02 +00:00
|
|
|
config_path = hass.config.path(YAML_CONFIG_FILE)
|
2019-07-10 18:56:50 +00:00
|
|
|
try:
|
2020-01-14 21:03:02 +00:00
|
|
|
if not await hass.async_add_executor_job(os.path.isfile, config_path):
|
2019-07-10 18:56:50 +00:00
|
|
|
return result.add_error("File configuration.yaml not found.")
|
2021-03-02 20:58:53 +00:00
|
|
|
|
|
|
|
config = await hass.async_add_executor_job(
|
|
|
|
load_yaml_config_file,
|
|
|
|
config_path,
|
|
|
|
yaml_loader.Secrets(Path(hass.config.config_dir)),
|
|
|
|
)
|
2019-07-10 18:56:50 +00:00
|
|
|
except FileNotFoundError:
|
2019-08-23 16:53:33 +00:00
|
|
|
return result.add_error(f"File not found: {config_path}")
|
2019-07-10 18:56:50 +00:00
|
|
|
except HomeAssistantError as err:
|
2019-08-23 16:53:33 +00:00
|
|
|
return result.add_error(f"Error loading {config_path}: {err}")
|
2019-07-10 18:56:50 +00:00
|
|
|
|
|
|
|
# Extract and validate core [homeassistant] config
|
|
|
|
try:
|
|
|
|
core_config = config.pop(CONF_CORE, {})
|
|
|
|
core_config = CORE_CONFIG_SCHEMA(core_config)
|
|
|
|
result[CONF_CORE] = core_config
|
|
|
|
except vol.Invalid as err:
|
|
|
|
result.add_error(err, CONF_CORE, core_config)
|
|
|
|
core_config = {}
|
|
|
|
|
|
|
|
# Merge packages
|
|
|
|
await merge_packages_config(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass, config, core_config.get(CONF_PACKAGES, {}), _pack_error
|
|
|
|
)
|
2019-07-10 18:56:50 +00:00
|
|
|
core_config.pop(CONF_PACKAGES, None)
|
|
|
|
|
|
|
|
# Filter out repeating config sections
|
2023-02-15 11:39:12 +00:00
|
|
|
components = {key.partition(" ")[0] for key in config}
|
2019-07-10 18:56:50 +00:00
|
|
|
|
|
|
|
# Process and validate config
|
|
|
|
for domain in components:
|
|
|
|
try:
|
2019-08-07 22:35:50 +00:00
|
|
|
integration = await async_get_integration_with_requirements(hass, domain)
|
2021-09-18 05:25:50 +00:00
|
|
|
except loader.IntegrationNotFound as ex:
|
|
|
|
if not hass.config.safe_mode:
|
|
|
|
result.add_error(f"Integration error: {domain} - {ex}")
|
|
|
|
continue
|
|
|
|
except RequirementsNotFound as ex:
|
|
|
|
result.add_error(f"Integration error: {domain} - {ex}")
|
2019-07-10 18:56:50 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
try:
|
|
|
|
component = integration.get_component()
|
2019-08-07 22:35:50 +00:00
|
|
|
except ImportError as ex:
|
2019-08-23 16:53:33 +00:00
|
|
|
result.add_error(f"Component error: {domain} - {ex}")
|
2019-07-10 18:56:50 +00:00
|
|
|
continue
|
|
|
|
|
2020-11-19 21:05:36 +00:00
|
|
|
# Check if the integration has a custom config validator
|
|
|
|
config_validator = None
|
|
|
|
try:
|
|
|
|
config_validator = integration.get_platform("config")
|
|
|
|
except ImportError as err:
|
|
|
|
# Filter out import error of the config platform.
|
|
|
|
# If the config platform contains bad imports, make sure
|
|
|
|
# that still fails.
|
|
|
|
if err.name != f"{integration.pkg_path}.config":
|
|
|
|
result.add_error(f"Error importing config platform {domain}: {err}")
|
|
|
|
continue
|
|
|
|
|
|
|
|
if config_validator is not None and hasattr(
|
|
|
|
config_validator, "async_validate_config"
|
|
|
|
):
|
|
|
|
try:
|
2020-11-26 21:25:21 +00:00
|
|
|
result[domain] = (
|
2021-12-27 16:55:17 +00:00
|
|
|
await config_validator.async_validate_config(hass, config)
|
2020-11-26 21:25:21 +00:00
|
|
|
)[domain]
|
|
|
|
continue
|
2020-11-19 21:05:36 +00:00
|
|
|
except (vol.Invalid, HomeAssistantError) as ex:
|
|
|
|
_comp_error(ex, domain, config)
|
|
|
|
continue
|
2020-11-26 21:25:21 +00:00
|
|
|
except Exception as err: # pylint: disable=broad-except
|
|
|
|
logging.getLogger(__name__).exception(
|
|
|
|
"Unexpected error validating config"
|
|
|
|
)
|
|
|
|
result.add_error(
|
|
|
|
f"Unexpected error calling config validator: {err}",
|
|
|
|
domain,
|
|
|
|
config.get(domain),
|
|
|
|
)
|
2020-11-19 21:05:36 +00:00
|
|
|
continue
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
config_schema = getattr(component, "CONFIG_SCHEMA", None)
|
2019-07-21 16:59:02 +00:00
|
|
|
if config_schema is not None:
|
2019-07-10 18:56:50 +00:00
|
|
|
try:
|
2019-07-21 16:59:02 +00:00
|
|
|
config = config_schema(config)
|
2023-07-20 09:45:44 +00:00
|
|
|
# Don't fail if the validator removed the domain from the config
|
|
|
|
if domain in config:
|
|
|
|
result[domain] = config[domain]
|
2019-07-10 18:56:50 +00:00
|
|
|
except vol.Invalid as ex:
|
|
|
|
_comp_error(ex, domain, config)
|
|
|
|
continue
|
|
|
|
|
|
|
|
component_platform_schema = getattr(
|
2019-07-31 19:25:30 +00:00
|
|
|
component,
|
|
|
|
"PLATFORM_SCHEMA_BASE",
|
|
|
|
getattr(component, "PLATFORM_SCHEMA", None),
|
|
|
|
)
|
2019-07-10 18:56:50 +00:00
|
|
|
|
|
|
|
if component_platform_schema is None:
|
|
|
|
continue
|
|
|
|
|
|
|
|
platforms = []
|
|
|
|
for p_name, p_config in config_per_platform(config, domain):
|
|
|
|
# Validate component specific platform schema
|
|
|
|
try:
|
2019-07-21 16:59:02 +00:00
|
|
|
p_validated = component_platform_schema(p_config)
|
2019-07-10 18:56:50 +00:00
|
|
|
except vol.Invalid as ex:
|
|
|
|
_comp_error(ex, domain, config)
|
|
|
|
continue
|
|
|
|
|
|
|
|
# Not all platform components follow same pattern for platforms
|
|
|
|
# So if p_name is None we are not going to validate platform
|
|
|
|
# (the automation component is one of them)
|
|
|
|
if p_name is None:
|
|
|
|
platforms.append(p_validated)
|
|
|
|
continue
|
|
|
|
|
|
|
|
try:
|
2019-08-07 22:35:50 +00:00
|
|
|
p_integration = await async_get_integration_with_requirements(
|
|
|
|
hass, p_name
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2019-07-10 18:56:50 +00:00
|
|
|
platform = p_integration.get_platform(domain)
|
2021-09-18 05:25:50 +00:00
|
|
|
except loader.IntegrationNotFound as ex:
|
|
|
|
if not hass.config.safe_mode:
|
|
|
|
result.add_error(f"Platform error {domain}.{p_name} - {ex}")
|
|
|
|
continue
|
2019-08-07 22:35:50 +00:00
|
|
|
except (
|
|
|
|
RequirementsNotFound,
|
|
|
|
ImportError,
|
|
|
|
) as ex:
|
2019-08-23 16:53:33 +00:00
|
|
|
result.add_error(f"Platform error {domain}.{p_name} - {ex}")
|
2019-07-10 18:56:50 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
# Validate platform specific schema
|
2019-07-31 19:25:30 +00:00
|
|
|
platform_schema = getattr(platform, "PLATFORM_SCHEMA", None)
|
2019-07-21 16:59:02 +00:00
|
|
|
if platform_schema is not None:
|
2019-07-10 18:56:50 +00:00
|
|
|
try:
|
2019-07-21 16:59:02 +00:00
|
|
|
p_validated = platform_schema(p_validated)
|
2019-07-10 18:56:50 +00:00
|
|
|
except vol.Invalid as ex:
|
2019-08-23 16:53:33 +00:00
|
|
|
_comp_error(ex, f"{domain}.{p_name}", p_validated)
|
2019-07-10 18:56:50 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
platforms.append(p_validated)
|
|
|
|
|
|
|
|
# Remove config for current component and add validated config back in.
|
|
|
|
for filter_comp in extract_domain_configs(config, domain):
|
|
|
|
del config[filter_comp]
|
|
|
|
result[domain] = platforms
|
|
|
|
|
|
|
|
return result
|