Home Assistant Unofficial Reference 2024.12.1
core_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 from collections import OrderedDict
6 from collections.abc import Sequence
7 from contextlib import suppress
8 import enum
9 import logging
10 import os
11 import pathlib
12 from typing import TYPE_CHECKING, Any, Final
13 from urllib.parse import urlparse
14 
15 import voluptuous as vol
16 from webrtc_models import RTCConfiguration, RTCIceServer
17 import yarl
18 
19 from . import auth
20 from .auth import mfa_modules as auth_mfa_modules, providers as auth_providers
21 from .const import (
22  ATTR_ASSUMED_STATE,
23  ATTR_FRIENDLY_NAME,
24  ATTR_HIDDEN,
25  BASE_PLATFORMS,
26  CONF_ALLOWLIST_EXTERNAL_DIRS,
27  CONF_ALLOWLIST_EXTERNAL_URLS,
28  CONF_AUTH_MFA_MODULES,
29  CONF_AUTH_PROVIDERS,
30  CONF_COUNTRY,
31  CONF_CURRENCY,
32  CONF_CUSTOMIZE,
33  CONF_CUSTOMIZE_DOMAIN,
34  CONF_CUSTOMIZE_GLOB,
35  CONF_DEBUG,
36  CONF_ELEVATION,
37  CONF_EXTERNAL_URL,
38  CONF_ID,
39  CONF_INTERNAL_URL,
40  CONF_LANGUAGE,
41  CONF_LATITUDE,
42  CONF_LEGACY_TEMPLATES,
43  CONF_LONGITUDE,
44  CONF_MEDIA_DIRS,
45  CONF_NAME,
46  CONF_PACKAGES,
47  CONF_RADIUS,
48  CONF_TEMPERATURE_UNIT,
49  CONF_TIME_ZONE,
50  CONF_TYPE,
51  CONF_UNIT_SYSTEM,
52  CONF_URL,
53  CONF_USERNAME,
54  EVENT_CORE_CONFIG_UPDATE,
55  LEGACY_CONF_WHITELIST_EXTERNAL_DIRS,
56  UnitOfLength,
57  __version__,
58 )
59 from .core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
60 from .generated.currencies import HISTORIC_CURRENCIES
61 from .helpers import config_validation as cv, issue_registry as ir
62 from .helpers.entity_values import EntityValues
63 from .helpers.frame import ReportBehavior, report_usage
64 from .helpers.storage import Store
65 from .helpers.typing import UNDEFINED, UndefinedType
66 from .util import dt as dt_util, location
67 from .util.hass_dict import HassKey
68 from .util.package import is_docker_env
69 from .util.unit_system import (
70  _CONF_UNIT_SYSTEM_IMPERIAL,
71  _CONF_UNIT_SYSTEM_US_CUSTOMARY,
72  METRIC_SYSTEM,
73  UnitSystem,
74  get_unit_system,
75  validate_unit_system,
76 )
77 
78 # Typing imports that create a circular dependency
79 if TYPE_CHECKING:
80  from .components.http import ApiConfig
81 
82 _LOGGER = logging.getLogger(__name__)
83 
84 DATA_CUSTOMIZE: HassKey[EntityValues] = HassKey("hass_customize")
85 
86 CONF_CREDENTIAL: Final = "credential"
87 CONF_ICE_SERVERS: Final = "ice_servers"
88 CONF_WEBRTC: Final = "webrtc"
89 
90 CORE_STORAGE_KEY = "core.config"
91 CORE_STORAGE_VERSION = 1
92 CORE_STORAGE_MINOR_VERSION = 4
93 
94 
95 class ConfigSource(enum.StrEnum):
96  """Source of core configuration."""
97 
98  DEFAULT = "default"
99  DISCOVERED = "discovered"
100  STORAGE = "storage"
101  YAML = "yaml"
102 
103 
105  configs: Sequence[dict[str, Any]],
106 ) -> Sequence[dict[str, Any]]:
107  """No duplicate auth provider config allowed in a list.
108 
109  Each type of auth provider can only have one config without optional id.
110  Unique id is required if same type of auth provider used multiple times.
111  """
112  config_keys: set[tuple[str, str | None]] = set()
113  for config in configs:
114  key = (config[CONF_TYPE], config.get(CONF_ID))
115  if key in config_keys:
116  raise vol.Invalid(
117  f"Duplicate auth provider {config[CONF_TYPE]} found. "
118  "Please add unique IDs "
119  "if you want to have the same auth provider twice"
120  )
121  config_keys.add(key)
122  return configs
123 
124 
126  configs: Sequence[dict[str, Any]],
127 ) -> Sequence[dict[str, Any]]:
128  """No duplicate auth mfa module item allowed in a list.
129 
130  Each type of mfa module can only have one config without optional id.
131  A global unique id is required if same type of mfa module used multiple
132  times.
133  Note: this is different than auth provider
134  """
135  config_keys: set[str] = set()
136  for config in configs:
137  key = config.get(CONF_ID, config[CONF_TYPE])
138  if key in config_keys:
139  raise vol.Invalid(
140  f"Duplicate mfa module {config[CONF_TYPE]} found. "
141  "Please add unique IDs "
142  "if you want to have the same mfa module twice"
143  )
144  config_keys.add(key)
145  return configs
146 
147 
148 def _filter_bad_internal_external_urls(conf: dict) -> dict:
149  """Filter internal/external URL with a path."""
150  for key in CONF_INTERNAL_URL, CONF_EXTERNAL_URL:
151  if key in conf and urlparse(conf[key]).path not in ("", "/"):
152  # We warn but do not fix, because if this was incorrectly configured,
153  # adjusting this value might impact security.
154  _LOGGER.warning(
155  "Invalid %s set. It's not allowed to have a path (/bla)", key
156  )
157 
158  return conf
159 
160 
161 # Schema for all packages element
162 _PACKAGES_CONFIG_SCHEMA = vol.Schema({cv.string: vol.Any(dict, list)})
163 
164 # Schema for individual package definition
165 _PACKAGE_DEFINITION_SCHEMA = vol.Schema({cv.string: vol.Any(dict, list, None)})
166 
167 _CUSTOMIZE_DICT_SCHEMA = vol.Schema(
168  {
169  vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
170  vol.Optional(ATTR_HIDDEN): cv.boolean,
171  vol.Optional(ATTR_ASSUMED_STATE): cv.boolean,
172  },
173  extra=vol.ALLOW_EXTRA,
174 )
175 
176 _CUSTOMIZE_CONFIG_SCHEMA = vol.Schema(
177  {
178  vol.Optional(CONF_CUSTOMIZE, default={}): vol.Schema(
179  {cv.entity_id: _CUSTOMIZE_DICT_SCHEMA}
180  ),
181  vol.Optional(CONF_CUSTOMIZE_DOMAIN, default={}): vol.Schema(
182  {cv.string: _CUSTOMIZE_DICT_SCHEMA}
183  ),
184  vol.Optional(CONF_CUSTOMIZE_GLOB, default={}): vol.Schema(
185  {cv.string: _CUSTOMIZE_DICT_SCHEMA}
186  ),
187  }
188 )
189 
190 
191 def _raise_issue_if_historic_currency(hass: HomeAssistant, currency: str) -> None:
192  if currency not in HISTORIC_CURRENCIES:
193  ir.async_delete_issue(hass, HOMEASSISTANT_DOMAIN, "historic_currency")
194  return
195 
196  ir.async_create_issue(
197  hass,
198  HOMEASSISTANT_DOMAIN,
199  "historic_currency",
200  is_fixable=False,
201  learn_more_url="homeassistant://config/general",
202  severity=ir.IssueSeverity.WARNING,
203  translation_key="historic_currency",
204  translation_placeholders={"currency": currency},
205  )
206 
207 
208 def _raise_issue_if_no_country(hass: HomeAssistant, country: str | None) -> None:
209  if country is not None:
210  ir.async_delete_issue(hass, HOMEASSISTANT_DOMAIN, "country_not_configured")
211  return
212 
213  ir.async_create_issue(
214  hass,
215  HOMEASSISTANT_DOMAIN,
216  "country_not_configured",
217  is_fixable=False,
218  learn_more_url="homeassistant://config/general",
219  severity=ir.IssueSeverity.WARNING,
220  translation_key="country_not_configured",
221  )
222 
223 
224 def _validate_currency(data: Any) -> Any:
225  try:
226  return cv.currency(data)
227  except vol.InInvalid:
228  with suppress(vol.InInvalid):
229  return cv.historic_currency(data)
230  raise
231 
232 
233 def _validate_stun_or_turn_url(value: Any) -> str:
234  """Validate an URL."""
235  url_in = str(value)
236  url = urlparse(url_in)
237 
238  if url.scheme not in ("stun", "stuns", "turn", "turns"):
239  raise vol.Invalid("invalid url")
240  return url_in
241 
242 
243 CORE_CONFIG_SCHEMA = vol.All(
244  _CUSTOMIZE_CONFIG_SCHEMA.extend(
245  {
246  CONF_NAME: vol.Coerce(str),
247  CONF_LATITUDE: cv.latitude,
248  CONF_LONGITUDE: cv.longitude,
249  CONF_ELEVATION: vol.Coerce(int),
250  CONF_RADIUS: cv.positive_int,
251  vol.Remove(CONF_TEMPERATURE_UNIT): cv.temperature_unit,
252  CONF_UNIT_SYSTEM: validate_unit_system,
253  CONF_TIME_ZONE: cv.time_zone,
254  vol.Optional(CONF_INTERNAL_URL): cv.url,
255  vol.Optional(CONF_EXTERNAL_URL): cv.url,
256  vol.Optional(CONF_ALLOWLIST_EXTERNAL_DIRS): vol.All(
257  cv.ensure_list, [vol.IsDir()]
258  ),
259  vol.Optional(LEGACY_CONF_WHITELIST_EXTERNAL_DIRS): vol.All(
260  cv.ensure_list, [vol.IsDir()]
261  ),
262  vol.Optional(CONF_ALLOWLIST_EXTERNAL_URLS): vol.All(
263  cv.ensure_list, [cv.url]
264  ),
265  vol.Optional(CONF_PACKAGES, default={}): _PACKAGES_CONFIG_SCHEMA,
266  vol.Optional(CONF_AUTH_PROVIDERS): vol.All(
267  cv.ensure_list,
268  [
269  auth_providers.AUTH_PROVIDER_SCHEMA.extend(
270  {
271  CONF_TYPE: vol.NotIn(
272  ["insecure_example"],
273  (
274  "The insecure_example auth provider"
275  " is for testing only."
276  ),
277  )
278  }
279  )
280  ],
281  _no_duplicate_auth_provider,
282  ),
283  vol.Optional(CONF_AUTH_MFA_MODULES): vol.All(
284  cv.ensure_list,
285  [
286  auth_mfa_modules.MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend(
287  {
288  CONF_TYPE: vol.NotIn(
289  ["insecure_example"],
290  "The insecure_example mfa module is for testing only.",
291  )
292  }
293  )
294  ],
295  _no_duplicate_auth_mfa_module,
296  ),
297  vol.Optional(CONF_MEDIA_DIRS): cv.schema_with_slug_keys(vol.IsDir()),
298  vol.Remove(CONF_LEGACY_TEMPLATES): cv.boolean,
299  vol.Optional(CONF_CURRENCY): _validate_currency,
300  vol.Optional(CONF_COUNTRY): cv.country,
301  vol.Optional(CONF_LANGUAGE): cv.language,
302  vol.Optional(CONF_DEBUG): cv.boolean,
303  vol.Optional(CONF_WEBRTC): vol.Schema(
304  {
305  vol.Required(CONF_ICE_SERVERS): vol.All(
306  cv.ensure_list,
307  [
308  vol.Schema(
309  {
310  vol.Required(CONF_URL): vol.All(
311  cv.ensure_list, [_validate_stun_or_turn_url]
312  ),
313  vol.Optional(CONF_USERNAME): cv.string,
314  vol.Optional(CONF_CREDENTIAL): cv.string,
315  }
316  )
317  ],
318  )
319  }
320  ),
321  }
322  ),
323  _filter_bad_internal_external_urls,
324 )
325 
326 
327 async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> None:
328  """Process the [homeassistant] section from the configuration.
329 
330  This method is a coroutine.
331  """
332  # CORE_CONFIG_SCHEMA is not async safe since it uses vol.IsDir
333  # so we need to run it in an executor job.
334  config = await hass.async_add_executor_job(CORE_CONFIG_SCHEMA, config)
335 
336  # Only load auth during startup.
337  if not hasattr(hass, "auth"):
338  if (auth_conf := config.get(CONF_AUTH_PROVIDERS)) is None:
339  auth_conf = [{"type": "homeassistant"}]
340 
341  mfa_conf = config.get(
342  CONF_AUTH_MFA_MODULES,
343  [{"type": "totp", "id": "totp", "name": "Authenticator app"}],
344  )
345 
346  setattr(
347  hass, "auth", await auth.auth_manager_from_config(hass, auth_conf, mfa_conf)
348  )
349 
350  await hass.config.async_load()
351 
352  hac = hass.config
353 
354  if any(
355  k in config
356  for k in (
357  CONF_COUNTRY,
358  CONF_CURRENCY,
359  CONF_ELEVATION,
360  CONF_EXTERNAL_URL,
361  CONF_INTERNAL_URL,
362  CONF_LANGUAGE,
363  CONF_LATITUDE,
364  CONF_LONGITUDE,
365  CONF_NAME,
366  CONF_RADIUS,
367  CONF_TIME_ZONE,
368  CONF_UNIT_SYSTEM,
369  )
370  ):
371  hac.config_source = ConfigSource.YAML
372 
373  for key, attr in (
374  (CONF_COUNTRY, "country"),
375  (CONF_CURRENCY, "currency"),
376  (CONF_ELEVATION, "elevation"),
377  (CONF_EXTERNAL_URL, "external_url"),
378  (CONF_INTERNAL_URL, "internal_url"),
379  (CONF_LANGUAGE, "language"),
380  (CONF_LATITUDE, "latitude"),
381  (CONF_LONGITUDE, "longitude"),
382  (CONF_MEDIA_DIRS, "media_dirs"),
383  (CONF_NAME, "location_name"),
384  (CONF_RADIUS, "radius"),
385  ):
386  if key in config:
387  setattr(hac, attr, config[key])
388 
389  if config.get(CONF_DEBUG):
390  hac.debug = True
391 
392  if CONF_WEBRTC in config:
393  hac.webrtc.ice_servers = [
394  RTCIceServer(
395  server[CONF_URL],
396  server.get(CONF_USERNAME),
397  server.get(CONF_CREDENTIAL),
398  )
399  for server in config[CONF_WEBRTC][CONF_ICE_SERVERS]
400  ]
401 
402  _raise_issue_if_historic_currency(hass, hass.config.currency)
403  _raise_issue_if_no_country(hass, hass.config.country)
404 
405  if CONF_TIME_ZONE in config:
406  await hac.async_set_time_zone(config[CONF_TIME_ZONE])
407 
408  if CONF_MEDIA_DIRS not in config:
409  if is_docker_env():
410  hac.media_dirs = {"local": "/media"}
411  else:
412  hac.media_dirs = {"local": hass.config.path("media")}
413 
414  # Init whitelist external dir
415  hac.allowlist_external_dirs = {hass.config.path("www"), *hac.media_dirs.values()}
416  if CONF_ALLOWLIST_EXTERNAL_DIRS in config:
417  hac.allowlist_external_dirs.update(set(config[CONF_ALLOWLIST_EXTERNAL_DIRS]))
418 
419  elif LEGACY_CONF_WHITELIST_EXTERNAL_DIRS in config:
420  _LOGGER.warning(
421  "Key %s has been replaced with %s. Please update your config",
422  LEGACY_CONF_WHITELIST_EXTERNAL_DIRS,
423  CONF_ALLOWLIST_EXTERNAL_DIRS,
424  )
425  hac.allowlist_external_dirs.update(
426  set(config[LEGACY_CONF_WHITELIST_EXTERNAL_DIRS])
427  )
428 
429  # Init whitelist external URL list – make sure to add / to every URL that doesn't
430  # already have it so that we can properly test "path ownership"
431  if CONF_ALLOWLIST_EXTERNAL_URLS in config:
432  hac.allowlist_external_urls.update(
433  url if url.endswith("/") else f"{url}/"
434  for url in config[CONF_ALLOWLIST_EXTERNAL_URLS]
435  )
436 
437  # Customize
438  cust_exact = dict(config[CONF_CUSTOMIZE])
439  cust_domain = dict(config[CONF_CUSTOMIZE_DOMAIN])
440  cust_glob = OrderedDict(config[CONF_CUSTOMIZE_GLOB])
441 
442  for name, pkg in config[CONF_PACKAGES].items():
443  if (pkg_cust := pkg.get(HOMEASSISTANT_DOMAIN)) is None:
444  continue
445 
446  try:
447  pkg_cust = _CUSTOMIZE_CONFIG_SCHEMA(pkg_cust)
448  except vol.Invalid:
449  _LOGGER.warning("Package %s contains invalid customize", name)
450  continue
451 
452  cust_exact.update(pkg_cust[CONF_CUSTOMIZE])
453  cust_domain.update(pkg_cust[CONF_CUSTOMIZE_DOMAIN])
454  cust_glob.update(pkg_cust[CONF_CUSTOMIZE_GLOB])
455 
456  hass.data[DATA_CUSTOMIZE] = EntityValues(cust_exact, cust_domain, cust_glob)
457 
458  if CONF_UNIT_SYSTEM in config:
459  hac.units = get_unit_system(config[CONF_UNIT_SYSTEM])
460 
461 
462 class _ComponentSet(set[str]):
463  """Set of loaded components.
464 
465  This set contains both top level components and platforms.
466 
467  Examples:
468  `light`, `switch`, `hue`, `mjpeg.camera`, `universal.media_player`,
469  `homeassistant.scene`
470 
471  The top level components set only contains the top level components.
472 
473  The all components set contains all components, including platform
474  based components.
475 
476  """
477 
478  def __init__(
479  self, top_level_components: set[str], all_components: set[str]
480  ) -> None:
481  """Initialize the component set."""
482  self._top_level_components_top_level_components = top_level_components
483  self._all_components_all_components = all_components
484 
485  def add(self, component: str) -> None:
486  """Add a component to the store."""
487  if "." not in component:
488  self._top_level_components_top_level_components.add(component)
489  self._all_components_all_components.add(component)
490  else:
491  platform, _, domain = component.partition(".")
492  if domain in BASE_PLATFORMS:
493  self._all_components_all_components.add(platform)
494  return super().add(component)
495 
496  def remove(self, component: str) -> None:
497  """Remove a component from the store."""
498  if "." in component:
499  raise ValueError("_ComponentSet does not support removing sub-components")
500  self._top_level_components_top_level_components.remove(component)
501  return super().remove(component)
502 
503  def discard(self, component: str) -> None:
504  """Remove a component from the store."""
505  raise NotImplementedError("_ComponentSet does not support discard, use remove")
506 
507 
508 class Config:
509  """Configuration settings for Home Assistant."""
510 
511  _store: Config._ConfigStore
512 
513  def __init__(self, hass: HomeAssistant, config_dir: str) -> None:
514  """Initialize a new config object."""
515  # pylint: disable-next=import-outside-toplevel
516  from .components.zone import DEFAULT_RADIUS
517 
518  self.hasshass = hass
519 
520  self.latitudelatitude: float = 0
521  self.longitudelongitude: float = 0
522 
523  self.elevationelevation: int = 0
524  """Elevation (always in meters regardless of the unit system)."""
525 
526  self.radiusradius: int = DEFAULT_RADIUS
527  """Radius of the Home Zone (always in meters regardless of the unit system)."""
528 
529  self.debug: bool = False
530  self.location_namelocation_name: str = "Home"
531  self.time_zonetime_zone: str = "UTC"
532  self.unitsunits: UnitSystem = METRIC_SYSTEM
533  self.internal_urlinternal_url: str | None = None
534  self.external_urlexternal_url: str | None = None
535  self.currencycurrency: str = "EUR"
536  self.countrycountry: str | None = None
537  self.languagelanguage: str = "en"
538 
539  self.config_sourceconfig_source: ConfigSource = ConfigSource.DEFAULT
540 
541  # If True, pip install is skipped for requirements on startup
542  self.skip_pip: bool = False
543 
544  # List of packages to skip when installing requirements on startup
545  self.skip_pip_packages: list[str] = []
546 
547  # Set of loaded top level components
548  # This set is updated by _ComponentSet
549  # and should not be modified directly
550  self.top_level_components: set[str] = set()
551 
552  # Set of all loaded components including platform
553  # based components
554  self.all_components: set[str] = set()
555 
556  # Set of loaded components
557  self.components: _ComponentSet = _ComponentSet(
558  self.top_level_components, self.all_components
559  )
560 
561  # API (HTTP) server configuration
562  self.api: ApiConfig | None = None
563 
564  # Directory that holds the configuration
565  self.config_dir: str = config_dir
566 
567  # List of allowed external dirs to access
568  self.allowlist_external_dirs: set[str] = set()
569 
570  # List of allowed external URLs that integrations may use
571  self.allowlist_external_urls: set[str] = set()
572 
573  # Dictionary of Media folders that integrations may use
574  self.media_dirs: dict[str, str] = {}
575 
576  # If Home Assistant is running in recovery mode
577  self.recovery_mode: bool = False
578 
579  # Use legacy template behavior
580  self.legacy_templates: bool = False
581 
582  # If Home Assistant is running in safe mode
583  self.safe_mode: bool = False
584 
585  self.webrtcwebrtc = RTCConfiguration()
586 
587  def async_initialize(self) -> None:
588  """Finish initializing a config object.
589 
590  This must be called before the config object is used.
591  """
592  self._store_store = self._ConfigStore_ConfigStore(self.hasshass)
593 
594  def distance(self, lat: float, lon: float) -> float | None:
595  """Calculate distance from Home Assistant.
596 
597  Async friendly.
598  """
599  return self.unitsunits.length(
600  location.distance(self.latitudelatitude, self.longitudelongitude, lat, lon),
601  UnitOfLength.METERS,
602  )
603 
604  def path(self, *path: str) -> str:
605  """Generate path to the file within the configuration directory.
606 
607  Async friendly.
608  """
609  return os.path.join(self.config_dir, *path)
610 
611  def is_allowed_external_url(self, url: str) -> bool:
612  """Check if an external URL is allowed."""
613  parsed_url = f"{yarl.URL(url)!s}/"
614 
615  return any(
616  allowed
617  for allowed in self.allowlist_external_urls
618  if parsed_url.startswith(allowed)
619  )
620 
621  def is_allowed_path(self, path: str) -> bool:
622  """Check if the path is valid for access from outside.
623 
624  This function does blocking I/O and should not be called from the event loop.
625  Use hass.async_add_executor_job to schedule it on the executor.
626  """
627  assert path is not None
628 
629  thepath = pathlib.Path(path)
630  try:
631  # The file path does not have to exist (it's parent should)
632  if thepath.exists():
633  thepath = thepath.resolve()
634  else:
635  thepath = thepath.parent.resolve()
636  except (FileNotFoundError, RuntimeError, PermissionError):
637  return False
638 
639  for allowed_path in self.allowlist_external_dirs:
640  try:
641  thepath.relative_to(allowed_path)
642  except ValueError:
643  pass
644  else:
645  return True
646 
647  return False
648 
649  def as_dict(self) -> dict[str, Any]:
650  """Return a dictionary representation of the configuration.
651 
652  Async friendly.
653  """
654  allowlist_external_dirs = list(self.allowlist_external_dirs)
655  return {
656  "allowlist_external_dirs": allowlist_external_dirs,
657  "allowlist_external_urls": list(self.allowlist_external_urls),
658  "components": list(self.components),
659  "config_dir": self.config_dir,
660  "config_source": self.config_sourceconfig_source,
661  "country": self.countrycountry,
662  "currency": self.currencycurrency,
663  "debug": self.debug,
664  "elevation": self.elevationelevation,
665  "external_url": self.external_urlexternal_url,
666  "internal_url": self.internal_urlinternal_url,
667  "language": self.languagelanguage,
668  "latitude": self.latitudelatitude,
669  "location_name": self.location_namelocation_name,
670  "longitude": self.longitudelongitude,
671  "radius": self.radiusradius,
672  "recovery_mode": self.recovery_mode,
673  "safe_mode": self.safe_mode,
674  "state": self.hasshass.state.value,
675  "time_zone": self.time_zonetime_zone,
676  "unit_system": self.unitsunits.as_dict(),
677  "version": __version__,
678  # legacy, backwards compat
679  "whitelist_external_dirs": allowlist_external_dirs,
680  }
681 
682  async def async_set_time_zone(self, time_zone_str: str) -> None:
683  """Help to set the time zone."""
684  if time_zone := await dt_util.async_get_time_zone(time_zone_str):
685  self.time_zonetime_zone = time_zone_str
686  dt_util.set_default_time_zone(time_zone)
687  else:
688  raise ValueError(f"Received invalid time zone {time_zone_str}")
689 
690  def set_time_zone(self, time_zone_str: str) -> None:
691  """Set the time zone.
692 
693  This is a legacy method that should not be used in new code.
694  Use async_set_time_zone instead.
695 
696  It will be removed in Home Assistant 2025.6.
697  """
698  report_usage(
699  "sets the time zone using set_time_zone instead of async_set_time_zone",
700  core_integration_behavior=ReportBehavior.ERROR,
701  custom_integration_behavior=ReportBehavior.ERROR,
702  breaks_in_ha_version="2025.6",
703  )
704  if time_zone := dt_util.get_time_zone(time_zone_str):
705  self.time_zonetime_zone = time_zone_str
706  dt_util.set_default_time_zone(time_zone)
707  else:
708  raise ValueError(f"Received invalid time zone {time_zone_str}")
709 
710  async def _async_update(
711  self,
712  *,
713  country: str | UndefinedType | None = UNDEFINED,
714  currency: str | None = None,
715  elevation: int | None = None,
716  external_url: str | UndefinedType | None = UNDEFINED,
717  internal_url: str | UndefinedType | None = UNDEFINED,
718  language: str | None = None,
719  latitude: float | None = None,
720  location_name: str | None = None,
721  longitude: float | None = None,
722  radius: int | None = None,
723  source: ConfigSource,
724  time_zone: str | None = None,
725  unit_system: str | None = None,
726  ) -> None:
727  """Update the configuration from a dictionary."""
728  self.config_sourceconfig_source = source
729  if country is not UNDEFINED:
730  self.countrycountry = country
731  if currency is not None:
732  self.currencycurrency = currency
733  if elevation is not None:
734  self.elevationelevation = elevation
735  if external_url is not UNDEFINED:
736  self.external_urlexternal_url = external_url
737  if internal_url is not UNDEFINED:
738  self.internal_urlinternal_url = internal_url
739  if language is not None:
740  self.languagelanguage = language
741  if latitude is not None:
742  self.latitudelatitude = latitude
743  if location_name is not None:
744  self.location_namelocation_name = location_name
745  if longitude is not None:
746  self.longitudelongitude = longitude
747  if radius is not None:
748  self.radiusradius = radius
749  if time_zone is not None:
750  await self.async_set_time_zoneasync_set_time_zone(time_zone)
751  if unit_system is not None:
752  try:
753  self.unitsunits = get_unit_system(unit_system)
754  except ValueError:
755  self.unitsunits = METRIC_SYSTEM
756 
757  async def async_update(self, **kwargs: Any) -> None:
758  """Update the configuration from a dictionary."""
759  await self._async_update_async_update(source=ConfigSource.STORAGE, **kwargs)
760  await self._async_store_async_store()
761  self.hasshass.bus.async_fire_internal(EVENT_CORE_CONFIG_UPDATE, kwargs)
762 
763  _raise_issue_if_historic_currency(self.hasshass, self.currencycurrency)
764  _raise_issue_if_no_country(self.hasshass, self.countrycountry)
765 
766  async def async_load(self) -> None:
767  """Load [homeassistant] core config."""
768  if not (data := await self._store_store.async_load()):
769  return
770 
771  # In 2021.9 we fixed validation to disallow a path (because that's never
772  # correct) but this data still lives in storage, so we print a warning.
773  if data.get("external_url") and urlparse(data["external_url"]).path not in (
774  "",
775  "/",
776  ):
777  _LOGGER.warning("Invalid external_url set. It's not allowed to have a path")
778 
779  if data.get("internal_url") and urlparse(data["internal_url"]).path not in (
780  "",
781  "/",
782  ):
783  _LOGGER.warning("Invalid internal_url set. It's not allowed to have a path")
784 
785  await self._async_update_async_update(
786  source=ConfigSource.STORAGE,
787  latitude=data.get("latitude"),
788  longitude=data.get("longitude"),
789  elevation=data.get("elevation"),
790  unit_system=data.get("unit_system_v2"),
791  location_name=data.get("location_name"),
792  time_zone=data.get("time_zone"),
793  external_url=data.get("external_url", UNDEFINED),
794  internal_url=data.get("internal_url", UNDEFINED),
795  currency=data.get("currency"),
796  country=data.get("country"),
797  language=data.get("language"),
798  radius=data["radius"],
799  )
800 
801  async def _async_store(self) -> None:
802  """Store [homeassistant] core config."""
803  data = {
804  "latitude": self.latitudelatitude,
805  "longitude": self.longitudelongitude,
806  "elevation": self.elevationelevation,
807  # We don't want any integrations to use the name of the unit system
808  # so we are using the private attribute here
809  "unit_system_v2": self.unitsunits._name, # noqa: SLF001
810  "location_name": self.location_namelocation_name,
811  "time_zone": self.time_zonetime_zone,
812  "external_url": self.external_urlexternal_url,
813  "internal_url": self.internal_urlinternal_url,
814  "currency": self.currencycurrency,
815  "country": self.countrycountry,
816  "language": self.languagelanguage,
817  "radius": self.radiusradius,
818  }
819  await self._store_store.async_save(data)
820 
821  class _ConfigStore(Store[dict[str, Any]]):
822  """Class to help storing Config data."""
823 
824  def __init__(self, hass: HomeAssistant) -> None:
825  """Initialize storage class."""
826  super().__init__(
827  hass,
828  CORE_STORAGE_VERSION,
829  CORE_STORAGE_KEY,
830  private=True,
831  atomic_writes=True,
832  minor_version=CORE_STORAGE_MINOR_VERSION,
833  )
834  self._original_unit_system_original_unit_system: str | None = None # from old store 1.1
835 
837  self,
838  old_major_version: int,
839  old_minor_version: int,
840  old_data: dict[str, Any],
841  ) -> dict[str, Any]:
842  """Migrate to the new version."""
843 
844  # pylint: disable-next=import-outside-toplevel
845  from .components.zone import DEFAULT_RADIUS
846 
847  data = old_data
848  if old_major_version == 1 and old_minor_version < 2:
849  # In 1.2, we remove support for "imperial", replaced by "us_customary"
850  # Using a new key to allow rollback
851  self._original_unit_system_original_unit_system = data.get("unit_system")
852  data["unit_system_v2"] = self._original_unit_system_original_unit_system
853  if data["unit_system_v2"] == _CONF_UNIT_SYSTEM_IMPERIAL:
854  data["unit_system_v2"] = _CONF_UNIT_SYSTEM_US_CUSTOMARY
855  if old_major_version == 1 and old_minor_version < 3:
856  # In 1.3, we add the key "language", initialize it from the
857  # owner account.
858  data["language"] = "en"
859  try:
860  owner = await self.hass.auth.async_get_owner()
861  if owner is not None:
862  # pylint: disable-next=import-outside-toplevel
863  from .components.frontend import storage as frontend_store
864 
865  _, owner_data = await frontend_store.async_user_store(
866  self.hass, owner.id
867  )
868 
869  if (
870  "language" in owner_data
871  and "language" in owner_data["language"]
872  ):
873  with suppress(vol.InInvalid):
874  data["language"] = cv.language(
875  owner_data["language"]["language"]
876  )
877  # pylint: disable-next=broad-except
878  except Exception:
879  _LOGGER.exception("Unexpected error during core config migration")
880  if old_major_version == 1 and old_minor_version < 4:
881  # In 1.4, we add the key "radius", initialize it with the default.
882  data.setdefault("radius", DEFAULT_RADIUS)
883 
884  if old_major_version > 1:
885  raise NotImplementedError
886  return data
887 
888  async def async_save(self, data: dict[str, Any]) -> None:
889  if self._original_unit_system_original_unit_system:
890  data["unit_system"] = self._original_unit_system_original_unit_system
891  return await super().async_save(data)
dict[str, Any] _async_migrate_func(self, int old_major_version, int old_minor_version, dict[str, Any] old_data)
Definition: core_config.py:841
None async_save(self, dict[str, Any] data)
Definition: core_config.py:888
None __init__(self, HomeAssistant hass)
Definition: core_config.py:824
None async_update(self, **Any kwargs)
Definition: core_config.py:757
None _async_update(self, *str|UndefinedType|None country=UNDEFINED, str|None currency=None, int|None elevation=None, str|UndefinedType|None external_url=UNDEFINED, str|UndefinedType|None internal_url=UNDEFINED, str|None language=None, float|None latitude=None, str|None location_name=None, float|None longitude=None, int|None radius=None, ConfigSource source, str|None time_zone=None, str|None unit_system=None)
Definition: core_config.py:726
float|None distance(self, float lat, float lon)
Definition: core_config.py:594
None async_set_time_zone(self, str time_zone_str)
Definition: core_config.py:682
None set_time_zone(self, str time_zone_str)
Definition: core_config.py:690
None __init__(self, HomeAssistant hass, str config_dir)
Definition: core_config.py:513
str path(self, *str path)
Definition: core_config.py:604
bool is_allowed_path(self, str path)
Definition: core_config.py:621
bool is_allowed_external_url(self, str url)
Definition: core_config.py:611
dict[str, Any] as_dict(self)
Definition: core_config.py:649
None __init__(self, set[str] top_level_components, set[str] all_components)
Definition: core_config.py:480
None discard(self, str component)
Definition: core_config.py:503
None remove(self, str component)
Definition: core_config.py:496
None add(self, str component)
Definition: core_config.py:485
str _validate_stun_or_turn_url(Any value)
Definition: core_config.py:233
Sequence[dict[str, Any]] _no_duplicate_auth_mfa_module(Sequence[dict[str, Any]] configs)
Definition: core_config.py:127
None _raise_issue_if_no_country(HomeAssistant hass, str|None country)
Definition: core_config.py:208
None _raise_issue_if_historic_currency(HomeAssistant hass, str currency)
Definition: core_config.py:191
None async_process_ha_core_config(HomeAssistant hass, dict config)
Definition: core_config.py:327
Any _validate_currency(Any data)
Definition: core_config.py:224
Sequence[dict[str, Any]] _no_duplicate_auth_provider(Sequence[dict[str, Any]] configs)
Definition: core_config.py:106
dict _filter_bad_internal_external_urls(dict conf)
Definition: core_config.py:148
None report_usage(str what, *str|None breaks_in_ha_version=None, ReportBehavior core_behavior=ReportBehavior.ERROR, ReportBehavior core_integration_behavior=ReportBehavior.LOG, ReportBehavior custom_integration_behavior=ReportBehavior.LOG, set[str]|None exclude_integrations=None, str|None integration_domain=None, int level=logging.WARNING)
Definition: frame.py:195
None async_save(self, _T data)
Definition: storage.py:424
UnitSystem get_unit_system(str key)
Definition: unit_system.py:223