Move core config functionality to its own module (#129065)
* Move core config functionality to its own module * Adjust test
This commit is contained in:
parent
cd4aa8ccd6
commit
3e62c6ae2f
53 changed files with 1308 additions and 1284 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
423
homeassistant/core_config.py
Normal file
423
homeassistant/core_config.py
Normal 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])
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
823
tests/test_core_config.py
Normal 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")
|
Loading…
Add table
Reference in a new issue