Home Assistant Unofficial Reference 2024.12.1
check_config.py
Go to the documentation of this file.
1 """Helper to check the configuration file."""
2 
3 from __future__ import annotations
4 
5 from collections import OrderedDict
6 import logging
7 import os
8 from pathlib import Path
9 from typing import NamedTuple, Self
10 
11 import voluptuous as vol
12 
13 from homeassistant import loader
14 from homeassistant.config import ( # type: ignore[attr-defined]
15  CONF_PACKAGES,
16  YAML_CONFIG_FILE,
17  config_per_platform,
18  extract_domain_configs,
19  format_homeassistant_error,
20  format_schema_error,
21  load_yaml_config_file,
22  merge_packages_config,
23 )
24 from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
25 from homeassistant.core_config import CORE_CONFIG_SCHEMA
26 from homeassistant.exceptions import HomeAssistantError
27 from homeassistant.requirements import (
28  RequirementsNotFound,
29  async_clear_install_history,
30  async_get_integration_with_requirements,
31 )
32 import homeassistant.util.yaml.loader as yaml_loader
33 
34 from . import config_validation as cv
35 from .typing import ConfigType
36 
37 
38 class CheckConfigError(NamedTuple):
39  """Configuration check error."""
40 
41  message: str
42  domain: str | None
43  config: ConfigType | None
44 
45 
46 class HomeAssistantConfig(OrderedDict):
47  """Configuration result with errors attribute."""
48 
49  def __init__(self) -> None:
50  """Initialize HA config."""
51  super().__init__()
52  self.errors: list[CheckConfigError] = []
53  self.warnings: list[CheckConfigError] = []
54 
55  def add_error(
56  self,
57  message: str,
58  domain: str | None = None,
59  config: ConfigType | None = None,
60  ) -> Self:
61  """Add an error."""
62  self.errors.append(CheckConfigError(str(message), domain, config))
63  return self
64 
65  @property
66  def error_str(self) -> str:
67  """Concatenate all errors to a string."""
68  return "\n".join([err.message for err in self.errors])
69 
71  self,
72  message: str,
73  domain: str | None = None,
74  config: ConfigType | None = None,
75  ) -> Self:
76  """Add a warning."""
77  self.warnings.append(CheckConfigError(str(message), domain, config))
78  return self
79 
80  @property
81  def warning_str(self) -> str:
82  """Concatenate all warnings to a string."""
83  return "\n".join([err.message for err in self.warnings])
84 
85 
86 async def async_check_ha_config_file( # noqa: C901
87  hass: HomeAssistant,
88 ) -> HomeAssistantConfig:
89  """Load and check if Home Assistant configuration file is valid.
90 
91  This method is a coroutine.
92  """
93  result = HomeAssistantConfig()
95 
96  def _pack_error(
97  hass: HomeAssistant,
98  package: str,
99  component: str | None,
100  config: ConfigType,
101  message: str,
102  ) -> None:
103  """Handle errors from packages."""
104  message = f"Setup of package '{package}' failed: {message}"
105  domain = f"homeassistant.packages.{package}{'.' + component if component is not None else ''}"
106  pack_config = core_config[CONF_PACKAGES].get(package, config)
107  result.add_warning(message, domain, pack_config)
108 
109  def _comp_error(
110  ex: vol.Invalid | HomeAssistantError,
111  domain: str,
112  component_config: ConfigType,
113  config_to_attach: ConfigType,
114  ) -> None:
115  """Handle errors from components."""
116  if isinstance(ex, vol.Invalid):
117  message = format_schema_error(hass, ex, domain, component_config)
118  else:
119  message = format_homeassistant_error(hass, ex, domain, component_config)
120  if domain in frontend_dependencies:
121  result.add_error(message, domain, config_to_attach)
122  else:
123  result.add_warning(message, domain, config_to_attach)
124 
125  async def _get_integration(
126  hass: HomeAssistant, domain: str
127  ) -> loader.Integration | None:
128  """Get an integration."""
129  integration: loader.Integration | None = None
130  try:
131  integration = await async_get_integration_with_requirements(hass, domain)
132  except loader.IntegrationNotFound as ex:
133  # We get this error if an integration is not found. In recovery mode and
134  # safe mode, this currently happens for all custom integrations. Don't
135  # show errors for a missing integration in recovery mode or safe mode to
136  # not confuse the user.
137  if not hass.config.recovery_mode and not hass.config.safe_mode:
138  result.add_warning(f"Integration error: {domain} - {ex}")
139  except RequirementsNotFound as ex:
140  result.add_warning(f"Integration error: {domain} - {ex}")
141  return integration
142 
143  # Load configuration.yaml
144  config_path = hass.config.path(YAML_CONFIG_FILE)
145  try:
146  if not await hass.async_add_executor_job(os.path.isfile, config_path):
147  return result.add_error("File configuration.yaml not found.")
148 
149  config = await hass.async_add_executor_job(
150  load_yaml_config_file,
151  config_path,
152  yaml_loader.Secrets(Path(hass.config.config_dir)),
153  )
154  except FileNotFoundError:
155  return result.add_error(f"File not found: {config_path}")
156  except HomeAssistantError as err:
157  return result.add_error(f"Error loading {config_path}: {err}")
158 
159  # Extract and validate core [homeassistant] config
160  core_config = config.pop(HOMEASSISTANT_DOMAIN, {})
161  try:
162  core_config = CORE_CONFIG_SCHEMA(core_config)
163  result[HOMEASSISTANT_DOMAIN] = core_config
164 
165  # Merge packages
166  await merge_packages_config(
167  hass, config, core_config.get(CONF_PACKAGES, {}), _pack_error
168  )
169  except vol.Invalid as err:
170  result.add_error(
171  format_schema_error(hass, err, HOMEASSISTANT_DOMAIN, core_config),
172  HOMEASSISTANT_DOMAIN,
173  core_config,
174  )
175  core_config = {}
176  core_config.pop(CONF_PACKAGES, None)
177 
178  # Filter out repeating config sections
179  components = {cv.domain_key(key) for key in config}
180 
181  frontend_dependencies: set[str] = set()
182  if "frontend" in components or "default_config" in components:
183  frontend = await _get_integration(hass, "frontend")
184  if frontend:
185  await frontend.resolve_dependencies()
186  frontend_dependencies = frontend.all_dependencies | {"frontend"}
187 
188  # Process and validate config
189  for domain in components:
190  if not (integration := await _get_integration(hass, domain)):
191  continue
192 
193  try:
194  component = await integration.async_get_component()
195  except ImportError as ex:
196  result.add_warning(f"Component error: {domain} - {ex}")
197  continue
198 
199  # Check if the integration has a custom config validator
200  config_validator = None
201  if integration.platforms_exists(("config",)):
202  try:
203  config_validator = await integration.async_get_platform("config")
204  except ImportError as err:
205  # Filter out import error of the config platform.
206  # If the config platform contains bad imports, make sure
207  # that still fails.
208  if err.name != f"{integration.pkg_path}.config":
209  result.add_error(f"Error importing config platform {domain}: {err}")
210  continue
211 
212  if config_validator is not None and hasattr(
213  config_validator, "async_validate_config"
214  ):
215  try:
216  result[domain] = (
217  await config_validator.async_validate_config(hass, config)
218  )[domain]
219  continue
220  except (vol.Invalid, HomeAssistantError) as ex:
221  _comp_error(ex, domain, config, config[domain])
222  continue
223  except Exception as err: # noqa: BLE001
224  logging.getLogger(__name__).exception(
225  "Unexpected error validating config"
226  )
227  result.add_error(
228  f"Unexpected error calling config validator: {err}",
229  domain,
230  config.get(domain),
231  )
232  continue
233 
234  config_schema = getattr(component, "CONFIG_SCHEMA", None)
235  if config_schema is not None:
236  try:
237  validated_config = await cv.async_validate(hass, config_schema, config)
238  # Don't fail if the validator removed the domain from the config
239  if domain in validated_config:
240  result[domain] = validated_config[domain]
241  except vol.Invalid as ex:
242  _comp_error(ex, domain, config, config[domain])
243  continue
244 
245  component_platform_schema = getattr(
246  component,
247  "PLATFORM_SCHEMA_BASE",
248  getattr(component, "PLATFORM_SCHEMA", None),
249  )
250 
251  if component_platform_schema is None:
252  continue
253 
254  platforms = []
255  for p_name, p_config in config_per_platform(config, domain):
256  # Validate component specific platform schema
257  try:
258  p_validated = await cv.async_validate(
259  hass, component_platform_schema, p_config
260  )
261  except vol.Invalid as ex:
262  _comp_error(ex, domain, p_config, p_config)
263  continue
264 
265  # Not all platform components follow same pattern for platforms
266  # So if p_name is None we are not going to validate platform
267  # (the automation component is one of them)
268  if p_name is None:
269  platforms.append(p_validated)
270  continue
271 
272  try:
273  p_integration = await async_get_integration_with_requirements(
274  hass, p_name
275  )
276  platform = await p_integration.async_get_platform(domain)
277  except loader.IntegrationNotFound as ex:
278  # We get this error if an integration is not found. In recovery mode and
279  # safe mode, this currently happens for all custom integrations. Don't
280  # show errors for a missing integration in recovery mode or safe mode to
281  # not confuse the user.
282  if not hass.config.recovery_mode and not hass.config.safe_mode:
283  result.add_warning(
284  f"Platform error '{domain}' from integration '{p_name}' - {ex}"
285  )
286  continue
287  except (
288  RequirementsNotFound,
289  ImportError,
290  ) as ex:
291  result.add_warning(
292  f"Platform error '{domain}' from integration '{p_name}' - {ex}"
293  )
294  continue
295 
296  # Validate platform specific schema
297  platform_schema = getattr(platform, "PLATFORM_SCHEMA", None)
298  if platform_schema is not None:
299  try:
300  p_validated = platform_schema(p_validated)
301  except vol.Invalid as ex:
302  _comp_error(ex, f"{domain}.{p_name}", p_config, p_config)
303  continue
304 
305  platforms.append(p_validated)
306 
307  # Remove config for current component and add validated config back in.
308  for filter_comp in extract_domain_configs(config, domain):
309  del config[filter_comp]
310  result[domain] = platforms
311 
312  return result
Self add_error(self, str message, str|None domain=None, ConfigType|None config=None)
Definition: check_config.py:60
Self add_warning(self, str message, str|None domain=None, ConfigType|None config=None)
Definition: check_config.py:75
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
Sequence[str] extract_domain_configs(ConfigType config, str domain)
Definition: config.py:1022
Iterable[tuple[str|None, ConfigType]] config_per_platform(ConfigType config, str domain)
Definition: config.py:969
str format_homeassistant_error(HomeAssistant hass, HomeAssistantError exc, str domain, dict config, str|None link=None)
Definition: config.py:547
dict merge_packages_config(HomeAssistant hass, dict config, dict[str, Any] packages, Callable[[HomeAssistant, str, str|None, dict, str], None] _log_pkg_error=_log_pkg_error)
Definition: config.py:676
str format_schema_error(HomeAssistant hass, vol.Invalid exc, str domain, dict config, str|None link=None)
Definition: config.py:575
HomeAssistantConfig async_check_ha_config_file(HomeAssistant hass)
Definition: check_config.py:88
None async_clear_install_history(HomeAssistant hass)
Definition: requirements.py:83
Integration async_get_integration_with_requirements(HomeAssistant hass, str domain)
Definition: requirements.py:46