Home Assistant Unofficial Reference 2024.12.1
config.py
Go to the documentation of this file.
1 """Module to help with parsing and generating configuration files."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections import OrderedDict
7 from collections.abc import Callable, Hashable, Iterable, Sequence
8 from contextlib import suppress
9 from dataclasses import dataclass
10 from enum import StrEnum
11 from functools import partial, reduce
12 import logging
13 import operator
14 import os
15 from pathlib import Path
16 import re
17 import shutil
18 from types import ModuleType
19 from typing import TYPE_CHECKING, Any
20 
21 from awesomeversion import AwesomeVersion
22 import voluptuous as vol
23 from voluptuous.humanize import MAX_VALIDATION_ERROR_ITEM_LENGTH
24 from yaml.error import MarkedYAMLError
25 
26 from .const import CONF_PACKAGES, CONF_PLATFORM, __version__
27 from .core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
28 from .core_config import _PACKAGE_DEFINITION_SCHEMA, _PACKAGES_CONFIG_SCHEMA
29 from .exceptions import ConfigValidationError, HomeAssistantError
30 from .helpers import config_validation as cv
31 from .helpers.translation import async_get_exception_message
32 from .helpers.typing import ConfigType
33 from .loader import ComponentProtocol, Integration, IntegrationNotFound
34 from .requirements import RequirementsNotFound, async_get_integration_with_requirements
35 from .util.async_ import create_eager_task
36 from .util.package import is_docker_env
37 from .util.yaml import SECRET_YAML, Secrets, YamlTypeError, load_yaml_dict
38 from .util.yaml.objects import NodeStrClass
39 
40 _LOGGER = logging.getLogger(__name__)
41 
42 RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml")
43 RE_ASCII = re.compile(r"\033\[[^m]*m")
44 YAML_CONFIG_FILE = "configuration.yaml"
45 VERSION_FILE = ".HA_VERSION"
46 CONFIG_DIR_NAME = ".homeassistant"
47 
48 AUTOMATION_CONFIG_PATH = "automations.yaml"
49 SCRIPT_CONFIG_PATH = "scripts.yaml"
50 SCENE_CONFIG_PATH = "scenes.yaml"
51 
52 LOAD_EXCEPTIONS = (ImportError, FileNotFoundError)
53 INTEGRATION_LOAD_EXCEPTIONS = (IntegrationNotFound, RequirementsNotFound)
54 
55 SAFE_MODE_FILENAME = "safe-mode"
56 
57 DEFAULT_CONFIG = f"""
58 # Loads default set of integrations. Do not remove.
59 default_config:
60 
61 # Load frontend themes from the themes folder
62 frontend:
63  themes: !include_dir_merge_named themes
64 
65 automation: !include {AUTOMATION_CONFIG_PATH}
66 script: !include {SCRIPT_CONFIG_PATH}
67 scene: !include {SCENE_CONFIG_PATH}
68 """
69 DEFAULT_SECRETS = """
70 # Use this file to store secrets like usernames and passwords.
71 # Learn more at https://www.home-assistant.io/docs/configuration/secrets/
72 some_password: welcome
73 """
74 TTS_PRE_92 = """
75 tts:
76  - platform: google
77 """
78 TTS_92 = """
79 tts:
80  - platform: google_translate
81  service_name: google_say
82 """
83 
84 
86  """Config error translation keys for config errors."""
87 
88  # translation keys with a generated config related message text
89  CONFIG_VALIDATION_ERR = "config_validation_err"
90  PLATFORM_CONFIG_VALIDATION_ERR = "platform_config_validation_err"
91 
92  # translation keys with a general static message text
93  COMPONENT_IMPORT_ERR = "component_import_err"
94  CONFIG_PLATFORM_IMPORT_ERR = "config_platform_import_err"
95  CONFIG_VALIDATOR_UNKNOWN_ERR = "config_validator_unknown_err"
96  CONFIG_SCHEMA_UNKNOWN_ERR = "config_schema_unknown_err"
97  PLATFORM_COMPONENT_LOAD_ERR = "platform_component_load_err"
98  PLATFORM_COMPONENT_LOAD_EXC = "platform_component_load_exc"
99  PLATFORM_SCHEMA_VALIDATOR_ERR = "platform_schema_validator_err"
100 
101  # translation key in case multiple errors occurred
102  MULTIPLE_INTEGRATION_CONFIG_ERRORS = "multiple_integration_config_errors"
103 
104 
105 _CONFIG_LOG_SHOW_STACK_TRACE: dict[ConfigErrorTranslationKey, bool] = {
106  ConfigErrorTranslationKey.COMPONENT_IMPORT_ERR: False,
107  ConfigErrorTranslationKey.CONFIG_PLATFORM_IMPORT_ERR: False,
108  ConfigErrorTranslationKey.CONFIG_VALIDATOR_UNKNOWN_ERR: True,
109  ConfigErrorTranslationKey.CONFIG_SCHEMA_UNKNOWN_ERR: True,
110  ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_ERR: False,
111  ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_EXC: True,
112  ConfigErrorTranslationKey.PLATFORM_SCHEMA_VALIDATOR_ERR: True,
113 }
114 
115 
116 @dataclass
118  """Configuration exception info class."""
119 
120  exception: Exception
121  translation_key: ConfigErrorTranslationKey
122  platform_path: str
123  config: ConfigType
124  integration_link: str | None
125 
126 
127 @dataclass
129  """Configuration for an integration and exception information."""
130 
131  config: ConfigType | None
132  exception_info_list: list[ConfigExceptionInfo]
133 
134 
136  """Put together the default configuration directory based on the OS."""
137  data_dir = os.path.expanduser("~")
138  return os.path.join(data_dir, CONFIG_DIR_NAME)
139 
140 
141 async def async_ensure_config_exists(hass: HomeAssistant) -> bool:
142  """Ensure a configuration file exists in given configuration directory.
143 
144  Creating a default one if needed.
145  Return boolean if configuration dir is ready to go.
146  """
147  config_path = hass.config.path(YAML_CONFIG_FILE)
148 
149  if os.path.isfile(config_path):
150  return True
151 
152  print( # noqa: T201
153  "Unable to find configuration. Creating default one in", hass.config.config_dir
154  )
155  return await async_create_default_config(hass)
156 
157 
158 async def async_create_default_config(hass: HomeAssistant) -> bool:
159  """Create a default configuration file in given configuration directory.
160 
161  Return if creation was successful.
162  """
163  return await hass.async_add_executor_job(
164  _write_default_config, hass.config.config_dir
165  )
166 
167 
168 def _write_default_config(config_dir: str) -> bool:
169  """Write the default config."""
170  config_path = os.path.join(config_dir, YAML_CONFIG_FILE)
171  secret_path = os.path.join(config_dir, SECRET_YAML)
172  version_path = os.path.join(config_dir, VERSION_FILE)
173  automation_yaml_path = os.path.join(config_dir, AUTOMATION_CONFIG_PATH)
174  script_yaml_path = os.path.join(config_dir, SCRIPT_CONFIG_PATH)
175  scene_yaml_path = os.path.join(config_dir, SCENE_CONFIG_PATH)
176 
177  # Writing files with YAML does not create the most human readable results
178  # So we're hard coding a YAML template.
179  try:
180  with open(config_path, "w", encoding="utf8") as config_file:
181  config_file.write(DEFAULT_CONFIG)
182 
183  if not os.path.isfile(secret_path):
184  with open(secret_path, "w", encoding="utf8") as secret_file:
185  secret_file.write(DEFAULT_SECRETS)
186 
187  with open(version_path, "w", encoding="utf8") as version_file:
188  version_file.write(__version__)
189 
190  if not os.path.isfile(automation_yaml_path):
191  with open(automation_yaml_path, "w", encoding="utf8") as automation_file:
192  automation_file.write("[]")
193 
194  if not os.path.isfile(script_yaml_path):
195  with open(script_yaml_path, "w", encoding="utf8"):
196  pass
197 
198  if not os.path.isfile(scene_yaml_path):
199  with open(scene_yaml_path, "w", encoding="utf8"):
200  pass
201  except OSError:
202  print( # noqa: T201
203  f"Unable to create default configuration file {config_path}"
204  )
205  return False
206  return True
207 
208 
209 async def async_hass_config_yaml(hass: HomeAssistant) -> dict:
210  """Load YAML from a Home Assistant configuration file.
211 
212  This function allows a component inside the asyncio loop to reload its
213  configuration by itself. Include package merge.
214  """
215  secrets = Secrets(Path(hass.config.config_dir))
216 
217  # Not using async_add_executor_job because this is an internal method.
218  try:
219  config = await hass.loop.run_in_executor(
220  None,
221  load_yaml_config_file,
222  hass.config.path(YAML_CONFIG_FILE),
223  secrets,
224  )
225  except HomeAssistantError as exc:
226  if not (base_exc := exc.__cause__) or not isinstance(base_exc, MarkedYAMLError):
227  raise
228 
229  # Rewrite path to offending YAML file to be relative the hass config dir
230  if base_exc.context_mark and base_exc.context_mark.name:
231  base_exc.context_mark.name = _relpath(hass, base_exc.context_mark.name)
232  if base_exc.problem_mark and base_exc.problem_mark.name:
233  base_exc.problem_mark.name = _relpath(hass, base_exc.problem_mark.name)
234  raise
235 
236  invalid_domains = []
237  for key in config:
238  try:
239  cv.domain_key(key)
240  except vol.Invalid as exc:
241  suffix = ""
242  if annotation := find_annotation(config, exc.path):
243  suffix = f" at {_relpath(hass, annotation[0])}, line {annotation[1]}"
244  _LOGGER.error("Invalid domain '%s'%s", key, suffix)
245  invalid_domains.append(key)
246  for invalid_domain in invalid_domains:
247  config.pop(invalid_domain)
248 
249  core_config = config.get(HOMEASSISTANT_DOMAIN, {})
250  try:
251  await merge_packages_config(hass, config, core_config.get(CONF_PACKAGES, {}))
252  except vol.Invalid as exc:
253  suffix = ""
254  if annotation := find_annotation(
255  config, [HOMEASSISTANT_DOMAIN, CONF_PACKAGES, *exc.path]
256  ):
257  suffix = f" at {_relpath(hass, annotation[0])}, line {annotation[1]}"
258  _LOGGER.error(
259  "Invalid package configuration '%s'%s: %s", CONF_PACKAGES, suffix, exc
260  )
261  core_config[CONF_PACKAGES] = {}
262 
263  return config
264 
265 
267  config_path: str, secrets: Secrets | None = None
268 ) -> dict[Any, Any]:
269  """Parse a YAML configuration file.
270 
271  Raises FileNotFoundError or HomeAssistantError.
272 
273  This method needs to run in an executor.
274  """
275  try:
276  conf_dict = load_yaml_dict(config_path, secrets)
277  except YamlTypeError as exc:
278  msg = (
279  f"The configuration file {os.path.basename(config_path)} "
280  "does not contain a dictionary"
281  )
282  _LOGGER.error(msg)
283  raise HomeAssistantError(msg) from exc
284 
285  # Convert values to dictionaries if they are None
286  for key, value in conf_dict.items():
287  conf_dict[key] = value or {}
288  return conf_dict
289 
290 
291 def process_ha_config_upgrade(hass: HomeAssistant) -> None:
292  """Upgrade configuration if necessary.
293 
294  This method needs to run in an executor.
295  """
296  version_path = hass.config.path(VERSION_FILE)
297 
298  try:
299  with open(version_path, encoding="utf8") as inp:
300  conf_version = inp.readline().strip()
301  except FileNotFoundError:
302  # Last version to not have this file
303  conf_version = "0.7.7"
304 
305  if conf_version == __version__:
306  return
307 
308  _LOGGER.info(
309  "Upgrading configuration directory from %s to %s", conf_version, __version__
310  )
311 
312  version_obj = AwesomeVersion(conf_version)
313 
314  if version_obj < AwesomeVersion("0.50"):
315  # 0.50 introduced persistent deps dir.
316  lib_path = hass.config.path("deps")
317  if os.path.isdir(lib_path):
318  shutil.rmtree(lib_path)
319 
320  if version_obj < AwesomeVersion("0.92"):
321  # 0.92 moved google/tts.py to google_translate/tts.py
322  config_path = hass.config.path(YAML_CONFIG_FILE)
323 
324  with open(config_path, encoding="utf-8") as config_file:
325  config_raw = config_file.read()
326 
327  if TTS_PRE_92 in config_raw:
328  _LOGGER.info("Migrating google tts to google_translate tts")
329  config_raw = config_raw.replace(TTS_PRE_92, TTS_92)
330  try:
331  with open(config_path, "w", encoding="utf-8") as config_file:
332  config_file.write(config_raw)
333  except OSError:
334  _LOGGER.exception("Migrating to google_translate tts failed")
335 
336  if version_obj < AwesomeVersion("0.94") and is_docker_env():
337  # In 0.94 we no longer install packages inside the deps folder when
338  # running inside a Docker container.
339  lib_path = hass.config.path("deps")
340  if os.path.isdir(lib_path):
341  shutil.rmtree(lib_path)
342 
343  with open(version_path, "w", encoding="utf8") as outp:
344  outp.write(__version__)
345 
346 
347 @callback
349  exc: vol.Invalid,
350  domain: str,
351  config: dict,
352  hass: HomeAssistant,
353  link: str | None = None,
354 ) -> None:
355  """Log a schema validation error."""
356  message = format_schema_error(hass, exc, domain, config, link)
357  _LOGGER.error(message)
358 
359 
360 @callback
362  exc: vol.Invalid | HomeAssistantError,
363  domain: str,
364  config: dict,
365  hass: HomeAssistant,
366  link: str | None = None,
367 ) -> None:
368  """Log an error from a custom config validator."""
369  if isinstance(exc, vol.Invalid):
370  async_log_schema_error(exc, domain, config, hass, link)
371  return
372 
373  message = format_homeassistant_error(hass, exc, domain, config, link)
374  _LOGGER.error(message, exc_info=exc)
375 
376 
377 def _get_annotation(item: Any) -> tuple[str, int | str] | None:
378  if not hasattr(item, "__config_file__"):
379  return None
380 
381  return (getattr(item, "__config_file__"), getattr(item, "__line__", "?"))
382 
383 
384 def _get_by_path(data: dict | list, items: list[Hashable]) -> Any:
385  """Access a nested object in root by item sequence.
386 
387  Returns None in case of error.
388  """
389  try:
390  return reduce(operator.getitem, items, data) # type: ignore[arg-type]
391  except (KeyError, IndexError, TypeError):
392  return None
393 
394 
396  config: dict | list, path: list[Hashable]
397 ) -> tuple[str, int | str] | None:
398  """Find file/line annotation for a node in config pointed to by path.
399 
400  If the node pointed to is a dict or list, prefer the annotation for the key in
401  the key/value pair defining the dict or list.
402  If the node is not annotated, try the parent node.
403  """
404 
405  def find_annotation_for_key(
406  item: dict, path: list[Hashable], tail: Hashable
407  ) -> tuple[str, int | str] | None:
408  for key in item:
409  if key == tail:
410  if annotation := _get_annotation(key):
411  return annotation
412  break
413  return None
414 
415  def find_annotation_rec(
416  config: dict | list, path: list[Hashable], tail: Hashable | None
417  ) -> tuple[str, int | str] | None:
418  item = _get_by_path(config, path)
419  if isinstance(item, dict) and tail is not None:
420  if tail_annotation := find_annotation_for_key(item, path, tail):
421  return tail_annotation
422 
423  if (
424  isinstance(item, (dict, list))
425  and path
426  and (
427  key_annotation := find_annotation_for_key(
428  _get_by_path(config, path[:-1]), path[:-1], path[-1]
429  )
430  )
431  ):
432  return key_annotation
433 
434  if annotation := _get_annotation(item):
435  return annotation
436 
437  if not path:
438  return None
439 
440  tail = path.pop()
441  if annotation := find_annotation_rec(config, path, tail):
442  return annotation
443  return _get_annotation(item)
444 
445  return find_annotation_rec(config, list(path), None)
446 
447 
448 def _relpath(hass: HomeAssistant, path: str) -> str:
449  """Return path relative to the Home Assistant config dir."""
450  return os.path.relpath(path, hass.config.config_dir)
451 
452 
454  hass: HomeAssistant,
455  exc: vol.Invalid,
456  domain: str,
457  config: dict,
458  link: str | None,
459  max_sub_error_length: int,
460 ) -> str:
461  """Stringify voluptuous.Invalid.
462 
463  This is an alternative to the custom __str__ implemented in
464  voluptuous.error.Invalid. The modifications are:
465  - Format the path delimited by -> instead of @data[]
466  - Prefix with domain, file and line of the error
467  - Suffix with a link to the documentation
468  - Give a more user friendly output for unknown options
469  - Give a more user friendly output for missing options
470  """
471  if "." in domain:
472  integration_domain, _, platform_domain = domain.partition(".")
473  message_prefix = (
474  f"Invalid config for '{platform_domain}' from integration "
475  f"'{integration_domain}'"
476  )
477  else:
478  message_prefix = f"Invalid config for '{domain}'"
479  if domain != HOMEASSISTANT_DOMAIN and link:
480  message_suffix = f", please check the docs at {link}"
481  else:
482  message_suffix = ""
483  if annotation := find_annotation(config, exc.path):
484  message_prefix += f" at {_relpath(hass, annotation[0])}, line {annotation[1]}"
485  path = "->".join(str(m) for m in exc.path)
486  if exc.error_message == "extra keys not allowed":
487  return (
488  f"{message_prefix}: '{exc.path[-1]}' is an invalid option for '{domain}', "
489  f"check: {path}{message_suffix}"
490  )
491  if exc.error_message == "required key not provided":
492  return (
493  f"{message_prefix}: required key '{exc.path[-1]}' not provided"
494  f"{message_suffix}"
495  )
496  # This function is an alternative to the stringification done by
497  # vol.Invalid.__str__, so we need to call Exception.__str__ here
498  # instead of str(exc)
499  output = Exception.__str__(exc)
500  if error_type := exc.error_type:
501  output += " for " + error_type
502  offending_item_summary = repr(_get_by_path(config, exc.path))
503  if len(offending_item_summary) > max_sub_error_length:
504  offending_item_summary = (
505  f"{offending_item_summary[: max_sub_error_length - 3]}..."
506  )
507  return (
508  f"{message_prefix}: {output} '{path}', got {offending_item_summary}"
509  f"{message_suffix}"
510  )
511 
512 
514  hass: HomeAssistant,
515  validation_error: vol.Invalid,
516  domain: str,
517  config: dict,
518  link: str | None,
519  max_sub_error_length: int = MAX_VALIDATION_ERROR_ITEM_LENGTH,
520 ) -> str:
521  """Provide a more helpful + complete validation error message.
522 
523  This is a modified version of voluptuous.error.Invalid.__str__,
524  the modifications make some minor changes to the formatting.
525  """
526  if isinstance(validation_error, vol.MultipleInvalid):
527  return "\n".join(
528  sorted(
530  hass, sub_error, domain, config, link, max_sub_error_length
531  )
532  for sub_error in validation_error.errors
533  )
534  )
535  return stringify_invalid(
536  hass, validation_error, domain, config, link, max_sub_error_length
537  )
538 
539 
540 @callback
542  hass: HomeAssistant,
543  exc: HomeAssistantError,
544  domain: str,
545  config: dict,
546  link: str | None = None,
547 ) -> str:
548  """Format HomeAssistantError thrown by a custom config validator."""
549  if "." in domain:
550  integration_domain, _, platform_domain = domain.partition(".")
551  message_prefix = (
552  f"Invalid config for '{platform_domain}' from integration "
553  f"'{integration_domain}'"
554  )
555  else:
556  message_prefix = f"Invalid config for '{domain}'"
557  # HomeAssistantError raised by custom config validator has no path to the
558  # offending configuration key, use the domain key as path instead.
559  if annotation := find_annotation(config, [domain]):
560  message_prefix += f" at {_relpath(hass, annotation[0])}, line {annotation[1]}"
561  message = f"{message_prefix}: {str(exc) or repr(exc)}"
562  if domain != HOMEASSISTANT_DOMAIN and link:
563  message += f", please check the docs at {link}"
564 
565  return message
566 
567 
568 @callback
570  hass: HomeAssistant,
571  exc: vol.Invalid,
572  domain: str,
573  config: dict,
574  link: str | None = None,
575 ) -> str:
576  """Format configuration validation error."""
577  return humanize_error(hass, exc, domain, config, link)
578 
579 
581  hass: HomeAssistant, package: str, component: str | None, config: dict, message: str
582 ) -> None:
583  """Log an error while merging packages."""
584  message_prefix = f"Setup of package '{package}'"
585  if annotation := find_annotation(
586  config, [HOMEASSISTANT_DOMAIN, CONF_PACKAGES, package]
587  ):
588  message_prefix += f" at {_relpath(hass, annotation[0])}, line {annotation[1]}"
589 
590  _LOGGER.error("%s failed: %s", message_prefix, message)
591 
592 
593 def _identify_config_schema(module: ComponentProtocol) -> str | None:
594  """Extract the schema and identify list or dict based."""
595  if not isinstance(module.CONFIG_SCHEMA, vol.Schema):
596  return None # type: ignore[unreachable]
597 
598  schema = module.CONFIG_SCHEMA.schema
599 
600  if isinstance(schema, vol.All):
601  for subschema in schema.validators:
602  if isinstance(subschema, dict):
603  schema = subschema
604  break
605  else:
606  return None
607 
608  try:
609  key = next(k for k in schema if k == module.DOMAIN)
610  except (TypeError, AttributeError, StopIteration):
611  return None
612  except Exception:
613  _LOGGER.exception("Unexpected error identifying config schema")
614  return None
615 
616  if hasattr(key, "default") and not isinstance(
617  key.default, vol.schema_builder.Undefined
618  ):
619  default_value = module.CONFIG_SCHEMA({module.DOMAIN: key.default()})[
620  module.DOMAIN
621  ]
622 
623  if isinstance(default_value, dict):
624  return "dict"
625 
626  if isinstance(default_value, list):
627  return "list"
628 
629  return None
630 
631  domain_schema = schema[key]
632 
633  t_schema = str(domain_schema)
634  if t_schema.startswith("{") or "schema_with_slug_keys" in t_schema:
635  return "dict"
636  if t_schema.startswith(("[", "All(<function ensure_list")):
637  return "list"
638  return None
639 
640 
641 def _validate_package_definition(name: str, conf: Any) -> None:
642  """Validate basic package definition properties."""
643  cv.slug(name)
645 
646 
647 def _recursive_merge(conf: dict[str, Any], package: dict[str, Any]) -> str | None:
648  """Merge package into conf, recursively."""
649  duplicate_key: str | None = None
650  for key, pack_conf in package.items():
651  if isinstance(pack_conf, dict):
652  if not pack_conf:
653  continue
654  conf[key] = conf.get(key, OrderedDict())
655  duplicate_key = _recursive_merge(conf=conf[key], package=pack_conf)
656 
657  elif isinstance(pack_conf, list):
658  conf[key] = cv.remove_falsy(
659  cv.ensure_list(conf.get(key)) + cv.ensure_list(pack_conf)
660  )
661 
662  else:
663  if conf.get(key) is not None:
664  return key
665  conf[key] = pack_conf
666  return duplicate_key
667 
668 
670  hass: HomeAssistant,
671  config: dict,
672  packages: dict[str, Any],
673  _log_pkg_error: Callable[
674  [HomeAssistant, str, str | None, dict, str], None
675  ] = _log_pkg_error,
676 ) -> dict:
677  """Merge packages into the top-level configuration.
678 
679  Ignores packages that cannot be setup. Mutates config. Raises
680  vol.Invalid if whole package config is invalid.
681  """
682 
683  _PACKAGES_CONFIG_SCHEMA(packages)
684 
685  invalid_packages = []
686  for pack_name, pack_conf in packages.items():
687  try:
688  _validate_package_definition(pack_name, pack_conf)
689  except vol.Invalid as exc:
691  hass,
692  pack_name,
693  None,
694  config,
695  f"Invalid package definition '{pack_name}': {exc!s}. Package "
696  f"will not be initialized",
697  )
698  invalid_packages.append(pack_name)
699  continue
700 
701  for comp_name, comp_conf in pack_conf.items():
702  if comp_name == HOMEASSISTANT_DOMAIN:
703  continue
704  try:
705  domain = cv.domain_key(comp_name)
706  except vol.Invalid:
708  hass, pack_name, comp_name, config, f"Invalid domain '{comp_name}'"
709  )
710  continue
711 
712  try:
713  integration = await async_get_integration_with_requirements(
714  hass, domain
715  )
716  component = await integration.async_get_component()
717  except LOAD_EXCEPTIONS as exc:
719  hass,
720  pack_name,
721  comp_name,
722  config,
723  f"Integration {comp_name} caused error: {exc!s}",
724  )
725  continue
726  except INTEGRATION_LOAD_EXCEPTIONS as exc:
727  _log_pkg_error(hass, pack_name, comp_name, config, str(exc))
728  continue
729 
730  try:
731  config_platform: (
732  ModuleType | None
733  ) = await integration.async_get_platform("config")
734  # Test if config platform has a config validator
735  if not hasattr(config_platform, "async_validate_config"):
736  config_platform = None
737  except ImportError:
738  config_platform = None
739 
740  merge_list = False
741 
742  # If integration has a custom config validator, it needs to provide a hint.
743  if config_platform is not None:
744  merge_list = config_platform.PACKAGE_MERGE_HINT == "list"
745 
746  if not merge_list:
747  merge_list = hasattr(component, "PLATFORM_SCHEMA")
748 
749  if not merge_list and hasattr(component, "CONFIG_SCHEMA"):
750  merge_list = _identify_config_schema(component) == "list"
751 
752  if merge_list:
753  config[comp_name] = cv.remove_falsy(
754  cv.ensure_list(config.get(comp_name)) + cv.ensure_list(comp_conf)
755  )
756  continue
757 
758  if comp_conf is None:
759  comp_conf = OrderedDict()
760 
761  if not isinstance(comp_conf, dict):
763  hass,
764  pack_name,
765  comp_name,
766  config,
767  f"integration '{comp_name}' cannot be merged, expected a dict",
768  )
769  continue
770 
771  if comp_name not in config or config[comp_name] is None:
772  config[comp_name] = OrderedDict()
773 
774  if not isinstance(config[comp_name], dict):
776  hass,
777  pack_name,
778  comp_name,
779  config,
780  (
781  f"integration '{comp_name}' cannot be merged, dict expected in "
782  "main config"
783  ),
784  )
785  continue
786 
787  duplicate_key = _recursive_merge(conf=config[comp_name], package=comp_conf)
788  if duplicate_key:
790  hass,
791  pack_name,
792  comp_name,
793  config,
794  f"integration '{comp_name}' has duplicate key '{duplicate_key}'",
795  )
796 
797  for pack_name in invalid_packages:
798  packages.pop(pack_name, {})
799 
800  return config
801 
802 
803 @callback
805  hass: HomeAssistant, domain: str, platform_exception: ConfigExceptionInfo
806 ) -> tuple[str | None, bool, dict[str, str]]:
807  """Get message to log and print stack trace preference."""
808  exception = platform_exception.exception
809  platform_path = platform_exception.platform_path
810  platform_config = platform_exception.config
811  link = platform_exception.integration_link
812 
813  placeholders: dict[str, str] = {
814  "domain": domain,
815  "error": str(exception),
816  "p_name": platform_path,
817  "config_file": "?",
818  "line": "?",
819  }
820 
821  show_stack_trace: bool | None = _CONFIG_LOG_SHOW_STACK_TRACE.get(
822  platform_exception.translation_key
823  )
824  if show_stack_trace is None:
825  # If no pre defined log_message is set, we generate an enriched error
826  # message, so we can notify about it during setup
827  show_stack_trace = False
828  if isinstance(exception, vol.Invalid):
829  log_message = format_schema_error(
830  hass, exception, platform_path, platform_config, link
831  )
832  if annotation := find_annotation(platform_config, exception.path):
833  placeholders["config_file"], line = annotation
834  placeholders["line"] = str(line)
835  else:
836  if TYPE_CHECKING:
837  assert isinstance(exception, HomeAssistantError)
838  log_message = format_homeassistant_error(
839  hass, exception, platform_path, platform_config, link
840  )
841  if annotation := find_annotation(platform_config, [platform_path]):
842  placeholders["config_file"], line = annotation
843  placeholders["line"] = str(line)
844  show_stack_trace = True
845  return (log_message, show_stack_trace, placeholders)
846 
847  # Generate the log message from the English translations
848  log_message = async_get_exception_message(
849  HOMEASSISTANT_DOMAIN,
850  platform_exception.translation_key,
851  translation_placeholders=placeholders,
852  )
853 
854  return (log_message, show_stack_trace, placeholders)
855 
856 
858  hass: HomeAssistant,
859  config: ConfigType,
860  integration: Integration,
861  raise_on_failure: bool = False,
862 ) -> ConfigType | None:
863  """Process and component configuration and handle errors.
864 
865  In case of errors:
866  - Print the error messages to the log.
867  - Raise a ConfigValidationError if raise_on_failure is set.
868 
869  Returns the integration config or `None`.
870  """
871  integration_config_info = await async_process_component_config(
872  hass, config, integration
873  )
875  hass, integration_config_info, integration, raise_on_failure
876  )
877  return async_drop_config_annotations(integration_config_info, integration)
878 
879 
880 @callback
882  integration_config_info: IntegrationConfigInfo,
883  integration: Integration,
884 ) -> ConfigType | None:
885  """Remove file and line annotations from str items in component configuration."""
886  if (config := integration_config_info.config) is None:
887  return None
888 
889  def drop_config_annotations_rec(node: Any) -> Any:
890  if isinstance(node, dict):
891  # Some integrations store metadata in custom dict classes, preserve those
892  tmp = dict(node)
893  node.clear()
894  node.update(
895  (drop_config_annotations_rec(k), drop_config_annotations_rec(v))
896  for k, v in tmp.items()
897  )
898  return node
899 
900  if isinstance(node, list):
901  return [drop_config_annotations_rec(v) for v in node]
902 
903  if isinstance(node, NodeStrClass):
904  return str(node)
905 
906  return node
907 
908  # Don't drop annotations from the homeassistant integration because it may
909  # have configuration for other integrations as packages.
910  if integration.domain in config and integration.domain != HOMEASSISTANT_DOMAIN:
911  drop_config_annotations_rec(config[integration.domain])
912  return config
913 
914 
915 @callback
917  hass: HomeAssistant,
918  integration_config_info: IntegrationConfigInfo,
919  integration: Integration,
920  raise_on_failure: bool = False,
921 ) -> None:
922  """Handle component configuration errors from async_process_component_config.
923 
924  In case of errors:
925  - Print the error messages to the log.
926  - Raise a ConfigValidationError if raise_on_failure is set.
927  """
928 
929  if not (config_exception_info := integration_config_info.exception_info_list):
930  return
931 
932  platform_exception: ConfigExceptionInfo
933  domain = integration.domain
934  placeholders: dict[str, str]
935  for platform_exception in config_exception_info:
936  exception = platform_exception.exception
937  (
938  log_message,
939  show_stack_trace,
940  placeholders,
941  ) = _get_log_message_and_stack_print_pref(hass, domain, platform_exception)
942  _LOGGER.error(
943  log_message,
944  exc_info=exception if show_stack_trace else None,
945  )
946 
947  if not raise_on_failure:
948  return
949 
950  if len(config_exception_info) == 1:
951  translation_key = platform_exception.translation_key
952  else:
953  translation_key = ConfigErrorTranslationKey.MULTIPLE_INTEGRATION_CONFIG_ERRORS
954  errors = str(len(config_exception_info))
955  placeholders = {
956  "domain": domain,
957  "errors": errors,
958  }
959  raise ConfigValidationError(
960  translation_key,
961  [platform_exception.exception for platform_exception in config_exception_info],
962  translation_domain=HOMEASSISTANT_DOMAIN,
963  translation_placeholders=placeholders,
964  )
965 
966 
968  config: ConfigType, domain: str
969 ) -> Iterable[tuple[str | None, ConfigType]]:
970  """Break a component config into different platforms.
971 
972  For example, will find 'switch', 'switch 2', 'switch 3', .. etc
973  Async friendly.
974  """
975  for config_key in extract_domain_configs(config, domain):
976  if not (platform_config := config[config_key]):
977  continue
978 
979  if not isinstance(platform_config, list):
980  platform_config = [platform_config]
981 
982  item: ConfigType
983  platform: str | None
984  for item in platform_config:
985  try:
986  platform = item.get(CONF_PLATFORM)
987  except AttributeError:
988  platform = None
989 
990  yield platform, item
991 
992 
994  config: ConfigType, domains: set[str]
995 ) -> dict[str, set[str]]:
996  """Find all the platforms in a configuration.
997 
998  Returns a dictionary with domain as key and a set of platforms as value.
999  """
1000  platform_integrations: dict[str, set[str]] = {}
1001  for key, domain_config in config.items():
1002  try:
1003  domain = cv.domain_key(key)
1004  except vol.Invalid:
1005  continue
1006  if domain not in domains:
1007  continue
1008 
1009  if not isinstance(domain_config, list):
1010  domain_config = [domain_config]
1011 
1012  for item in domain_config:
1013  try:
1014  platform = item.get(CONF_PLATFORM)
1015  except AttributeError:
1016  continue
1017  if platform and isinstance(platform, Hashable):
1018  platform_integrations.setdefault(domain, set()).add(platform)
1019  return platform_integrations
1020 
1021 
1022 def extract_domain_configs(config: ConfigType, domain: str) -> Sequence[str]:
1023  """Extract keys from config for given domain name.
1024 
1025  Async friendly.
1026  """
1027  domain_configs = []
1028  for key in config:
1029  with suppress(vol.Invalid):
1030  if cv.domain_key(key) != domain:
1031  continue
1032  domain_configs.append(key)
1033  return domain_configs
1034 
1035 
1036 @dataclass(slots=True)
1038  """Class to hold platform integration information."""
1039 
1040  path: str # integration.platform; ex: filter.sensor
1041  name: str # integration; ex: filter
1042  integration: Integration # <Integration filter>
1043  config: ConfigType # un-validated config
1044  validated_config: ConfigType # component validated config
1045 
1046 
1048  domain: str,
1049  integration_docs: str | None,
1050  config_exceptions: list[ConfigExceptionInfo],
1051  p_integration: _PlatformIntegration,
1052 ) -> ConfigType | None:
1053  """Load a platform integration and validate its config."""
1054  try:
1055  platform = await p_integration.integration.async_get_platform(domain)
1056  except LOAD_EXCEPTIONS as exc:
1057  exc_info = ConfigExceptionInfo(
1058  exc,
1059  ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_EXC,
1060  p_integration.path,
1061  p_integration.config,
1062  integration_docs,
1063  )
1064  config_exceptions.append(exc_info)
1065  return None
1066 
1067  # If the platform does not have a config schema
1068  # the top level component validated schema will be used
1069  if not hasattr(platform, "PLATFORM_SCHEMA"):
1070  return p_integration.validated_config
1071 
1072  # Validate platform specific schema
1073  try:
1074  return platform.PLATFORM_SCHEMA(p_integration.config) # type: ignore[no-any-return]
1075  except vol.Invalid as exc:
1076  exc_info = ConfigExceptionInfo(
1077  exc,
1078  ConfigErrorTranslationKey.PLATFORM_CONFIG_VALIDATION_ERR,
1079  p_integration.path,
1080  p_integration.config,
1081  p_integration.integration.documentation,
1082  )
1083  config_exceptions.append(exc_info)
1084  except Exception as exc: # noqa: BLE001
1085  exc_info = ConfigExceptionInfo(
1086  exc,
1087  ConfigErrorTranslationKey.PLATFORM_SCHEMA_VALIDATOR_ERR,
1088  p_integration.name,
1089  p_integration.config,
1090  p_integration.integration.documentation,
1091  )
1092  config_exceptions.append(exc_info)
1093 
1094  return None
1095 
1096 
1098  hass: HomeAssistant,
1099  config: ConfigType,
1100  integration: Integration,
1101  component: ComponentProtocol | None = None,
1102 ) -> IntegrationConfigInfo:
1103  """Check component configuration.
1104 
1105  Returns processed configuration and exception information.
1106 
1107  This method must be run in the event loop.
1108  """
1109  domain = integration.domain
1110  integration_docs = integration.documentation
1111  config_exceptions: list[ConfigExceptionInfo] = []
1112 
1113  if not component:
1114  try:
1115  component = await integration.async_get_component()
1116  except LOAD_EXCEPTIONS as exc:
1117  exc_info = ConfigExceptionInfo(
1118  exc,
1119  ConfigErrorTranslationKey.COMPONENT_IMPORT_ERR,
1120  domain,
1121  config,
1122  integration_docs,
1123  )
1124  config_exceptions.append(exc_info)
1125  return IntegrationConfigInfo(None, config_exceptions)
1126 
1127  # Check if the integration has a custom config validator
1128  config_validator = None
1129  # A successful call to async_get_component will prime
1130  # the cache for platforms_exists to ensure it does no
1131  # blocking I/O
1132  if integration.platforms_exists(("config",)):
1133  # If the config platform cannot possibly exist, don't try to load it.
1134  try:
1135  config_validator = await integration.async_get_platform("config")
1136  except ImportError as err:
1137  # Filter out import error of the config platform.
1138  # If the config platform contains bad imports, make sure
1139  # that still fails.
1140  if err.name != f"{integration.pkg_path}.config":
1141  exc_info = ConfigExceptionInfo(
1142  err,
1143  ConfigErrorTranslationKey.CONFIG_PLATFORM_IMPORT_ERR,
1144  domain,
1145  config,
1146  integration_docs,
1147  )
1148  config_exceptions.append(exc_info)
1149  return IntegrationConfigInfo(None, config_exceptions)
1150 
1151  if config_validator is not None and hasattr(
1152  config_validator, "async_validate_config"
1153  ):
1154  try:
1155  return IntegrationConfigInfo(
1156  await config_validator.async_validate_config(hass, config), []
1157  )
1158  except (vol.Invalid, HomeAssistantError) as exc:
1159  exc_info = ConfigExceptionInfo(
1160  exc,
1161  ConfigErrorTranslationKey.CONFIG_VALIDATION_ERR,
1162  domain,
1163  config,
1164  integration_docs,
1165  )
1166  config_exceptions.append(exc_info)
1167  return IntegrationConfigInfo(None, config_exceptions)
1168  except Exception as exc: # noqa: BLE001
1169  exc_info = ConfigExceptionInfo(
1170  exc,
1171  ConfigErrorTranslationKey.CONFIG_VALIDATOR_UNKNOWN_ERR,
1172  domain,
1173  config,
1174  integration_docs,
1175  )
1176  config_exceptions.append(exc_info)
1177  return IntegrationConfigInfo(None, config_exceptions)
1178 
1179  # No custom config validator, proceed with schema validation
1180  if hasattr(component, "CONFIG_SCHEMA"):
1181  try:
1182  return IntegrationConfigInfo(
1183  await cv.async_validate(hass, component.CONFIG_SCHEMA, config), []
1184  )
1185  except vol.Invalid as exc:
1186  exc_info = ConfigExceptionInfo(
1187  exc,
1188  ConfigErrorTranslationKey.CONFIG_VALIDATION_ERR,
1189  domain,
1190  config,
1191  integration_docs,
1192  )
1193  config_exceptions.append(exc_info)
1194  return IntegrationConfigInfo(None, config_exceptions)
1195  except Exception as exc: # noqa: BLE001
1196  exc_info = ConfigExceptionInfo(
1197  exc,
1198  ConfigErrorTranslationKey.CONFIG_SCHEMA_UNKNOWN_ERR,
1199  domain,
1200  config,
1201  integration_docs,
1202  )
1203  config_exceptions.append(exc_info)
1204  return IntegrationConfigInfo(None, config_exceptions)
1205 
1206  component_platform_schema = getattr(
1207  component, "PLATFORM_SCHEMA_BASE", getattr(component, "PLATFORM_SCHEMA", None)
1208  )
1209 
1210  if component_platform_schema is None:
1211  return IntegrationConfigInfo(config, [])
1212 
1213  platform_integrations_to_load: list[_PlatformIntegration] = []
1214  platforms: list[ConfigType] = []
1215  for p_name, p_config in config_per_platform(config, domain):
1216  # Validate component specific platform schema
1217  platform_path = f"{p_name}.{domain}"
1218  try:
1219  p_validated = await cv.async_validate(
1220  hass, component_platform_schema, p_config
1221  )
1222  except vol.Invalid as exc:
1223  exc_info = ConfigExceptionInfo(
1224  exc,
1225  ConfigErrorTranslationKey.PLATFORM_CONFIG_VALIDATION_ERR,
1226  domain,
1227  p_config,
1228  integration_docs,
1229  )
1230  config_exceptions.append(exc_info)
1231  continue
1232  except Exception as exc: # noqa: BLE001
1233  exc_info = ConfigExceptionInfo(
1234  exc,
1235  ConfigErrorTranslationKey.PLATFORM_SCHEMA_VALIDATOR_ERR,
1236  str(p_name),
1237  config,
1238  integration_docs,
1239  )
1240  config_exceptions.append(exc_info)
1241  continue
1242 
1243  # Not all platform components follow same pattern for platforms
1244  # So if p_name is None we are not going to validate platform
1245  # (the automation component is one of them)
1246  if p_name is None:
1247  platforms.append(p_validated)
1248  continue
1249 
1250  try:
1251  p_integration = await async_get_integration_with_requirements(hass, p_name)
1252  except (RequirementsNotFound, IntegrationNotFound) as exc:
1253  exc_info = ConfigExceptionInfo(
1254  exc,
1255  ConfigErrorTranslationKey.PLATFORM_COMPONENT_LOAD_ERR,
1256  platform_path,
1257  p_config,
1258  integration_docs,
1259  )
1260  config_exceptions.append(exc_info)
1261  continue
1262 
1263  platform_integration = _PlatformIntegration(
1264  platform_path, p_name, p_integration, p_config, p_validated
1265  )
1266  platform_integrations_to_load.append(platform_integration)
1267 
1268  #
1269  # Since bootstrap will order base platform (ie sensor) integrations
1270  # first, we eagerly gather importing the platforms that need to be
1271  # validated for the base platform since everything that uses the
1272  # base platform has to wait for it to finish.
1273  #
1274  # For example if `hue` where to load first and than called
1275  # `async_forward_entry_setup` for the `sensor` platform it would have to
1276  # wait for the sensor platform to finish loading before it could continue.
1277  # Since the base `sensor` platform must also import all of its platform
1278  # integrations to do validation before it can finish setup, its important
1279  # that the platform integrations are imported first so we do not waste
1280  # time importing `hue` first when we could have been importing the platforms
1281  # that the base `sensor` platform need to load to do validation and allow
1282  # all integrations that need the base `sensor` platform to proceed with setup.
1283  #
1284  if platform_integrations_to_load:
1285  async_load_and_validate = partial(
1286  _async_load_and_validate_platform_integration,
1287  domain,
1288  integration_docs,
1289  config_exceptions,
1290  )
1291  platforms.extend(
1292  validated_config
1293  for validated_config in await asyncio.gather(
1294  *(
1295  create_eager_task(
1296  async_load_and_validate(p_integration), loop=hass.loop
1297  )
1298  for p_integration in platform_integrations_to_load
1299  )
1300  )
1301  if validated_config is not None
1302  )
1303 
1304  # Create a copy of the configuration with all config for current
1305  # component removed and add validated config back in.
1306  config = config_without_domain(config, domain)
1307  config[domain] = platforms
1308 
1309  return IntegrationConfigInfo(config, config_exceptions)
1310 
1311 
1312 @callback
1313 def config_without_domain(config: ConfigType, domain: str) -> ConfigType:
1314  """Return a config with all configuration for a domain removed."""
1315  filter_keys = extract_domain_configs(config, domain)
1316  return {key: value for key, value in config.items() if key not in filter_keys}
1317 
1318 
1319 async def async_check_ha_config_file(hass: HomeAssistant) -> str | None:
1320  """Check if Home Assistant configuration file is valid.
1321 
1322  This method is a coroutine.
1323  """
1324  # pylint: disable-next=import-outside-toplevel
1325  from .helpers import check_config
1326 
1327  res = await check_config.async_check_ha_config_file(hass)
1328 
1329  if not res.errors:
1330  return None
1331  return res.error_str
1332 
1333 
1334 def safe_mode_enabled(config_dir: str) -> bool:
1335  """Return if safe mode is enabled.
1336 
1337  If safe mode is enabled, the safe mode file will be removed.
1338  """
1339  safe_mode_path = os.path.join(config_dir, SAFE_MODE_FILENAME)
1340  safe_mode = os.path.exists(safe_mode_path)
1341  if safe_mode:
1342  os.remove(safe_mode_path)
1343  return safe_mode
1344 
1345 
1346 async def async_enable_safe_mode(hass: HomeAssistant) -> None:
1347  """Enable safe mode."""
1348 
1349  def _enable_safe_mode() -> None:
1350  Path(hass.config.path(SAFE_MODE_FILENAME)).touch()
1351 
1352  await hass.async_add_executor_job(_enable_safe_mode)
bool add(self, _T matcher)
Definition: match.py:185
None open(self, **Any kwargs)
Definition: lock.py:86
Sequence[str] extract_domain_configs(ConfigType config, str domain)
Definition: config.py:1022
bool async_ensure_config_exists(HomeAssistant hass)
Definition: config.py:141
bool _write_default_config(str config_dir)
Definition: config.py:168
tuple[str, int|str]|None _get_annotation(Any item)
Definition: config.py:377
str|None _identify_config_schema(ComponentProtocol module)
Definition: config.py:593
Iterable[tuple[str|None, ConfigType]] config_per_platform(ConfigType config, str domain)
Definition: config.py:969
ConfigType|None _async_load_and_validate_platform_integration(str domain, str|None integration_docs, list[ConfigExceptionInfo] config_exceptions, _PlatformIntegration p_integration)
Definition: config.py:1052
tuple[str|None, bool, dict[str, str]] _get_log_message_and_stack_print_pref(HomeAssistant hass, str domain, ConfigExceptionInfo platform_exception)
Definition: config.py:806
ConfigType|None async_drop_config_annotations(IntegrationConfigInfo integration_config_info, Integration integration)
Definition: config.py:884
ConfigType|None async_process_component_and_handle_errors(HomeAssistant hass, ConfigType config, Integration integration, bool raise_on_failure=False)
Definition: config.py:862
Any _get_by_path(dict|list data, list[Hashable] items)
Definition: config.py:384
None async_log_schema_error(vol.Invalid exc, str domain, dict config, HomeAssistant hass, str|None link=None)
Definition: config.py:354
dict[Any, Any] load_yaml_config_file(str config_path, Secrets|None secrets=None)
Definition: config.py:268
str get_default_config_dir()
Definition: config.py:135
tuple[str, int|str]|None find_annotation(dict|list config, list[Hashable] path)
Definition: config.py:397
str|None _recursive_merge(dict[str, Any] conf, dict[str, Any] package)
Definition: config.py:647
dict async_hass_config_yaml(HomeAssistant hass)
Definition: config.py:209
str format_homeassistant_error(HomeAssistant hass, HomeAssistantError exc, str domain, dict config, str|None link=None)
Definition: config.py:547
None _validate_package_definition(str name, Any conf)
Definition: config.py:641
str|None async_check_ha_config_file(HomeAssistant hass)
Definition: config.py:1319
IntegrationConfigInfo async_process_component_config(HomeAssistant hass, ConfigType config, Integration integration, ComponentProtocol|None component=None)
Definition: config.py:1102
None _log_pkg_error(HomeAssistant hass, str package, str|None component, dict config, str message)
Definition: config.py:582
ConfigType config_without_domain(ConfigType config, str domain)
Definition: config.py:1313
bool safe_mode_enabled(str config_dir)
Definition: config.py:1334
str _relpath(HomeAssistant hass, str path)
Definition: config.py:448
None async_enable_safe_mode(HomeAssistant hass)
Definition: config.py:1346
dict[str, set[str]] extract_platform_integrations(ConfigType config, set[str] domains)
Definition: config.py:995
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
None async_log_config_validator_error(vol.Invalid|HomeAssistantError exc, str domain, dict config, HomeAssistant hass, str|None link=None)
Definition: config.py:367
str humanize_error(HomeAssistant hass, vol.Invalid validation_error, str domain, dict config, str|None link, int max_sub_error_length=MAX_VALIDATION_ERROR_ITEM_LENGTH)
Definition: config.py:520
str format_schema_error(HomeAssistant hass, vol.Invalid exc, str domain, dict config, str|None link=None)
Definition: config.py:575
None async_handle_component_errors(HomeAssistant hass, IntegrationConfigInfo integration_config_info, Integration integration, bool raise_on_failure=False)
Definition: config.py:921
str stringify_invalid(HomeAssistant hass, vol.Invalid exc, str domain, dict config, str|None link, int max_sub_error_length)
Definition: config.py:460
bool async_create_default_config(HomeAssistant hass)
Definition: config.py:158
None process_ha_config_upgrade(HomeAssistant hass)
Definition: config.py:291
str async_get_exception_message(str translation_domain, str translation_key, dict[str, str]|None translation_placeholders=None)
Definition: translation.py:438
Integration async_get_integration_with_requirements(HomeAssistant hass, str domain)
Definition: requirements.py:46
dict load_yaml_dict(str|os.PathLike[str] fname, Secrets|None secrets=None)
Definition: loader.py:180