Move core config functionality to its own module (#129065)

* Move core config functionality to its own module

* Adjust test
This commit is contained in:
Erik Montnemery 2024-10-24 13:34:51 +02:00 committed by GitHub
parent cd4aa8ccd6
commit 3e62c6ae2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 1308 additions and 1284 deletions

View file

@ -70,6 +70,7 @@ from .const import (
REQUIRED_NEXT_PYTHON_VER,
SIGNAL_BOOTSTRAP_INTEGRATIONS,
)
from .core_config import async_process_ha_core_config
from .exceptions import HomeAssistantError
from .helpers import (
area_registry,
@ -479,7 +480,7 @@ async def async_from_config_dict(
core_config = config.get(core.DOMAIN, {})
try:
await conf_util.async_process_ha_core_config(hass, core_config)
await async_process_ha_core_config(hass, core_config)
except vol.Invalid as config_err:
conf_util.async_log_schema_error(config_err, core.DOMAIN, core_config, hass)
async_notify_setup_error(hass, core.DOMAIN)

View file

@ -8,9 +8,9 @@ from typing import Any
import voluptuous as vol
from homeassistant import config as conf_util, core_config
from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_CONTROL
from homeassistant.components import persistent_notification
import homeassistant.config as conf_util
from homeassistant.const import (
ATTR_ELEVATION,
ATTR_ENTITY_ID,
@ -269,7 +269,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
return
# auth only processed during startup
await conf_util.async_process_ha_core_config(hass, conf.get(DOMAIN) or {})
await core_config.async_process_ha_core_config(hass, conf.get(DOMAIN) or {})
async_register_admin_service(
hass, DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config

View file

@ -16,66 +16,24 @@ from pathlib import Path
import re
import shutil
from types import ModuleType
from typing import TYPE_CHECKING, Any, Final
from urllib.parse import urlparse
from typing import TYPE_CHECKING, Any
from awesomeversion import AwesomeVersion
import voluptuous as vol
from voluptuous.humanize import MAX_VALIDATION_ERROR_ITEM_LENGTH
from yaml.error import MarkedYAMLError
from . import auth
from .auth import mfa_modules as auth_mfa_modules, providers as auth_providers
from .const import (
ATTR_ASSUMED_STATE,
ATTR_FRIENDLY_NAME,
ATTR_HIDDEN,
CONF_ALLOWLIST_EXTERNAL_DIRS,
CONF_ALLOWLIST_EXTERNAL_URLS,
CONF_AUTH_MFA_MODULES,
CONF_AUTH_PROVIDERS,
CONF_COUNTRY,
CONF_CURRENCY,
CONF_CUSTOMIZE,
CONF_CUSTOMIZE_DOMAIN,
CONF_CUSTOMIZE_GLOB,
CONF_DEBUG,
CONF_ELEVATION,
CONF_EXTERNAL_URL,
CONF_ID,
CONF_INTERNAL_URL,
CONF_LANGUAGE,
CONF_LATITUDE,
CONF_LEGACY_TEMPLATES,
CONF_LONGITUDE,
CONF_MEDIA_DIRS,
CONF_NAME,
CONF_PACKAGES,
CONF_PLATFORM,
CONF_RADIUS,
CONF_TEMPERATURE_UNIT,
CONF_TIME_ZONE,
CONF_TYPE,
CONF_UNIT_SYSTEM,
CONF_URL,
CONF_USERNAME,
LEGACY_CONF_WHITELIST_EXTERNAL_DIRS,
__version__,
)
from .core import DOMAIN as HOMEASSISTANT_DOMAIN, ConfigSource, HomeAssistant, callback
from .const import CONF_PACKAGES, CONF_PLATFORM, __version__
from .core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
from .core_config import _PACKAGE_DEFINITION_SCHEMA, _PACKAGES_CONFIG_SCHEMA
from .exceptions import ConfigValidationError, HomeAssistantError
from .generated.currencies import HISTORIC_CURRENCIES
from .helpers import config_validation as cv, issue_registry as ir
from .helpers.entity_values import EntityValues
from .helpers import config_validation as cv
from .helpers.translation import async_get_exception_message
from .helpers.typing import ConfigType
from .loader import ComponentProtocol, Integration, IntegrationNotFound
from .requirements import RequirementsNotFound, async_get_integration_with_requirements
from .util.async_ import create_eager_task
from .util.hass_dict import HassKey
from .util.package import is_docker_env
from .util.unit_system import get_unit_system, validate_unit_system
from .util.webrtc import RTCIceServer
from .util.yaml import SECRET_YAML, Secrets, YamlTypeError, load_yaml_dict
from .util.yaml.objects import NodeStrClass
@ -86,7 +44,6 @@ RE_ASCII = re.compile(r"\033\[[^m]*m")
YAML_CONFIG_FILE = "configuration.yaml"
VERSION_FILE = ".HA_VERSION"
CONFIG_DIR_NAME = ".homeassistant"
DATA_CUSTOMIZE: HassKey[EntityValues] = HassKey("hass_customize")
AUTOMATION_CONFIG_PATH = "automations.yaml"
SCRIPT_CONFIG_PATH = "scripts.yaml"
@ -97,10 +54,6 @@ INTEGRATION_LOAD_EXCEPTIONS = (IntegrationNotFound, RequirementsNotFound)
SAFE_MODE_FILENAME = "safe-mode"
CONF_CREDENTIAL: Final = "credential"
CONF_ICE_SERVERS: Final = "ice_servers"
CONF_WEBRTC: Final = "webrtc"
DEFAULT_CONFIG = f"""
# Loads default set of integrations. Do not remove.
default_config:
@ -179,229 +132,6 @@ class IntegrationConfigInfo:
exception_info_list: list[ConfigExceptionInfo]
def _no_duplicate_auth_provider(
configs: Sequence[dict[str, Any]],
) -> Sequence[dict[str, Any]]:
"""No duplicate auth provider config allowed in a list.
Each type of auth provider can only have one config without optional id.
Unique id is required if same type of auth provider used multiple times.
"""
config_keys: set[tuple[str, str | None]] = set()
for config in configs:
key = (config[CONF_TYPE], config.get(CONF_ID))
if key in config_keys:
raise vol.Invalid(
f"Duplicate auth provider {config[CONF_TYPE]} found. "
"Please add unique IDs "
"if you want to have the same auth provider twice"
)
config_keys.add(key)
return configs
def _no_duplicate_auth_mfa_module(
configs: Sequence[dict[str, Any]],
) -> Sequence[dict[str, Any]]:
"""No duplicate auth mfa module item allowed in a list.
Each type of mfa module can only have one config without optional id.
A global unique id is required if same type of mfa module used multiple
times.
Note: this is different than auth provider
"""
config_keys: set[str] = set()
for config in configs:
key = config.get(CONF_ID, config[CONF_TYPE])
if key in config_keys:
raise vol.Invalid(
f"Duplicate mfa module {config[CONF_TYPE]} found. "
"Please add unique IDs "
"if you want to have the same mfa module twice"
)
config_keys.add(key)
return configs
def _filter_bad_internal_external_urls(conf: dict) -> dict:
"""Filter internal/external URL with a path."""
for key in CONF_INTERNAL_URL, CONF_EXTERNAL_URL:
if key in conf and urlparse(conf[key]).path not in ("", "/"):
# We warn but do not fix, because if this was incorrectly configured,
# adjusting this value might impact security.
_LOGGER.warning(
"Invalid %s set. It's not allowed to have a path (/bla)", key
)
return conf
# Schema for all packages element
PACKAGES_CONFIG_SCHEMA = vol.Schema({cv.string: vol.Any(dict, list)})
# Schema for individual package definition
PACKAGE_DEFINITION_SCHEMA = vol.Schema({cv.string: vol.Any(dict, list, None)})
CUSTOMIZE_DICT_SCHEMA = vol.Schema(
{
vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
vol.Optional(ATTR_HIDDEN): cv.boolean,
vol.Optional(ATTR_ASSUMED_STATE): cv.boolean,
},
extra=vol.ALLOW_EXTRA,
)
CUSTOMIZE_CONFIG_SCHEMA = vol.Schema(
{
vol.Optional(CONF_CUSTOMIZE, default={}): vol.Schema(
{cv.entity_id: CUSTOMIZE_DICT_SCHEMA}
),
vol.Optional(CONF_CUSTOMIZE_DOMAIN, default={}): vol.Schema(
{cv.string: CUSTOMIZE_DICT_SCHEMA}
),
vol.Optional(CONF_CUSTOMIZE_GLOB, default={}): vol.Schema(
{cv.string: CUSTOMIZE_DICT_SCHEMA}
),
}
)
def _raise_issue_if_historic_currency(hass: HomeAssistant, currency: str) -> None:
if currency not in HISTORIC_CURRENCIES:
ir.async_delete_issue(hass, HOMEASSISTANT_DOMAIN, "historic_currency")
return
ir.async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
"historic_currency",
is_fixable=False,
learn_more_url="homeassistant://config/general",
severity=ir.IssueSeverity.WARNING,
translation_key="historic_currency",
translation_placeholders={"currency": currency},
)
def _raise_issue_if_no_country(hass: HomeAssistant, country: str | None) -> None:
if country is not None:
ir.async_delete_issue(hass, HOMEASSISTANT_DOMAIN, "country_not_configured")
return
ir.async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
"country_not_configured",
is_fixable=False,
learn_more_url="homeassistant://config/general",
severity=ir.IssueSeverity.WARNING,
translation_key="country_not_configured",
)
def _validate_currency(data: Any) -> Any:
try:
return cv.currency(data)
except vol.InInvalid:
with suppress(vol.InInvalid):
return cv.historic_currency(data)
raise
def _validate_stun_or_turn_url(value: Any) -> str:
"""Validate an URL."""
url_in = str(value)
url = urlparse(url_in)
if url.scheme not in ("stun", "stuns", "turn", "turns"):
raise vol.Invalid("invalid url")
return url_in
CORE_CONFIG_SCHEMA = vol.All(
CUSTOMIZE_CONFIG_SCHEMA.extend(
{
CONF_NAME: vol.Coerce(str),
CONF_LATITUDE: cv.latitude,
CONF_LONGITUDE: cv.longitude,
CONF_ELEVATION: vol.Coerce(int),
CONF_RADIUS: cv.positive_int,
vol.Remove(CONF_TEMPERATURE_UNIT): cv.temperature_unit,
CONF_UNIT_SYSTEM: validate_unit_system,
CONF_TIME_ZONE: cv.time_zone,
vol.Optional(CONF_INTERNAL_URL): cv.url,
vol.Optional(CONF_EXTERNAL_URL): cv.url,
vol.Optional(CONF_ALLOWLIST_EXTERNAL_DIRS): vol.All(
cv.ensure_list, [vol.IsDir()]
),
vol.Optional(LEGACY_CONF_WHITELIST_EXTERNAL_DIRS): vol.All(
cv.ensure_list, [vol.IsDir()]
),
vol.Optional(CONF_ALLOWLIST_EXTERNAL_URLS): vol.All(
cv.ensure_list, [cv.url]
),
vol.Optional(CONF_PACKAGES, default={}): PACKAGES_CONFIG_SCHEMA,
vol.Optional(CONF_AUTH_PROVIDERS): vol.All(
cv.ensure_list,
[
auth_providers.AUTH_PROVIDER_SCHEMA.extend(
{
CONF_TYPE: vol.NotIn(
["insecure_example"],
(
"The insecure_example auth provider"
" is for testing only."
),
)
}
)
],
_no_duplicate_auth_provider,
),
vol.Optional(CONF_AUTH_MFA_MODULES): vol.All(
cv.ensure_list,
[
auth_mfa_modules.MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend(
{
CONF_TYPE: vol.NotIn(
["insecure_example"],
"The insecure_example mfa module is for testing only.",
)
}
)
],
_no_duplicate_auth_mfa_module,
),
vol.Optional(CONF_MEDIA_DIRS): cv.schema_with_slug_keys(vol.IsDir()),
vol.Remove(CONF_LEGACY_TEMPLATES): cv.boolean,
vol.Optional(CONF_CURRENCY): _validate_currency,
vol.Optional(CONF_COUNTRY): cv.country,
vol.Optional(CONF_LANGUAGE): cv.language,
vol.Optional(CONF_DEBUG): cv.boolean,
vol.Optional(CONF_WEBRTC): vol.Schema(
{
vol.Required(CONF_ICE_SERVERS): vol.All(
cv.ensure_list,
[
vol.Schema(
{
vol.Required(CONF_URL): vol.All(
cv.ensure_list, [_validate_stun_or_turn_url]
),
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_CREDENTIAL): cv.string,
}
)
],
)
}
),
}
),
_filter_bad_internal_external_urls,
)
def get_default_config_dir() -> str:
"""Put together the default configuration directory based on the OS."""
data_dir = os.path.expanduser("~")
@ -847,141 +577,6 @@ def format_schema_error(
return humanize_error(hass, exc, domain, config, link)
async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> None:
"""Process the [homeassistant] section from the configuration.
This method is a coroutine.
"""
# CORE_CONFIG_SCHEMA is not async safe since it uses vol.IsDir
# so we need to run it in an executor job.
config = await hass.async_add_executor_job(CORE_CONFIG_SCHEMA, config)
# Only load auth during startup.
if not hasattr(hass, "auth"):
if (auth_conf := config.get(CONF_AUTH_PROVIDERS)) is None:
auth_conf = [{"type": "homeassistant"}]
mfa_conf = config.get(
CONF_AUTH_MFA_MODULES,
[{"type": "totp", "id": "totp", "name": "Authenticator app"}],
)
setattr(
hass, "auth", await auth.auth_manager_from_config(hass, auth_conf, mfa_conf)
)
await hass.config.async_load()
hac = hass.config
if any(
k in config
for k in (
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
CONF_ELEVATION,
CONF_TIME_ZONE,
CONF_UNIT_SYSTEM,
CONF_EXTERNAL_URL,
CONF_INTERNAL_URL,
CONF_CURRENCY,
CONF_COUNTRY,
CONF_LANGUAGE,
CONF_RADIUS,
)
):
hac.config_source = ConfigSource.YAML
for key, attr in (
(CONF_LATITUDE, "latitude"),
(CONF_LONGITUDE, "longitude"),
(CONF_NAME, "location_name"),
(CONF_ELEVATION, "elevation"),
(CONF_INTERNAL_URL, "internal_url"),
(CONF_EXTERNAL_URL, "external_url"),
(CONF_MEDIA_DIRS, "media_dirs"),
(CONF_CURRENCY, "currency"),
(CONF_COUNTRY, "country"),
(CONF_LANGUAGE, "language"),
(CONF_RADIUS, "radius"),
):
if key in config:
setattr(hac, attr, config[key])
if config.get(CONF_DEBUG):
hac.debug = True
if CONF_WEBRTC in config:
hac.webrtc.ice_servers = [
RTCIceServer(
server[CONF_URL],
server.get(CONF_USERNAME),
server.get(CONF_CREDENTIAL),
)
for server in config[CONF_WEBRTC][CONF_ICE_SERVERS]
]
_raise_issue_if_historic_currency(hass, hass.config.currency)
_raise_issue_if_no_country(hass, hass.config.country)
if CONF_TIME_ZONE in config:
await hac.async_set_time_zone(config[CONF_TIME_ZONE])
if CONF_MEDIA_DIRS not in config:
if is_docker_env():
hac.media_dirs = {"local": "/media"}
else:
hac.media_dirs = {"local": hass.config.path("media")}
# Init whitelist external dir
hac.allowlist_external_dirs = {hass.config.path("www"), *hac.media_dirs.values()}
if CONF_ALLOWLIST_EXTERNAL_DIRS in config:
hac.allowlist_external_dirs.update(set(config[CONF_ALLOWLIST_EXTERNAL_DIRS]))
elif LEGACY_CONF_WHITELIST_EXTERNAL_DIRS in config:
_LOGGER.warning(
"Key %s has been replaced with %s. Please update your config",
LEGACY_CONF_WHITELIST_EXTERNAL_DIRS,
CONF_ALLOWLIST_EXTERNAL_DIRS,
)
hac.allowlist_external_dirs.update(
set(config[LEGACY_CONF_WHITELIST_EXTERNAL_DIRS])
)
# Init whitelist external URL list make sure to add / to every URL that doesn't
# already have it so that we can properly test "path ownership"
if CONF_ALLOWLIST_EXTERNAL_URLS in config:
hac.allowlist_external_urls.update(
url if url.endswith("/") else f"{url}/"
for url in config[CONF_ALLOWLIST_EXTERNAL_URLS]
)
# Customize
cust_exact = dict(config[CONF_CUSTOMIZE])
cust_domain = dict(config[CONF_CUSTOMIZE_DOMAIN])
cust_glob = OrderedDict(config[CONF_CUSTOMIZE_GLOB])
for name, pkg in config[CONF_PACKAGES].items():
if (pkg_cust := pkg.get(HOMEASSISTANT_DOMAIN)) is None:
continue
try:
pkg_cust = CUSTOMIZE_CONFIG_SCHEMA(pkg_cust)
except vol.Invalid:
_LOGGER.warning("Package %s contains invalid customize", name)
continue
cust_exact.update(pkg_cust[CONF_CUSTOMIZE])
cust_domain.update(pkg_cust[CONF_CUSTOMIZE_DOMAIN])
cust_glob.update(pkg_cust[CONF_CUSTOMIZE_GLOB])
hass.data[DATA_CUSTOMIZE] = EntityValues(cust_exact, cust_domain, cust_glob)
if CONF_UNIT_SYSTEM in config:
hac.units = get_unit_system(config[CONF_UNIT_SYSTEM])
def _log_pkg_error(
hass: HomeAssistant, package: str, component: str | None, config: dict, message: str
) -> None:
@ -1046,7 +641,7 @@ def _identify_config_schema(module: ComponentProtocol) -> str | None:
def _validate_package_definition(name: str, conf: Any) -> None:
"""Validate basic package definition properties."""
cv.slug(name)
PACKAGE_DEFINITION_SCHEMA(conf)
_PACKAGE_DEFINITION_SCHEMA(conf)
def _recursive_merge(conf: dict[str, Any], package: dict[str, Any]) -> str | None:
@ -1085,7 +680,7 @@ async def merge_packages_config(
vol.Invalid if whole package config is invalid.
"""
PACKAGES_CONFIG_SCHEMA(packages)
_PACKAGES_CONFIG_SCHEMA(packages)
invalid_packages = []
for pack_name, pack_conf in packages.items():

View file

@ -3145,7 +3145,7 @@ class Config:
async def async_update(self, **kwargs: Any) -> None:
"""Update the configuration from a dictionary."""
# pylint: disable-next=import-outside-toplevel
from .config import (
from .core_config import (
_raise_issue_if_historic_currency,
_raise_issue_if_no_country,
)

View file

@ -0,0 +1,423 @@
"""Module to help with parsing and generating configuration files."""
from __future__ import annotations
from collections import OrderedDict
from collections.abc import Sequence
from contextlib import suppress
import logging
from typing import Any, Final
from urllib.parse import urlparse
import voluptuous as vol
from . import auth
from .auth import mfa_modules as auth_mfa_modules, providers as auth_providers
from .const import (
ATTR_ASSUMED_STATE,
ATTR_FRIENDLY_NAME,
ATTR_HIDDEN,
CONF_ALLOWLIST_EXTERNAL_DIRS,
CONF_ALLOWLIST_EXTERNAL_URLS,
CONF_AUTH_MFA_MODULES,
CONF_AUTH_PROVIDERS,
CONF_COUNTRY,
CONF_CURRENCY,
CONF_CUSTOMIZE,
CONF_CUSTOMIZE_DOMAIN,
CONF_CUSTOMIZE_GLOB,
CONF_DEBUG,
CONF_ELEVATION,
CONF_EXTERNAL_URL,
CONF_ID,
CONF_INTERNAL_URL,
CONF_LANGUAGE,
CONF_LATITUDE,
CONF_LEGACY_TEMPLATES,
CONF_LONGITUDE,
CONF_MEDIA_DIRS,
CONF_NAME,
CONF_PACKAGES,
CONF_RADIUS,
CONF_TEMPERATURE_UNIT,
CONF_TIME_ZONE,
CONF_TYPE,
CONF_UNIT_SYSTEM,
CONF_URL,
CONF_USERNAME,
LEGACY_CONF_WHITELIST_EXTERNAL_DIRS,
)
from .core import DOMAIN as HOMEASSISTANT_DOMAIN, ConfigSource, HomeAssistant
from .generated.currencies import HISTORIC_CURRENCIES
from .helpers import config_validation as cv, issue_registry as ir
from .helpers.entity_values import EntityValues
from .util.hass_dict import HassKey
from .util.package import is_docker_env
from .util.unit_system import get_unit_system, validate_unit_system
from .util.webrtc import RTCIceServer
_LOGGER = logging.getLogger(__name__)
DATA_CUSTOMIZE: HassKey[EntityValues] = HassKey("hass_customize")
CONF_CREDENTIAL: Final = "credential"
CONF_ICE_SERVERS: Final = "ice_servers"
CONF_WEBRTC: Final = "webrtc"
def _no_duplicate_auth_provider(
configs: Sequence[dict[str, Any]],
) -> Sequence[dict[str, Any]]:
"""No duplicate auth provider config allowed in a list.
Each type of auth provider can only have one config without optional id.
Unique id is required if same type of auth provider used multiple times.
"""
config_keys: set[tuple[str, str | None]] = set()
for config in configs:
key = (config[CONF_TYPE], config.get(CONF_ID))
if key in config_keys:
raise vol.Invalid(
f"Duplicate auth provider {config[CONF_TYPE]} found. "
"Please add unique IDs "
"if you want to have the same auth provider twice"
)
config_keys.add(key)
return configs
def _no_duplicate_auth_mfa_module(
configs: Sequence[dict[str, Any]],
) -> Sequence[dict[str, Any]]:
"""No duplicate auth mfa module item allowed in a list.
Each type of mfa module can only have one config without optional id.
A global unique id is required if same type of mfa module used multiple
times.
Note: this is different than auth provider
"""
config_keys: set[str] = set()
for config in configs:
key = config.get(CONF_ID, config[CONF_TYPE])
if key in config_keys:
raise vol.Invalid(
f"Duplicate mfa module {config[CONF_TYPE]} found. "
"Please add unique IDs "
"if you want to have the same mfa module twice"
)
config_keys.add(key)
return configs
def _filter_bad_internal_external_urls(conf: dict) -> dict:
"""Filter internal/external URL with a path."""
for key in CONF_INTERNAL_URL, CONF_EXTERNAL_URL:
if key in conf and urlparse(conf[key]).path not in ("", "/"):
# We warn but do not fix, because if this was incorrectly configured,
# adjusting this value might impact security.
_LOGGER.warning(
"Invalid %s set. It's not allowed to have a path (/bla)", key
)
return conf
# Schema for all packages element
_PACKAGES_CONFIG_SCHEMA = vol.Schema({cv.string: vol.Any(dict, list)})
# Schema for individual package definition
_PACKAGE_DEFINITION_SCHEMA = vol.Schema({cv.string: vol.Any(dict, list, None)})
_CUSTOMIZE_DICT_SCHEMA = vol.Schema(
{
vol.Optional(ATTR_FRIENDLY_NAME): cv.string,
vol.Optional(ATTR_HIDDEN): cv.boolean,
vol.Optional(ATTR_ASSUMED_STATE): cv.boolean,
},
extra=vol.ALLOW_EXTRA,
)
_CUSTOMIZE_CONFIG_SCHEMA = vol.Schema(
{
vol.Optional(CONF_CUSTOMIZE, default={}): vol.Schema(
{cv.entity_id: _CUSTOMIZE_DICT_SCHEMA}
),
vol.Optional(CONF_CUSTOMIZE_DOMAIN, default={}): vol.Schema(
{cv.string: _CUSTOMIZE_DICT_SCHEMA}
),
vol.Optional(CONF_CUSTOMIZE_GLOB, default={}): vol.Schema(
{cv.string: _CUSTOMIZE_DICT_SCHEMA}
),
}
)
def _raise_issue_if_historic_currency(hass: HomeAssistant, currency: str) -> None:
if currency not in HISTORIC_CURRENCIES:
ir.async_delete_issue(hass, HOMEASSISTANT_DOMAIN, "historic_currency")
return
ir.async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
"historic_currency",
is_fixable=False,
learn_more_url="homeassistant://config/general",
severity=ir.IssueSeverity.WARNING,
translation_key="historic_currency",
translation_placeholders={"currency": currency},
)
def _raise_issue_if_no_country(hass: HomeAssistant, country: str | None) -> None:
if country is not None:
ir.async_delete_issue(hass, HOMEASSISTANT_DOMAIN, "country_not_configured")
return
ir.async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
"country_not_configured",
is_fixable=False,
learn_more_url="homeassistant://config/general",
severity=ir.IssueSeverity.WARNING,
translation_key="country_not_configured",
)
def _validate_currency(data: Any) -> Any:
try:
return cv.currency(data)
except vol.InInvalid:
with suppress(vol.InInvalid):
return cv.historic_currency(data)
raise
def _validate_stun_or_turn_url(value: Any) -> str:
"""Validate an URL."""
url_in = str(value)
url = urlparse(url_in)
if url.scheme not in ("stun", "stuns", "turn", "turns"):
raise vol.Invalid("invalid url")
return url_in
CORE_CONFIG_SCHEMA = vol.All(
_CUSTOMIZE_CONFIG_SCHEMA.extend(
{
CONF_NAME: vol.Coerce(str),
CONF_LATITUDE: cv.latitude,
CONF_LONGITUDE: cv.longitude,
CONF_ELEVATION: vol.Coerce(int),
CONF_RADIUS: cv.positive_int,
vol.Remove(CONF_TEMPERATURE_UNIT): cv.temperature_unit,
CONF_UNIT_SYSTEM: validate_unit_system,
CONF_TIME_ZONE: cv.time_zone,
vol.Optional(CONF_INTERNAL_URL): cv.url,
vol.Optional(CONF_EXTERNAL_URL): cv.url,
vol.Optional(CONF_ALLOWLIST_EXTERNAL_DIRS): vol.All(
cv.ensure_list, [vol.IsDir()]
),
vol.Optional(LEGACY_CONF_WHITELIST_EXTERNAL_DIRS): vol.All(
cv.ensure_list, [vol.IsDir()]
),
vol.Optional(CONF_ALLOWLIST_EXTERNAL_URLS): vol.All(
cv.ensure_list, [cv.url]
),
vol.Optional(CONF_PACKAGES, default={}): _PACKAGES_CONFIG_SCHEMA,
vol.Optional(CONF_AUTH_PROVIDERS): vol.All(
cv.ensure_list,
[
auth_providers.AUTH_PROVIDER_SCHEMA.extend(
{
CONF_TYPE: vol.NotIn(
["insecure_example"],
(
"The insecure_example auth provider"
" is for testing only."
),
)
}
)
],
_no_duplicate_auth_provider,
),
vol.Optional(CONF_AUTH_MFA_MODULES): vol.All(
cv.ensure_list,
[
auth_mfa_modules.MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend(
{
CONF_TYPE: vol.NotIn(
["insecure_example"],
"The insecure_example mfa module is for testing only.",
)
}
)
],
_no_duplicate_auth_mfa_module,
),
vol.Optional(CONF_MEDIA_DIRS): cv.schema_with_slug_keys(vol.IsDir()),
vol.Remove(CONF_LEGACY_TEMPLATES): cv.boolean,
vol.Optional(CONF_CURRENCY): _validate_currency,
vol.Optional(CONF_COUNTRY): cv.country,
vol.Optional(CONF_LANGUAGE): cv.language,
vol.Optional(CONF_DEBUG): cv.boolean,
vol.Optional(CONF_WEBRTC): vol.Schema(
{
vol.Required(CONF_ICE_SERVERS): vol.All(
cv.ensure_list,
[
vol.Schema(
{
vol.Required(CONF_URL): vol.All(
cv.ensure_list, [_validate_stun_or_turn_url]
),
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_CREDENTIAL): cv.string,
}
)
],
)
}
),
}
),
_filter_bad_internal_external_urls,
)
async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> None:
"""Process the [homeassistant] section from the configuration.
This method is a coroutine.
"""
# CORE_CONFIG_SCHEMA is not async safe since it uses vol.IsDir
# so we need to run it in an executor job.
config = await hass.async_add_executor_job(CORE_CONFIG_SCHEMA, config)
# Only load auth during startup.
if not hasattr(hass, "auth"):
if (auth_conf := config.get(CONF_AUTH_PROVIDERS)) is None:
auth_conf = [{"type": "homeassistant"}]
mfa_conf = config.get(
CONF_AUTH_MFA_MODULES,
[{"type": "totp", "id": "totp", "name": "Authenticator app"}],
)
setattr(
hass, "auth", await auth.auth_manager_from_config(hass, auth_conf, mfa_conf)
)
await hass.config.async_load()
hac = hass.config
if any(
k in config
for k in (
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
CONF_ELEVATION,
CONF_TIME_ZONE,
CONF_UNIT_SYSTEM,
CONF_EXTERNAL_URL,
CONF_INTERNAL_URL,
CONF_CURRENCY,
CONF_COUNTRY,
CONF_LANGUAGE,
CONF_RADIUS,
)
):
hac.config_source = ConfigSource.YAML
for key, attr in (
(CONF_LATITUDE, "latitude"),
(CONF_LONGITUDE, "longitude"),
(CONF_NAME, "location_name"),
(CONF_ELEVATION, "elevation"),
(CONF_INTERNAL_URL, "internal_url"),
(CONF_EXTERNAL_URL, "external_url"),
(CONF_MEDIA_DIRS, "media_dirs"),
(CONF_CURRENCY, "currency"),
(CONF_COUNTRY, "country"),
(CONF_LANGUAGE, "language"),
(CONF_RADIUS, "radius"),
):
if key in config:
setattr(hac, attr, config[key])
if config.get(CONF_DEBUG):
hac.debug = True
if CONF_WEBRTC in config:
hac.webrtc.ice_servers = [
RTCIceServer(
server[CONF_URL],
server.get(CONF_USERNAME),
server.get(CONF_CREDENTIAL),
)
for server in config[CONF_WEBRTC][CONF_ICE_SERVERS]
]
_raise_issue_if_historic_currency(hass, hass.config.currency)
_raise_issue_if_no_country(hass, hass.config.country)
if CONF_TIME_ZONE in config:
await hac.async_set_time_zone(config[CONF_TIME_ZONE])
if CONF_MEDIA_DIRS not in config:
if is_docker_env():
hac.media_dirs = {"local": "/media"}
else:
hac.media_dirs = {"local": hass.config.path("media")}
# Init whitelist external dir
hac.allowlist_external_dirs = {hass.config.path("www"), *hac.media_dirs.values()}
if CONF_ALLOWLIST_EXTERNAL_DIRS in config:
hac.allowlist_external_dirs.update(set(config[CONF_ALLOWLIST_EXTERNAL_DIRS]))
elif LEGACY_CONF_WHITELIST_EXTERNAL_DIRS in config:
_LOGGER.warning(
"Key %s has been replaced with %s. Please update your config",
LEGACY_CONF_WHITELIST_EXTERNAL_DIRS,
CONF_ALLOWLIST_EXTERNAL_DIRS,
)
hac.allowlist_external_dirs.update(
set(config[LEGACY_CONF_WHITELIST_EXTERNAL_DIRS])
)
# Init whitelist external URL list make sure to add / to every URL that doesn't
# already have it so that we can properly test "path ownership"
if CONF_ALLOWLIST_EXTERNAL_URLS in config:
hac.allowlist_external_urls.update(
url if url.endswith("/") else f"{url}/"
for url in config[CONF_ALLOWLIST_EXTERNAL_URLS]
)
# Customize
cust_exact = dict(config[CONF_CUSTOMIZE])
cust_domain = dict(config[CONF_CUSTOMIZE_DOMAIN])
cust_glob = OrderedDict(config[CONF_CUSTOMIZE_GLOB])
for name, pkg in config[CONF_PACKAGES].items():
if (pkg_cust := pkg.get(HOMEASSISTANT_DOMAIN)) is None:
continue
try:
pkg_cust = _CUSTOMIZE_CONFIG_SCHEMA(pkg_cust)
except vol.Invalid:
_LOGGER.warning("Package %s contains invalid customize", name)
continue
cust_exact.update(pkg_cust[CONF_CUSTOMIZE])
cust_domain.update(pkg_cust[CONF_CUSTOMIZE_DOMAIN])
cust_glob.update(pkg_cust[CONF_CUSTOMIZE_GLOB])
hass.data[DATA_CUSTOMIZE] = EntityValues(cust_exact, cust_domain, cust_glob)
if CONF_UNIT_SYSTEM in config:
hac.units = get_unit_system(config[CONF_UNIT_SYSTEM])

View file

@ -13,7 +13,6 @@ import voluptuous as vol
from homeassistant import loader
from homeassistant.config import ( # type: ignore[attr-defined]
CONF_PACKAGES,
CORE_CONFIG_SCHEMA,
YAML_CONFIG_FILE,
config_per_platform,
extract_domain_configs,
@ -23,6 +22,7 @@ from homeassistant.config import ( # type: ignore[attr-defined]
merge_packages_config,
)
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.core_config import CORE_CONFIG_SCHEMA
from homeassistant.exceptions import HomeAssistantError
from homeassistant.requirements import (
RequirementsNotFound,

View file

@ -21,7 +21,6 @@ from typing import TYPE_CHECKING, Any, Final, Literal, NotRequired, TypedDict, f
from propcache import cached_property
import voluptuous as vol
from homeassistant.config import DATA_CUSTOMIZE
from homeassistant.const import (
ATTR_ASSUMED_STATE,
ATTR_ATTRIBUTION,
@ -49,6 +48,7 @@ from homeassistant.core import (
get_hassjob_callable_job_type,
get_release_channel,
)
from homeassistant.core_config import DATA_CUSTOMIZE
from homeassistant.exceptions import (
HomeAssistantError,
InvalidStateError,

View file

@ -10,7 +10,7 @@ from .model import Config, Integration
CONFIG_SCHEMA_IGNORE = {
# Configuration under the homeassistant key is a special case, it's handled by
# conf_util.async_process_ha_core_config already during bootstrapping, not by
# core_config.async_process_ha_core_config already during bootstrapping, not by
# a schema in the homeassistant integration.
HOMEASSISTANT_DOMAIN,
}

View file

@ -12,7 +12,6 @@ from homeassistant.components.cover import CoverDeviceClass, CoverEntityFeature
from homeassistant.components.media_player import MediaPlayerEntityFeature
from homeassistant.components.vacuum import VacuumEntityFeature
from homeassistant.components.valve import SERVICE_STOP_VALVE, ValveEntityFeature
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import (
SERVICE_CLOSE_VALVE,
SERVICE_OPEN_VALVE,
@ -20,6 +19,7 @@ from homeassistant.const import (
UnitOfTemperature,
)
from homeassistant.core import Context, Event, HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.helpers import entityfilter
from homeassistant.setup import async_setup_component
from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM

View file

@ -16,13 +16,13 @@ from homeassistant.components.camera.const import (
PREF_PRELOAD_STREAM,
)
from homeassistant.components.websocket_api import TYPE_RESULT
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import (
ATTR_ENTITY_ID,
EVENT_HOMEASSISTANT_STARTED,
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er, issue_registry as ir
from homeassistant.setup import async_setup_component

View file

@ -13,8 +13,8 @@ from homeassistant.components.camera.webrtc import (
async_register_webrtc_provider,
)
from homeassistant.components.websocket_api import TYPE_RESULT
from homeassistant.config import async_process_ha_core_config
from homeassistant.core import HomeAssistant, callback
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.setup import async_setup_component
from tests.typing import WebSocketGenerator

View file

@ -5,8 +5,8 @@ from unittest.mock import patch
import pytest
from homeassistant.components.cast import DOMAIN, home_assistant_cast
from homeassistant.config import async_process_ha_core_config
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.exceptions import HomeAssistantError
from tests.common import MockConfigEntry, async_mock_signal

View file

@ -27,13 +27,13 @@ from homeassistant.components.media_player import (
MediaClass,
MediaPlayerEntityFeature,
)
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import (
ATTR_ENTITY_ID,
CAST_APP_ID_HOMEASSISTANT_LOVELACE,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er, network
from homeassistant.helpers.dispatcher import (

View file

@ -25,9 +25,9 @@ from homeassistant.components.tts import (
DOMAIN as TTS_DOMAIN,
get_engine_instance,
)
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.entity_registry import EntityRegistry
from homeassistant.setup import async_setup_component

View file

@ -8,8 +8,8 @@ import pytest
from homeassistant import config_entries
from homeassistant.components import dialogflow, intent_script
from homeassistant.config import async_process_ha_core_config
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.setup import async_setup_component

View file

@ -32,9 +32,9 @@ from homeassistant.components.media_player import (
DOMAIN as DOMAIN_MP,
SERVICE_PLAY_MEDIA,
)
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import ATTR_ENTITY_ID, CONF_API_KEY
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core_config import async_process_ha_core_config
from .const import MOCK_MODELS, MOCK_VOICES

View file

@ -10,7 +10,6 @@ from homeassistant import config_entries
from homeassistant.components import zone
from homeassistant.components.device_tracker.legacy import Device
from homeassistant.components.geofency import CONF_MOBILE_BEACONS, DOMAIN
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import (
ATTR_LATITUDE,
ATTR_LONGITUDE,
@ -18,6 +17,7 @@ from homeassistant.const import (
STATE_NOT_HOME,
)
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component

View file

@ -15,8 +15,8 @@ from homeassistant.components.google_assistant.const import (
STORE_GOOGLE_LOCAL_WEBHOOK_ID,
)
from homeassistant.components.matter import MatterDeviceInfo
from homeassistant.config import async_process_ha_core_config
from homeassistant.core import HomeAssistant, State
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util

View file

@ -32,7 +32,6 @@ from homeassistant.components.google_assistant import (
smart_home as sh,
trait,
)
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
EVENT_CALL_SERVICE,
@ -41,6 +40,7 @@ from homeassistant.const import (
__version__,
)
from homeassistant.core import HomeAssistant, State
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.helpers import (
area_registry as ar,
device_registry as dr,

View file

@ -54,7 +54,6 @@ from homeassistant.components.media_player import (
from homeassistant.components.vacuum import VacuumEntityFeature
from homeassistant.components.valve import ValveEntityFeature
from homeassistant.components.water_heater import WaterHeaterEntityFeature
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import (
ATTR_ASSUMED_STATE,
ATTR_BATTERY_LEVEL,
@ -77,6 +76,7 @@ from homeassistant.const import (
UnitOfTemperature,
)
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, State
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.util import color, dt as dt_util
from homeassistant.util.unit_conversion import TemperatureConverter

View file

@ -14,9 +14,9 @@ import pytest
from homeassistant.components import tts
from homeassistant.components.google_translate.const import CONF_TLD, DOMAIN
from homeassistant.components.media_player import ATTR_MEDIA_CONTENT_ID
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry

View file

@ -11,9 +11,9 @@ from homeassistant.components import gpslogger, zone
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN
from homeassistant.components.device_tracker.legacy import Device
from homeassistant.components.gpslogger import DOMAIN, TRACKER_UPDATE
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import DATA_DISPATCHER

View file

@ -127,7 +127,7 @@ async def test_reload_core_conf(hass: HomeAssistant) -> None:
@patch("homeassistant.config.os.path.isfile", Mock(return_value=True))
@patch("homeassistant.components.homeassistant._LOGGER.error")
@patch("homeassistant.config.async_process_ha_core_config")
@patch("homeassistant.core_config.async_process_ha_core_config")
async def test_reload_core_with_wrong_conf(
mock_process, mock_error, hass: HomeAssistant
) -> None:

View file

@ -2,8 +2,8 @@
from homeassistant import config_entries
from homeassistant.components import ifttt
from homeassistant.config import async_process_ha_core_config
from homeassistant.core import HomeAssistant, callback
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.data_entry_flow import FlowResultType
from tests.typing import ClientSessionGenerator

View file

@ -7,8 +7,8 @@ import pytest
from homeassistant.components import konnected
from homeassistant.components.konnected import config_flow
from homeassistant.config import async_process_ha_core_config
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry

View file

@ -11,8 +11,8 @@ from homeassistant.components import locative
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN
from homeassistant.components.device_tracker.legacy import Device
from homeassistant.components.locative import DOMAIN, TRACKER_UPDATE
from homeassistant.config import async_process_ha_core_config
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.dispatcher import DATA_DISPATCHER
from homeassistant.setup import async_setup_component

View file

@ -8,8 +8,8 @@ import pytest
from homeassistant.components.lovelace import cast as lovelace_cast
from homeassistant.components.media_player import MediaClass
from homeassistant.config import async_process_ha_core_config
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component

View file

@ -8,9 +8,9 @@ import pytest
from homeassistant import config_entries
from homeassistant.components import mailgun, webhook
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import CONF_API_KEY, CONF_DOMAIN
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.setup import async_setup_component

View file

@ -7,8 +7,8 @@ import pytest
from homeassistant.components.media_player.browse_media import (
async_process_play_media_url,
)
from homeassistant.config import async_process_ha_core_config
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.network import NoURLAvailableError

View file

@ -11,8 +11,8 @@ import pytest
from homeassistant.components import media_source, websocket_api
from homeassistant.components.media_source import const
from homeassistant.config import async_process_ha_core_config
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.setup import async_setup_component
from tests.common import MockUser

View file

@ -8,9 +8,9 @@ import pytest
from homeassistant import config_entries
from homeassistant.components.met.const import DOMAIN, HOME_LOCATION_NAME
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.data_entry_flow import FlowResultType
from . import init_integration

View file

@ -7,9 +7,9 @@ from homeassistant.components.met.const import (
DEFAULT_HOME_LONGITUDE,
DOMAIN,
)
from homeassistant.config import async_process_ha_core_config
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.helpers import device_registry as dr
from . import init_integration

View file

@ -10,8 +10,8 @@ import pytest
from homeassistant.components import tts
from homeassistant.components.media_player import ATTR_MEDIA_CONTENT_ID
from homeassistant.components.microsoft.tts import SUPPORTED_LANGUAGES
from homeassistant.config import async_process_ha_core_config
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.exceptions import ServiceNotFound
from homeassistant.setup import async_setup_component

View file

@ -9,10 +9,10 @@ from motioneye_client.const import DEFAULT_PORT
from homeassistant.components.motioneye.const import DOMAIN
from homeassistant.components.motioneye.entity import get_motioneye_entity_unique_id
from homeassistant.config import async_process_ha_core_config
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry

View file

@ -8,9 +8,9 @@ from homeassistant import config_entries
from homeassistant.components.owntracks import config_flow
from homeassistant.components.owntracks.config_flow import CONF_CLOUDHOOK, CONF_SECRET
from homeassistant.components.owntracks.const import DOMAIN
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.setup import async_setup_component

View file

@ -4,8 +4,8 @@ from datetime import timedelta
from http import HTTPStatus
import io
from homeassistant.config import async_process_ha_core_config
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util

View file

@ -15,10 +15,10 @@ from homeassistant.components.reolink import (
NUM_CRED_ERRORS,
)
from homeassistant.components.reolink.const import DOMAIN
from homeassistant.config import async_process_ha_core_config
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_PORT, STATE_OFF, STATE_UNAVAILABLE, Platform
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,

View file

@ -12,6 +12,7 @@ from homeassistant import config as hass_config
from homeassistant.components.rest.const import DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_PACKAGES,
SERVICE_RELOAD,
STATE_UNAVAILABLE,
UnitOfInformation,
@ -468,7 +469,7 @@ async def test_config_schema_via_packages(hass: HomeAssistant) -> None:
"pack_11": {"rest": {"resource": "http://url1"}},
"pack_list": {"rest": [{"resource": "http://url2"}]},
}
config = {HOMEASSISTANT_DOMAIN: {hass_config.CONF_PACKAGES: packages}}
config = {HOMEASSISTANT_DOMAIN: {CONF_PACKAGES: packages}}
await hass_config.merge_packages_config(hass, config, packages)
assert len(config) == 2

View file

@ -38,7 +38,6 @@ from homeassistant.components.roku.const import (
)
from homeassistant.components.stream import FORMAT_CONTENT_TYPE, HLS_PROVIDER
from homeassistant.components.websocket_api import TYPE_RESULT
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_NAME,
@ -60,6 +59,7 @@ from homeassistant.const import (
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util

View file

@ -38,7 +38,6 @@ from homeassistant.components.smartthings.const import (
STORAGE_KEY,
STORAGE_VERSION,
)
from homeassistant.config import async_process_ha_core_config
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
from homeassistant.const import (
CONF_ACCESS_TOKEN,
@ -47,6 +46,7 @@ from homeassistant.const import (
CONF_WEBHOOK_ID,
)
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry

View file

@ -16,9 +16,9 @@ from homeassistant.components.smartthings.const import (
CONF_LOCATION_ID,
DOMAIN,
)
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry

View file

@ -23,8 +23,8 @@ from homeassistant.components.smartthings.const import (
PLATFORMS,
SIGNAL_SMARTTHINGS_UPDATE,
)
from homeassistant.config import async_process_ha_core_config
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_connect

View file

@ -7,10 +7,10 @@ import pytest
from toonapi import Agreement, ToonError
from homeassistant.components.toon.const import CONF_AGREEMENT, DOMAIN
from homeassistant.config import async_process_ha_core_config
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_entry_oauth2_flow
from homeassistant.setup import async_setup_component

View file

@ -11,9 +11,9 @@ from homeassistant.components import traccar, zone
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN
from homeassistant.components.device_tracker.legacy import Device
from homeassistant.components.traccar import DOMAIN, TRACKER_UPDATE
from homeassistant.config import async_process_ha_core_config
from homeassistant.const import STATE_HOME, STATE_NOT_HOME
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.dispatcher import DATA_DISPATCHER

View file

@ -10,9 +10,9 @@ from unittest.mock import MagicMock
import pytest
from homeassistant.config import async_process_ha_core_config
from homeassistant.config_entries import ConfigFlow
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from .common import (
DEFAULT_LANG,

View file

@ -9,8 +9,8 @@ from homeassistant.components.media_player import (
DOMAIN as DOMAIN_MP,
SERVICE_PLAY_MEDIA,
)
from homeassistant.config import async_process_ha_core_config
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.setup import async_setup_component
from .common import MockTTSEntity, mock_config_entry_setup

View file

@ -2,8 +2,8 @@
from homeassistant import config_entries
from homeassistant.components import twilio
from homeassistant.config import async_process_ha_core_config
from homeassistant.core import HomeAssistant, callback
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.data_entry_flow import FlowResultType
from tests.typing import ClientSessionGenerator

View file

@ -9,8 +9,8 @@ from aiohttp.test_utils import TestClient
import pytest
from homeassistant.components import webhook
from homeassistant.config import async_process_ha_core_config
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.setup import async_setup_component
from tests.typing import ClientSessionGenerator, WebSocketGenerator

View file

@ -10,8 +10,8 @@ from aiowithings import Activity, Device, Goals, MeasurementGroup, SleepSummary,
from freezegun.api import FrozenDateTimeFactory
from homeassistant.components.webhook import async_generate_url
from homeassistant.config import async_process_ha_core_config
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from tests.common import (
MockConfigEntry,

View file

@ -6,8 +6,8 @@ from unittest.mock import Mock, PropertyMock, patch
import pytest
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.config import async_process_ha_core_config
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.helpers import config_entry_flow
from tests.common import MockConfigEntry, MockModule, mock_integration, mock_platform

View file

@ -8,8 +8,8 @@ import pytest
from yarl import URL
from homeassistant.components import cloud
from homeassistant.config import async_process_ha_core_config
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from homeassistant.helpers.network import (
NoURLAvailableError,
_get_cloud_url,

View file

@ -4,63 +4,32 @@ import asyncio
from collections import OrderedDict
from collections.abc import Generator
import contextlib
import copy
import logging
import os
from pathlib import Path
from typing import Any
from unittest import mock
from unittest.mock import AsyncMock, MagicMock, Mock, patch
import pytest
from syrupy.assertion import SnapshotAssertion
import voluptuous as vol
from voluptuous import Invalid, MultipleInvalid
import yaml
from homeassistant import loader
import homeassistant.config as config_util
from homeassistant.const import (
ATTR_ASSUMED_STATE,
ATTR_FRIENDLY_NAME,
CONF_AUTH_MFA_MODULES,
CONF_AUTH_PROVIDERS,
CONF_CUSTOMIZE,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
CONF_PACKAGES,
__version__,
)
from homeassistant.core import (
DOMAIN as HOMEASSISTANT_DOMAIN,
ConfigSource,
HomeAssistant,
State,
)
from homeassistant.const import CONF_PACKAGES, __version__
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.exceptions import ConfigValidationError, HomeAssistantError
from homeassistant.helpers import (
check_config,
config_validation as cv,
issue_registry as ir,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers import check_config, config_validation as cv
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import Integration, async_get_integration
from homeassistant.setup import async_setup_component
from homeassistant.util import webrtc as webrtc_util
from homeassistant.util.unit_system import (
METRIC_SYSTEM,
US_CUSTOMARY_SYSTEM,
UnitSystem,
)
from homeassistant.util.yaml import SECRET_YAML
from homeassistant.util.yaml.objects import NodeDictClass
from .common import (
MockModule,
MockPlatform,
MockUser,
get_test_config_dir,
mock_integration,
mock_platform,
@ -510,198 +479,6 @@ async def test_create_default_config_returns_none_if_write_error(
assert mock_print.called
def test_core_config_schema() -> None:
"""Test core config schema."""
for value in (
{"unit_system": "K"},
{"time_zone": "non-exist"},
{"latitude": "91"},
{"longitude": -181},
{"external_url": "not an url"},
{"internal_url": "not an url"},
{"currency", 100},
{"customize": "bla"},
{"customize": {"light.sensor": 100}},
{"customize": {"entity_id": []}},
{"country": "xx"},
{"language": "xx"},
{"radius": -10},
{"webrtc": "bla"},
{"webrtc": {}},
):
with pytest.raises(MultipleInvalid):
config_util.CORE_CONFIG_SCHEMA(value)
config_util.CORE_CONFIG_SCHEMA(
{
"name": "Test name",
"latitude": "-23.45",
"longitude": "123.45",
"external_url": "https://www.example.com",
"internal_url": "http://example.local",
"unit_system": "metric",
"currency": "USD",
"customize": {"sensor.temperature": {"hidden": True}},
"country": "SE",
"language": "sv",
"radius": "10",
"webrtc": {"ice_servers": [{"url": "stun:custom_stun_server:3478"}]},
}
)
def test_core_config_schema_internal_external_warning(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that we warn for internal/external URL with path."""
config_util.CORE_CONFIG_SCHEMA(
{
"external_url": "https://www.example.com/bla",
"internal_url": "http://example.local/yo",
}
)
assert "Invalid external_url set" in caplog.text
assert "Invalid internal_url set" in caplog.text
def test_customize_dict_schema() -> None:
"""Test basic customize config validation."""
values = ({ATTR_FRIENDLY_NAME: None}, {ATTR_ASSUMED_STATE: "2"})
for val in values:
with pytest.raises(MultipleInvalid):
config_util.CUSTOMIZE_DICT_SCHEMA(val)
assert config_util.CUSTOMIZE_DICT_SCHEMA(
{ATTR_FRIENDLY_NAME: 2, ATTR_ASSUMED_STATE: "0"}
) == {ATTR_FRIENDLY_NAME: "2", ATTR_ASSUMED_STATE: False}
def test_webrtc_schema() -> None:
"""Test webrtc config validation."""
invalid_webrtc_configs = (
"bla",
{},
{"ice_servers": [], "unknown_key": 123},
{"ice_servers": [{}]},
{"ice_servers": [{"invalid_key": 123}]},
)
valid_webrtc_configs = (
(
{"ice_servers": []},
{"ice_servers": []},
),
(
{"ice_servers": {"url": "stun:custom_stun_server:3478"}},
{"ice_servers": [{"url": ["stun:custom_stun_server:3478"]}]},
),
(
{"ice_servers": [{"url": "stun:custom_stun_server:3478"}]},
{"ice_servers": [{"url": ["stun:custom_stun_server:3478"]}]},
),
(
{"ice_servers": [{"url": ["stun:custom_stun_server:3478"]}]},
{"ice_servers": [{"url": ["stun:custom_stun_server:3478"]}]},
),
(
{
"ice_servers": [
{
"url": ["stun:custom_stun_server:3478"],
"username": "bla",
"credential": "hunter2",
}
]
},
{
"ice_servers": [
{
"url": ["stun:custom_stun_server:3478"],
"username": "bla",
"credential": "hunter2",
}
]
},
),
)
for config in invalid_webrtc_configs:
with pytest.raises(MultipleInvalid):
config_util.CORE_CONFIG_SCHEMA({"webrtc": config})
for config, validated_webrtc in valid_webrtc_configs:
validated = config_util.CORE_CONFIG_SCHEMA({"webrtc": config})
assert validated["webrtc"] == validated_webrtc
def test_validate_stun_or_turn_url() -> None:
"""Test _validate_stun_or_turn_url."""
invalid_urls = (
"custom_stun_server",
"custom_stun_server:3478",
"bum:custom_stun_server:3478" "http://blah.com:80",
)
valid_urls = (
"stun:custom_stun_server:3478",
"turn:custom_stun_server:3478",
"stuns:custom_stun_server:3478",
"turns:custom_stun_server:3478",
# The validator does not reject urls with path
"stun:custom_stun_server:3478/path",
"turn:custom_stun_server:3478/path",
"stuns:custom_stun_server:3478/path",
"turns:custom_stun_server:3478/path",
# The validator allows any query
"stun:custom_stun_server:3478?query",
"turn:custom_stun_server:3478?query",
"stuns:custom_stun_server:3478?query",
"turns:custom_stun_server:3478?query",
)
for url in invalid_urls:
with pytest.raises(Invalid):
config_util._validate_stun_or_turn_url(url)
for url in valid_urls:
assert config_util._validate_stun_or_turn_url(url) == url
def test_customize_glob_is_ordered() -> None:
"""Test that customize_glob preserves order."""
conf = config_util.CORE_CONFIG_SCHEMA({"customize_glob": OrderedDict()})
assert isinstance(conf["customize_glob"], OrderedDict)
async def _compute_state(hass: HomeAssistant, config: dict[str, Any]) -> State | None:
await config_util.async_process_ha_core_config(hass, config)
entity = Entity()
entity.entity_id = "test.test"
entity.hass = hass
entity.schedule_update_ha_state()
await hass.async_block_till_done()
return hass.states.get("test.test")
async def test_entity_customization(hass: HomeAssistant) -> None:
"""Test entity customization through configuration."""
config = {
CONF_LATITUDE: 50,
CONF_LONGITUDE: 50,
CONF_NAME: "Test",
CONF_CUSTOMIZE: {"test.test": {"hidden": True}},
}
state = await _compute_state(hass, config)
assert state.attributes["hidden"]
@patch("homeassistant.config.shutil")
@patch("homeassistant.config.os")
@patch("homeassistant.config.is_docker_env", return_value=False)
@ -791,365 +568,6 @@ def test_config_upgrade_no_file(hass: HomeAssistant) -> None:
assert opened_file.write.call_args == mock.call(__version__)
async def test_loading_configuration_from_storage(
hass: HomeAssistant, hass_storage: dict[str, Any]
) -> None:
"""Test loading core config onto hass object."""
hass_storage["core.config"] = {
"data": {
"elevation": 10,
"latitude": 55,
"location_name": "Home",
"longitude": 13,
"time_zone": "Europe/Copenhagen",
"unit_system": "metric",
"external_url": "https://www.example.com",
"internal_url": "http://example.local",
"currency": "EUR",
"country": "SE",
"language": "sv",
"radius": 150,
},
"key": "core.config",
"version": 1,
"minor_version": 4,
}
await config_util.async_process_ha_core_config(
hass, {"allowlist_external_dirs": "/etc"}
)
assert hass.config.latitude == 55
assert hass.config.longitude == 13
assert hass.config.elevation == 10
assert hass.config.location_name == "Home"
assert hass.config.units is METRIC_SYSTEM
assert hass.config.time_zone == "Europe/Copenhagen"
assert hass.config.external_url == "https://www.example.com"
assert hass.config.internal_url == "http://example.local"
assert hass.config.currency == "EUR"
assert hass.config.country == "SE"
assert hass.config.language == "sv"
assert hass.config.radius == 150
assert len(hass.config.allowlist_external_dirs) == 3
assert "/etc" in hass.config.allowlist_external_dirs
assert hass.config.config_source is ConfigSource.STORAGE
async def test_loading_configuration_from_storage_with_yaml_only(
hass: HomeAssistant, hass_storage: dict[str, Any]
) -> None:
"""Test loading core and YAML config onto hass object."""
hass_storage["core.config"] = {
"data": {
"elevation": 10,
"latitude": 55,
"location_name": "Home",
"longitude": 13,
"time_zone": "Europe/Copenhagen",
"unit_system": "metric",
},
"key": "core.config",
"version": 1,
}
await config_util.async_process_ha_core_config(
hass, {"media_dirs": {"mymedia": "/usr"}, "allowlist_external_dirs": "/etc"}
)
assert hass.config.latitude == 55
assert hass.config.longitude == 13
assert hass.config.elevation == 10
assert hass.config.location_name == "Home"
assert hass.config.units is METRIC_SYSTEM
assert hass.config.time_zone == "Europe/Copenhagen"
assert len(hass.config.allowlist_external_dirs) == 3
assert "/etc" in hass.config.allowlist_external_dirs
assert hass.config.media_dirs == {"mymedia": "/usr"}
assert hass.config.config_source is ConfigSource.STORAGE
async def test_migration_and_updating_configuration(
hass: HomeAssistant, hass_storage: dict[str, Any]
) -> None:
"""Test updating configuration stores the new configuration."""
core_data = {
"data": {
"elevation": 10,
"latitude": 55,
"location_name": "Home",
"longitude": 13,
"time_zone": "Europe/Copenhagen",
"unit_system": "imperial",
"external_url": "https://www.example.com",
"internal_url": "http://example.local",
"currency": "BTC",
},
"key": "core.config",
"version": 1,
"minor_version": 1,
}
hass_storage["core.config"] = dict(core_data)
await config_util.async_process_ha_core_config(
hass, {"allowlist_external_dirs": "/etc"}
)
await hass.config.async_update(latitude=50, currency="USD")
expected_new_core_data = copy.deepcopy(core_data)
# From async_update above
expected_new_core_data["data"]["latitude"] = 50
expected_new_core_data["data"]["currency"] = "USD"
# 1.1 -> 1.2 store migration with migrated unit system
expected_new_core_data["data"]["unit_system_v2"] = "us_customary"
# 1.1 -> 1.3 defaults for country and language
expected_new_core_data["data"]["country"] = None
expected_new_core_data["data"]["language"] = "en"
# 1.1 -> 1.4 defaults for zone radius
expected_new_core_data["data"]["radius"] = 100
# Bumped minor version
expected_new_core_data["minor_version"] = 4
assert hass_storage["core.config"] == expected_new_core_data
assert hass.config.latitude == 50
assert hass.config.currency == "USD"
assert hass.config.country is None
assert hass.config.language == "en"
assert hass.config.radius == 100
async def test_override_stored_configuration(
hass: HomeAssistant, hass_storage: dict[str, Any]
) -> None:
"""Test loading core and YAML config onto hass object."""
hass_storage["core.config"] = {
"data": {
"elevation": 10,
"latitude": 55,
"location_name": "Home",
"longitude": 13,
"time_zone": "Europe/Copenhagen",
"unit_system": "metric",
},
"key": "core.config",
"version": 1,
}
await config_util.async_process_ha_core_config(
hass, {"latitude": 60, "allowlist_external_dirs": "/etc"}
)
assert hass.config.latitude == 60
assert hass.config.longitude == 13
assert hass.config.elevation == 10
assert hass.config.location_name == "Home"
assert hass.config.units is METRIC_SYSTEM
assert hass.config.time_zone == "Europe/Copenhagen"
assert len(hass.config.allowlist_external_dirs) == 3
assert "/etc" in hass.config.allowlist_external_dirs
assert hass.config.config_source is ConfigSource.YAML
async def test_loading_configuration(hass: HomeAssistant) -> None:
"""Test loading core config onto hass object."""
await config_util.async_process_ha_core_config(
hass,
{
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
"unit_system": "imperial",
"time_zone": "America/New_York",
"allowlist_external_dirs": "/etc",
"external_url": "https://www.example.com",
"internal_url": "http://example.local",
"media_dirs": {"mymedia": "/usr"},
"debug": True,
"currency": "EUR",
"country": "SE",
"language": "sv",
"radius": 150,
"webrtc": {"ice_servers": [{"url": "stun:custom_stun_server:3478"}]},
},
)
assert hass.config.latitude == 60
assert hass.config.longitude == 50
assert hass.config.elevation == 25
assert hass.config.location_name == "Huis"
assert hass.config.units is US_CUSTOMARY_SYSTEM
assert hass.config.time_zone == "America/New_York"
assert hass.config.external_url == "https://www.example.com"
assert hass.config.internal_url == "http://example.local"
assert len(hass.config.allowlist_external_dirs) == 3
assert "/etc" in hass.config.allowlist_external_dirs
assert "/usr" in hass.config.allowlist_external_dirs
assert hass.config.media_dirs == {"mymedia": "/usr"}
assert hass.config.config_source is ConfigSource.YAML
assert hass.config.debug is True
assert hass.config.currency == "EUR"
assert hass.config.country == "SE"
assert hass.config.language == "sv"
assert hass.config.radius == 150
assert hass.config.webrtc == webrtc_util.RTCConfiguration(
[webrtc_util.RTCIceServer(urls=["stun:custom_stun_server:3478"])]
)
@pytest.mark.parametrize(
("minor_version", "users", "user_data", "default_language"),
[
(2, (), {}, "en"),
(2, ({"is_owner": True},), {}, "en"),
(
2,
({"id": "user1", "is_owner": True},),
{"user1": {"language": {"language": "sv"}}},
"sv",
),
(
2,
({"id": "user1", "is_owner": False},),
{"user1": {"language": {"language": "sv"}}},
"en",
),
(3, (), {}, "en"),
(3, ({"is_owner": True},), {}, "en"),
(
3,
({"id": "user1", "is_owner": True},),
{"user1": {"language": {"language": "sv"}}},
"en",
),
(
3,
({"id": "user1", "is_owner": False},),
{"user1": {"language": {"language": "sv"}}},
"en",
),
],
)
async def test_language_default(
hass: HomeAssistant,
hass_storage: dict[str, Any],
minor_version,
users,
user_data,
default_language,
) -> None:
"""Test language config default to owner user's language during migration.
This should only happen if the core store version < 1.3
"""
core_data = {
"data": {},
"key": "core.config",
"version": 1,
"minor_version": minor_version,
}
hass_storage["core.config"] = dict(core_data)
for user_config in users:
user = MockUser(**user_config).add_to_hass(hass)
if user.id not in user_data:
continue
storage_key = f"frontend.user_data_{user.id}"
hass_storage[storage_key] = {
"key": storage_key,
"version": 1,
"data": user_data[user.id],
}
await config_util.async_process_ha_core_config(
hass,
{},
)
assert hass.config.language == default_language
async def test_loading_configuration_default_media_dirs_docker(
hass: HomeAssistant,
) -> None:
"""Test loading core config onto hass object."""
with patch("homeassistant.config.is_docker_env", return_value=True):
await config_util.async_process_ha_core_config(
hass,
{
"name": "Huis",
},
)
assert hass.config.location_name == "Huis"
assert len(hass.config.allowlist_external_dirs) == 2
assert "/media" in hass.config.allowlist_external_dirs
assert hass.config.media_dirs == {"local": "/media"}
async def test_loading_configuration_from_packages(hass: HomeAssistant) -> None:
"""Test loading packages config onto hass object config."""
await config_util.async_process_ha_core_config(
hass,
{
"latitude": 39,
"longitude": -1,
"elevation": 500,
"name": "Huis",
"unit_system": "metric",
"time_zone": "Europe/Madrid",
"external_url": "https://www.example.com",
"internal_url": "http://example.local",
"packages": {
"package_1": {"wake_on_lan": None},
"package_2": {
"light": {"platform": "hue"},
"media_extractor": None,
"sun": None,
},
},
},
)
# Empty packages not allowed
with pytest.raises(MultipleInvalid):
await config_util.async_process_ha_core_config(
hass,
{
"latitude": 39,
"longitude": -1,
"elevation": 500,
"name": "Huis",
"unit_system": "metric",
"time_zone": "Europe/Madrid",
"packages": {"empty_package": None},
},
)
@pytest.mark.parametrize(
("unit_system_name", "expected_unit_system"),
[
("metric", METRIC_SYSTEM),
("imperial", US_CUSTOMARY_SYSTEM),
("us_customary", US_CUSTOMARY_SYSTEM),
],
)
async def test_loading_configuration_unit_system(
hass: HomeAssistant, unit_system_name: str, expected_unit_system: UnitSystem
) -> None:
"""Test backward compatibility when loading core config."""
await config_util.async_process_ha_core_config(
hass,
{
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
"unit_system": unit_system_name,
"time_zone": "America/New_York",
"external_url": "https://www.example.com",
"internal_url": "http://example.local",
},
)
assert hass.config.units is expected_unit_system
@patch("homeassistant.helpers.check_config.async_check_ha_config_file")
async def test_check_ha_config_file_correct(mock_check, hass: HomeAssistant) -> None:
"""Check that restart propagates to stop."""
@ -1401,148 +819,6 @@ async def test_merge_duplicate_keys(
assert len(config["input_select"]) == 1
async def test_merge_customize(hass: HomeAssistant) -> None:
"""Test loading core config onto hass object."""
core_config = {
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
"unit_system": "imperial",
"time_zone": "GMT",
"customize": {"a.a": {"friendly_name": "A"}},
"packages": {
"pkg1": {"homeassistant": {"customize": {"b.b": {"friendly_name": "BB"}}}}
},
}
await config_util.async_process_ha_core_config(hass, core_config)
assert hass.data[config_util.DATA_CUSTOMIZE].get("b.b") == {"friendly_name": "BB"}
async def test_auth_provider_config(hass: HomeAssistant) -> None:
"""Test loading auth provider config onto hass object."""
core_config = {
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
"unit_system": "imperial",
"time_zone": "GMT",
CONF_AUTH_PROVIDERS: [
{"type": "homeassistant"},
],
CONF_AUTH_MFA_MODULES: [{"type": "totp"}, {"type": "totp", "id": "second"}],
}
if hasattr(hass, "auth"):
del hass.auth
await config_util.async_process_ha_core_config(hass, core_config)
assert len(hass.auth.auth_providers) == 1
assert hass.auth.auth_providers[0].type == "homeassistant"
assert len(hass.auth.auth_mfa_modules) == 2
assert hass.auth.auth_mfa_modules[0].id == "totp"
assert hass.auth.auth_mfa_modules[1].id == "second"
async def test_auth_provider_config_default(hass: HomeAssistant) -> None:
"""Test loading default auth provider config."""
core_config = {
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
"unit_system": "imperial",
"time_zone": "GMT",
}
if hasattr(hass, "auth"):
del hass.auth
await config_util.async_process_ha_core_config(hass, core_config)
assert len(hass.auth.auth_providers) == 1
assert hass.auth.auth_providers[0].type == "homeassistant"
assert len(hass.auth.auth_mfa_modules) == 1
assert hass.auth.auth_mfa_modules[0].id == "totp"
async def test_disallowed_auth_provider_config(hass: HomeAssistant) -> None:
"""Test loading insecure example auth provider is disallowed."""
core_config = {
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
"unit_system": "imperial",
"time_zone": "GMT",
CONF_AUTH_PROVIDERS: [
{
"type": "insecure_example",
"users": [
{
"username": "test-user",
"password": "test-pass",
"name": "Test Name",
}
],
}
],
}
with pytest.raises(Invalid):
await config_util.async_process_ha_core_config(hass, core_config)
async def test_disallowed_duplicated_auth_provider_config(hass: HomeAssistant) -> None:
"""Test loading insecure example auth provider is disallowed."""
core_config = {
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
"unit_system": "imperial",
"time_zone": "GMT",
CONF_AUTH_PROVIDERS: [{"type": "homeassistant"}, {"type": "homeassistant"}],
}
with pytest.raises(Invalid):
await config_util.async_process_ha_core_config(hass, core_config)
async def test_disallowed_auth_mfa_module_config(hass: HomeAssistant) -> None:
"""Test loading insecure example auth mfa module is disallowed."""
core_config = {
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
"unit_system": "imperial",
"time_zone": "GMT",
CONF_AUTH_MFA_MODULES: [
{
"type": "insecure_example",
"data": [{"user_id": "mock-user", "pin": "test-pin"}],
}
],
}
with pytest.raises(Invalid):
await config_util.async_process_ha_core_config(hass, core_config)
async def test_disallowed_duplicated_auth_mfa_module_config(
hass: HomeAssistant,
) -> None:
"""Test loading insecure example auth mfa module is disallowed."""
core_config = {
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
"unit_system": "imperial",
"time_zone": "GMT",
CONF_AUTH_MFA_MODULES: [{"type": "totp"}, {"type": "totp"}],
}
with pytest.raises(Invalid):
await config_util.async_process_ha_core_config(hass, core_config)
async def test_merge_split_component_definition(hass: HomeAssistant) -> None:
"""Test components with trailing description in packages are merged."""
packages = {
@ -2094,74 +1370,6 @@ def test_identify_config_schema(domain, schema, expected) -> None:
)
async def test_core_config_schema_historic_currency(
hass: HomeAssistant, issue_registry: ir.IssueRegistry
) -> None:
"""Test core config schema."""
await config_util.async_process_ha_core_config(hass, {"currency": "LTT"})
issue = issue_registry.async_get_issue("homeassistant", "historic_currency")
assert issue
assert issue.translation_placeholders == {"currency": "LTT"}
async def test_core_store_historic_currency(
hass: HomeAssistant, hass_storage: dict[str, Any], issue_registry: ir.IssueRegistry
) -> None:
"""Test core config store."""
core_data = {
"data": {
"currency": "LTT",
},
"key": "core.config",
"version": 1,
"minor_version": 1,
}
hass_storage["core.config"] = dict(core_data)
await config_util.async_process_ha_core_config(hass, {})
issue_id = "historic_currency"
issue = issue_registry.async_get_issue("homeassistant", issue_id)
assert issue
assert issue.translation_placeholders == {"currency": "LTT"}
await hass.config.async_update(currency="EUR")
issue = issue_registry.async_get_issue("homeassistant", issue_id)
assert not issue
async def test_core_config_schema_no_country(
hass: HomeAssistant, issue_registry: ir.IssueRegistry
) -> None:
"""Test core config schema."""
await config_util.async_process_ha_core_config(hass, {})
issue = issue_registry.async_get_issue("homeassistant", "country_not_configured")
assert issue
async def test_core_store_no_country(
hass: HomeAssistant, hass_storage: dict[str, Any], issue_registry: ir.IssueRegistry
) -> None:
"""Test core config store."""
core_data = {
"data": {},
"key": "core.config",
"version": 1,
"minor_version": 1,
}
hass_storage["core.config"] = dict(core_data)
await config_util.async_process_ha_core_config(hass, {})
issue_id = "country_not_configured"
issue = issue_registry.async_get_issue("homeassistant", issue_id)
assert issue
await hass.config.async_update(country="SE")
issue = issue_registry.async_get_issue("homeassistant", issue_id)
assert not issue
async def test_safe_mode(hass: HomeAssistant) -> None:
"""Test safe mode."""
assert config_util.safe_mode_enabled(hass.config.config_dir) is False
@ -2581,30 +1789,3 @@ async def test_loading_platforms_gathers(hass: HomeAssistant) -> None:
("platform_int", "sensor"),
("platform_int2", "sensor"),
]
async def test_configuration_legacy_template_is_removed(hass: HomeAssistant) -> None:
"""Test loading core config onto hass object."""
await config_util.async_process_ha_core_config(
hass,
{
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
"unit_system": "imperial",
"time_zone": "America/New_York",
"allowlist_external_dirs": "/etc",
"external_url": "https://www.example.com",
"internal_url": "http://example.local",
"media_dirs": {"mymedia": "/usr"},
"legacy_templates": True,
"debug": True,
"currency": "EUR",
"country": "SE",
"language": "sv",
"radius": 150,
},
)
assert not getattr(hass.config, "legacy_templates")

823
tests/test_core_config.py Normal file
View file

@ -0,0 +1,823 @@
"""Test core_config."""
from collections import OrderedDict
import copy
from typing import Any
from unittest.mock import patch
import pytest
from voluptuous import Invalid, MultipleInvalid
from homeassistant.const import (
ATTR_ASSUMED_STATE,
ATTR_FRIENDLY_NAME,
CONF_AUTH_MFA_MODULES,
CONF_AUTH_PROVIDERS,
CONF_CUSTOMIZE,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
)
from homeassistant.core import ConfigSource, HomeAssistant, State
from homeassistant.core_config import (
_CUSTOMIZE_DICT_SCHEMA,
CORE_CONFIG_SCHEMA,
DATA_CUSTOMIZE,
_validate_stun_or_turn_url,
async_process_ha_core_config,
)
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.entity import Entity
from homeassistant.util import webrtc as webrtc_util
from homeassistant.util.unit_system import (
METRIC_SYSTEM,
US_CUSTOMARY_SYSTEM,
UnitSystem,
)
from .common import MockUser
def test_core_config_schema() -> None:
"""Test core config schema."""
for value in (
{"unit_system": "K"},
{"time_zone": "non-exist"},
{"latitude": "91"},
{"longitude": -181},
{"external_url": "not an url"},
{"internal_url": "not an url"},
{"currency", 100},
{"customize": "bla"},
{"customize": {"light.sensor": 100}},
{"customize": {"entity_id": []}},
{"country": "xx"},
{"language": "xx"},
{"radius": -10},
{"webrtc": "bla"},
{"webrtc": {}},
):
with pytest.raises(MultipleInvalid):
CORE_CONFIG_SCHEMA(value)
CORE_CONFIG_SCHEMA(
{
"name": "Test name",
"latitude": "-23.45",
"longitude": "123.45",
"external_url": "https://www.example.com",
"internal_url": "http://example.local",
"unit_system": "metric",
"currency": "USD",
"customize": {"sensor.temperature": {"hidden": True}},
"country": "SE",
"language": "sv",
"radius": "10",
"webrtc": {"ice_servers": [{"url": "stun:custom_stun_server:3478"}]},
}
)
def test_core_config_schema_internal_external_warning(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that we warn for internal/external URL with path."""
CORE_CONFIG_SCHEMA(
{
"external_url": "https://www.example.com/bla",
"internal_url": "http://example.local/yo",
}
)
assert "Invalid external_url set" in caplog.text
assert "Invalid internal_url set" in caplog.text
def test_customize_dict_schema() -> None:
"""Test basic customize config validation."""
values = ({ATTR_FRIENDLY_NAME: None}, {ATTR_ASSUMED_STATE: "2"})
for val in values:
with pytest.raises(MultipleInvalid):
_CUSTOMIZE_DICT_SCHEMA(val)
assert _CUSTOMIZE_DICT_SCHEMA({ATTR_FRIENDLY_NAME: 2, ATTR_ASSUMED_STATE: "0"}) == {
ATTR_FRIENDLY_NAME: "2",
ATTR_ASSUMED_STATE: False,
}
def test_webrtc_schema() -> None:
"""Test webrtc config validation."""
invalid_webrtc_configs = (
"bla",
{},
{"ice_servers": [], "unknown_key": 123},
{"ice_servers": [{}]},
{"ice_servers": [{"invalid_key": 123}]},
)
valid_webrtc_configs = (
(
{"ice_servers": []},
{"ice_servers": []},
),
(
{"ice_servers": {"url": "stun:custom_stun_server:3478"}},
{"ice_servers": [{"url": ["stun:custom_stun_server:3478"]}]},
),
(
{"ice_servers": [{"url": "stun:custom_stun_server:3478"}]},
{"ice_servers": [{"url": ["stun:custom_stun_server:3478"]}]},
),
(
{"ice_servers": [{"url": ["stun:custom_stun_server:3478"]}]},
{"ice_servers": [{"url": ["stun:custom_stun_server:3478"]}]},
),
(
{
"ice_servers": [
{
"url": ["stun:custom_stun_server:3478"],
"username": "bla",
"credential": "hunter2",
}
]
},
{
"ice_servers": [
{
"url": ["stun:custom_stun_server:3478"],
"username": "bla",
"credential": "hunter2",
}
]
},
),
)
for config in invalid_webrtc_configs:
with pytest.raises(MultipleInvalid):
CORE_CONFIG_SCHEMA({"webrtc": config})
for config, validated_webrtc in valid_webrtc_configs:
validated = CORE_CONFIG_SCHEMA({"webrtc": config})
assert validated["webrtc"] == validated_webrtc
def test_validate_stun_or_turn_url() -> None:
"""Test _validate_stun_or_turn_url."""
invalid_urls = (
"custom_stun_server",
"custom_stun_server:3478",
"bum:custom_stun_server:3478" "http://blah.com:80",
)
valid_urls = (
"stun:custom_stun_server:3478",
"turn:custom_stun_server:3478",
"stuns:custom_stun_server:3478",
"turns:custom_stun_server:3478",
# The validator does not reject urls with path
"stun:custom_stun_server:3478/path",
"turn:custom_stun_server:3478/path",
"stuns:custom_stun_server:3478/path",
"turns:custom_stun_server:3478/path",
# The validator allows any query
"stun:custom_stun_server:3478?query",
"turn:custom_stun_server:3478?query",
"stuns:custom_stun_server:3478?query",
"turns:custom_stun_server:3478?query",
)
for url in invalid_urls:
with pytest.raises(Invalid):
_validate_stun_or_turn_url(url)
for url in valid_urls:
assert _validate_stun_or_turn_url(url) == url
def test_customize_glob_is_ordered() -> None:
"""Test that customize_glob preserves order."""
conf = CORE_CONFIG_SCHEMA({"customize_glob": OrderedDict()})
assert isinstance(conf["customize_glob"], OrderedDict)
async def _compute_state(hass: HomeAssistant, config: dict[str, Any]) -> State | None:
await async_process_ha_core_config(hass, config)
entity = Entity()
entity.entity_id = "test.test"
entity.hass = hass
entity.schedule_update_ha_state()
await hass.async_block_till_done()
return hass.states.get("test.test")
async def test_entity_customization(hass: HomeAssistant) -> None:
"""Test entity customization through configuration."""
config = {
CONF_LATITUDE: 50,
CONF_LONGITUDE: 50,
CONF_NAME: "Test",
CONF_CUSTOMIZE: {"test.test": {"hidden": True}},
}
state = await _compute_state(hass, config)
assert state.attributes["hidden"]
async def test_loading_configuration_from_storage(
hass: HomeAssistant, hass_storage: dict[str, Any]
) -> None:
"""Test loading core config onto hass object."""
hass_storage["core.config"] = {
"data": {
"elevation": 10,
"latitude": 55,
"location_name": "Home",
"longitude": 13,
"time_zone": "Europe/Copenhagen",
"unit_system": "metric",
"external_url": "https://www.example.com",
"internal_url": "http://example.local",
"currency": "EUR",
"country": "SE",
"language": "sv",
"radius": 150,
},
"key": "core.config",
"version": 1,
"minor_version": 4,
}
await async_process_ha_core_config(hass, {"allowlist_external_dirs": "/etc"})
assert hass.config.latitude == 55
assert hass.config.longitude == 13
assert hass.config.elevation == 10
assert hass.config.location_name == "Home"
assert hass.config.units is METRIC_SYSTEM
assert hass.config.time_zone == "Europe/Copenhagen"
assert hass.config.external_url == "https://www.example.com"
assert hass.config.internal_url == "http://example.local"
assert hass.config.currency == "EUR"
assert hass.config.country == "SE"
assert hass.config.language == "sv"
assert hass.config.radius == 150
assert len(hass.config.allowlist_external_dirs) == 3
assert "/etc" in hass.config.allowlist_external_dirs
assert hass.config.config_source is ConfigSource.STORAGE
async def test_loading_configuration_from_storage_with_yaml_only(
hass: HomeAssistant, hass_storage: dict[str, Any]
) -> None:
"""Test loading core and YAML config onto hass object."""
hass_storage["core.config"] = {
"data": {
"elevation": 10,
"latitude": 55,
"location_name": "Home",
"longitude": 13,
"time_zone": "Europe/Copenhagen",
"unit_system": "metric",
},
"key": "core.config",
"version": 1,
}
await async_process_ha_core_config(
hass, {"media_dirs": {"mymedia": "/usr"}, "allowlist_external_dirs": "/etc"}
)
assert hass.config.latitude == 55
assert hass.config.longitude == 13
assert hass.config.elevation == 10
assert hass.config.location_name == "Home"
assert hass.config.units is METRIC_SYSTEM
assert hass.config.time_zone == "Europe/Copenhagen"
assert len(hass.config.allowlist_external_dirs) == 3
assert "/etc" in hass.config.allowlist_external_dirs
assert hass.config.media_dirs == {"mymedia": "/usr"}
assert hass.config.config_source is ConfigSource.STORAGE
async def test_migration_and_updating_configuration(
hass: HomeAssistant, hass_storage: dict[str, Any]
) -> None:
"""Test updating configuration stores the new configuration."""
core_data = {
"data": {
"elevation": 10,
"latitude": 55,
"location_name": "Home",
"longitude": 13,
"time_zone": "Europe/Copenhagen",
"unit_system": "imperial",
"external_url": "https://www.example.com",
"internal_url": "http://example.local",
"currency": "BTC",
},
"key": "core.config",
"version": 1,
"minor_version": 1,
}
hass_storage["core.config"] = dict(core_data)
await async_process_ha_core_config(hass, {"allowlist_external_dirs": "/etc"})
await hass.config.async_update(latitude=50, currency="USD")
expected_new_core_data = copy.deepcopy(core_data)
# From async_update above
expected_new_core_data["data"]["latitude"] = 50
expected_new_core_data["data"]["currency"] = "USD"
# 1.1 -> 1.2 store migration with migrated unit system
expected_new_core_data["data"]["unit_system_v2"] = "us_customary"
# 1.1 -> 1.3 defaults for country and language
expected_new_core_data["data"]["country"] = None
expected_new_core_data["data"]["language"] = "en"
# 1.1 -> 1.4 defaults for zone radius
expected_new_core_data["data"]["radius"] = 100
# Bumped minor version
expected_new_core_data["minor_version"] = 4
assert hass_storage["core.config"] == expected_new_core_data
assert hass.config.latitude == 50
assert hass.config.currency == "USD"
assert hass.config.country is None
assert hass.config.language == "en"
assert hass.config.radius == 100
async def test_override_stored_configuration(
hass: HomeAssistant, hass_storage: dict[str, Any]
) -> None:
"""Test loading core and YAML config onto hass object."""
hass_storage["core.config"] = {
"data": {
"elevation": 10,
"latitude": 55,
"location_name": "Home",
"longitude": 13,
"time_zone": "Europe/Copenhagen",
"unit_system": "metric",
},
"key": "core.config",
"version": 1,
}
await async_process_ha_core_config(
hass, {"latitude": 60, "allowlist_external_dirs": "/etc"}
)
assert hass.config.latitude == 60
assert hass.config.longitude == 13
assert hass.config.elevation == 10
assert hass.config.location_name == "Home"
assert hass.config.units is METRIC_SYSTEM
assert hass.config.time_zone == "Europe/Copenhagen"
assert len(hass.config.allowlist_external_dirs) == 3
assert "/etc" in hass.config.allowlist_external_dirs
assert hass.config.config_source is ConfigSource.YAML
async def test_loading_configuration(hass: HomeAssistant) -> None:
"""Test loading core config onto hass object."""
await async_process_ha_core_config(
hass,
{
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
"unit_system": "imperial",
"time_zone": "America/New_York",
"allowlist_external_dirs": "/etc",
"external_url": "https://www.example.com",
"internal_url": "http://example.local",
"media_dirs": {"mymedia": "/usr"},
"debug": True,
"currency": "EUR",
"country": "SE",
"language": "sv",
"radius": 150,
"webrtc": {"ice_servers": [{"url": "stun:custom_stun_server:3478"}]},
},
)
assert hass.config.latitude == 60
assert hass.config.longitude == 50
assert hass.config.elevation == 25
assert hass.config.location_name == "Huis"
assert hass.config.units is US_CUSTOMARY_SYSTEM
assert hass.config.time_zone == "America/New_York"
assert hass.config.external_url == "https://www.example.com"
assert hass.config.internal_url == "http://example.local"
assert len(hass.config.allowlist_external_dirs) == 3
assert "/etc" in hass.config.allowlist_external_dirs
assert "/usr" in hass.config.allowlist_external_dirs
assert hass.config.media_dirs == {"mymedia": "/usr"}
assert hass.config.config_source is ConfigSource.YAML
assert hass.config.debug is True
assert hass.config.currency == "EUR"
assert hass.config.country == "SE"
assert hass.config.language == "sv"
assert hass.config.radius == 150
assert hass.config.webrtc == webrtc_util.RTCConfiguration(
[webrtc_util.RTCIceServer(urls=["stun:custom_stun_server:3478"])]
)
@pytest.mark.parametrize(
("minor_version", "users", "user_data", "default_language"),
[
(2, (), {}, "en"),
(2, ({"is_owner": True},), {}, "en"),
(
2,
({"id": "user1", "is_owner": True},),
{"user1": {"language": {"language": "sv"}}},
"sv",
),
(
2,
({"id": "user1", "is_owner": False},),
{"user1": {"language": {"language": "sv"}}},
"en",
),
(3, (), {}, "en"),
(3, ({"is_owner": True},), {}, "en"),
(
3,
({"id": "user1", "is_owner": True},),
{"user1": {"language": {"language": "sv"}}},
"en",
),
(
3,
({"id": "user1", "is_owner": False},),
{"user1": {"language": {"language": "sv"}}},
"en",
),
],
)
async def test_language_default(
hass: HomeAssistant,
hass_storage: dict[str, Any],
minor_version,
users,
user_data,
default_language,
) -> None:
"""Test language config default to owner user's language during migration.
This should only happen if the core store version < 1.3
"""
core_data = {
"data": {},
"key": "core.config",
"version": 1,
"minor_version": minor_version,
}
hass_storage["core.config"] = dict(core_data)
for user_config in users:
user = MockUser(**user_config).add_to_hass(hass)
if user.id not in user_data:
continue
storage_key = f"frontend.user_data_{user.id}"
hass_storage[storage_key] = {
"key": storage_key,
"version": 1,
"data": user_data[user.id],
}
await async_process_ha_core_config(
hass,
{},
)
assert hass.config.language == default_language
async def test_loading_configuration_default_media_dirs_docker(
hass: HomeAssistant,
) -> None:
"""Test loading core config onto hass object."""
with patch("homeassistant.core_config.is_docker_env", return_value=True):
await async_process_ha_core_config(
hass,
{
"name": "Huis",
},
)
assert hass.config.location_name == "Huis"
assert len(hass.config.allowlist_external_dirs) == 2
assert "/media" in hass.config.allowlist_external_dirs
assert hass.config.media_dirs == {"local": "/media"}
async def test_loading_configuration_from_packages(hass: HomeAssistant) -> None:
"""Test loading packages config onto hass object config."""
await async_process_ha_core_config(
hass,
{
"latitude": 39,
"longitude": -1,
"elevation": 500,
"name": "Huis",
"unit_system": "metric",
"time_zone": "Europe/Madrid",
"external_url": "https://www.example.com",
"internal_url": "http://example.local",
"packages": {
"package_1": {"wake_on_lan": None},
"package_2": {
"light": {"platform": "hue"},
"media_extractor": None,
"sun": None,
},
},
},
)
# Empty packages not allowed
with pytest.raises(MultipleInvalid):
await async_process_ha_core_config(
hass,
{
"latitude": 39,
"longitude": -1,
"elevation": 500,
"name": "Huis",
"unit_system": "metric",
"time_zone": "Europe/Madrid",
"packages": {"empty_package": None},
},
)
@pytest.mark.parametrize(
("unit_system_name", "expected_unit_system"),
[
("metric", METRIC_SYSTEM),
("imperial", US_CUSTOMARY_SYSTEM),
("us_customary", US_CUSTOMARY_SYSTEM),
],
)
async def test_loading_configuration_unit_system(
hass: HomeAssistant, unit_system_name: str, expected_unit_system: UnitSystem
) -> None:
"""Test backward compatibility when loading core config."""
await async_process_ha_core_config(
hass,
{
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
"unit_system": unit_system_name,
"time_zone": "America/New_York",
"external_url": "https://www.example.com",
"internal_url": "http://example.local",
},
)
assert hass.config.units is expected_unit_system
async def test_merge_customize(hass: HomeAssistant) -> None:
"""Test loading core config onto hass object."""
core_config = {
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
"unit_system": "imperial",
"time_zone": "GMT",
"customize": {"a.a": {"friendly_name": "A"}},
"packages": {
"pkg1": {"homeassistant": {"customize": {"b.b": {"friendly_name": "BB"}}}}
},
}
await async_process_ha_core_config(hass, core_config)
assert hass.data[DATA_CUSTOMIZE].get("b.b") == {"friendly_name": "BB"}
async def test_auth_provider_config(hass: HomeAssistant) -> None:
"""Test loading auth provider config onto hass object."""
core_config = {
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
"unit_system": "imperial",
"time_zone": "GMT",
CONF_AUTH_PROVIDERS: [
{"type": "homeassistant"},
],
CONF_AUTH_MFA_MODULES: [{"type": "totp"}, {"type": "totp", "id": "second"}],
}
if hasattr(hass, "auth"):
del hass.auth
await async_process_ha_core_config(hass, core_config)
assert len(hass.auth.auth_providers) == 1
assert hass.auth.auth_providers[0].type == "homeassistant"
assert len(hass.auth.auth_mfa_modules) == 2
assert hass.auth.auth_mfa_modules[0].id == "totp"
assert hass.auth.auth_mfa_modules[1].id == "second"
async def test_auth_provider_config_default(hass: HomeAssistant) -> None:
"""Test loading default auth provider config."""
core_config = {
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
"unit_system": "imperial",
"time_zone": "GMT",
}
if hasattr(hass, "auth"):
del hass.auth
await async_process_ha_core_config(hass, core_config)
assert len(hass.auth.auth_providers) == 1
assert hass.auth.auth_providers[0].type == "homeassistant"
assert len(hass.auth.auth_mfa_modules) == 1
assert hass.auth.auth_mfa_modules[0].id == "totp"
async def test_disallowed_auth_provider_config(hass: HomeAssistant) -> None:
"""Test loading insecure example auth provider is disallowed."""
core_config = {
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
"unit_system": "imperial",
"time_zone": "GMT",
CONF_AUTH_PROVIDERS: [
{
"type": "insecure_example",
"users": [
{
"username": "test-user",
"password": "test-pass",
"name": "Test Name",
}
],
}
],
}
with pytest.raises(Invalid):
await async_process_ha_core_config(hass, core_config)
async def test_disallowed_duplicated_auth_provider_config(hass: HomeAssistant) -> None:
"""Test loading insecure example auth provider is disallowed."""
core_config = {
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
"unit_system": "imperial",
"time_zone": "GMT",
CONF_AUTH_PROVIDERS: [{"type": "homeassistant"}, {"type": "homeassistant"}],
}
with pytest.raises(Invalid):
await async_process_ha_core_config(hass, core_config)
async def test_disallowed_auth_mfa_module_config(hass: HomeAssistant) -> None:
"""Test loading insecure example auth mfa module is disallowed."""
core_config = {
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
"unit_system": "imperial",
"time_zone": "GMT",
CONF_AUTH_MFA_MODULES: [
{
"type": "insecure_example",
"data": [{"user_id": "mock-user", "pin": "test-pin"}],
}
],
}
with pytest.raises(Invalid):
await async_process_ha_core_config(hass, core_config)
async def test_disallowed_duplicated_auth_mfa_module_config(
hass: HomeAssistant,
) -> None:
"""Test loading insecure example auth mfa module is disallowed."""
core_config = {
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
"unit_system": "imperial",
"time_zone": "GMT",
CONF_AUTH_MFA_MODULES: [{"type": "totp"}, {"type": "totp"}],
}
with pytest.raises(Invalid):
await async_process_ha_core_config(hass, core_config)
async def test_core_config_schema_historic_currency(
hass: HomeAssistant, issue_registry: ir.IssueRegistry
) -> None:
"""Test core config schema."""
await async_process_ha_core_config(hass, {"currency": "LTT"})
issue = issue_registry.async_get_issue("homeassistant", "historic_currency")
assert issue
assert issue.translation_placeholders == {"currency": "LTT"}
async def test_core_store_historic_currency(
hass: HomeAssistant, hass_storage: dict[str, Any], issue_registry: ir.IssueRegistry
) -> None:
"""Test core config store."""
core_data = {
"data": {
"currency": "LTT",
},
"key": "core.config",
"version": 1,
"minor_version": 1,
}
hass_storage["core.config"] = dict(core_data)
await async_process_ha_core_config(hass, {})
issue_id = "historic_currency"
issue = issue_registry.async_get_issue("homeassistant", issue_id)
assert issue
assert issue.translation_placeholders == {"currency": "LTT"}
await hass.config.async_update(currency="EUR")
issue = issue_registry.async_get_issue("homeassistant", issue_id)
assert not issue
async def test_core_config_schema_no_country(
hass: HomeAssistant, issue_registry: ir.IssueRegistry
) -> None:
"""Test core config schema."""
await async_process_ha_core_config(hass, {})
issue = issue_registry.async_get_issue("homeassistant", "country_not_configured")
assert issue
async def test_core_store_no_country(
hass: HomeAssistant, hass_storage: dict[str, Any], issue_registry: ir.IssueRegistry
) -> None:
"""Test core config store."""
core_data = {
"data": {},
"key": "core.config",
"version": 1,
"minor_version": 1,
}
hass_storage["core.config"] = dict(core_data)
await async_process_ha_core_config(hass, {})
issue_id = "country_not_configured"
issue = issue_registry.async_get_issue("homeassistant", issue_id)
assert issue
await hass.config.async_update(country="SE")
issue = issue_registry.async_get_issue("homeassistant", issue_id)
assert not issue
async def test_configuration_legacy_template_is_removed(hass: HomeAssistant) -> None:
"""Test loading core config onto hass object."""
await async_process_ha_core_config(
hass,
{
"latitude": 60,
"longitude": 50,
"elevation": 25,
"name": "Huis",
"unit_system": "imperial",
"time_zone": "America/New_York",
"allowlist_external_dirs": "/etc",
"external_url": "https://www.example.com",
"internal_url": "http://example.local",
"media_dirs": {"mymedia": "/usr"},
"legacy_templates": True,
"debug": True,
"currency": "EUR",
"country": "SE",
"language": "sv",
"radius": 150,
},
)
assert not getattr(hass.config, "legacy_templates")