mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
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
@ -70,6 +70,7 @@ from .const import (
|
|||||||
REQUIRED_NEXT_PYTHON_VER,
|
REQUIRED_NEXT_PYTHON_VER,
|
||||||
SIGNAL_BOOTSTRAP_INTEGRATIONS,
|
SIGNAL_BOOTSTRAP_INTEGRATIONS,
|
||||||
)
|
)
|
||||||
|
from .core_config import async_process_ha_core_config
|
||||||
from .exceptions import HomeAssistantError
|
from .exceptions import HomeAssistantError
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
area_registry,
|
area_registry,
|
||||||
@ -479,7 +480,7 @@ async def async_from_config_dict(
|
|||||||
core_config = config.get(core.DOMAIN, {})
|
core_config = config.get(core.DOMAIN, {})
|
||||||
|
|
||||||
try:
|
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:
|
except vol.Invalid as config_err:
|
||||||
conf_util.async_log_schema_error(config_err, core.DOMAIN, core_config, hass)
|
conf_util.async_log_schema_error(config_err, core.DOMAIN, core_config, hass)
|
||||||
async_notify_setup_error(hass, core.DOMAIN)
|
async_notify_setup_error(hass, core.DOMAIN)
|
||||||
|
@ -8,9 +8,9 @@ from typing import Any
|
|||||||
|
|
||||||
import voluptuous as vol
|
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.auth.permissions.const import CAT_ENTITIES, POLICY_CONTROL
|
||||||
from homeassistant.components import persistent_notification
|
from homeassistant.components import persistent_notification
|
||||||
import homeassistant.config as conf_util
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ELEVATION,
|
ATTR_ELEVATION,
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
@ -269,7 +269,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# auth only processed during startup
|
# 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(
|
async_register_admin_service(
|
||||||
hass, DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config
|
hass, DOMAIN, SERVICE_RELOAD_CORE_CONFIG, async_handle_reload_config
|
||||||
|
@ -16,66 +16,24 @@ from pathlib import Path
|
|||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import TYPE_CHECKING, Any, Final
|
from typing import TYPE_CHECKING, Any
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
from awesomeversion import AwesomeVersion
|
from awesomeversion import AwesomeVersion
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from voluptuous.humanize import MAX_VALIDATION_ERROR_ITEM_LENGTH
|
from voluptuous.humanize import MAX_VALIDATION_ERROR_ITEM_LENGTH
|
||||||
from yaml.error import MarkedYAMLError
|
from yaml.error import MarkedYAMLError
|
||||||
|
|
||||||
from . import auth
|
from .const import CONF_PACKAGES, CONF_PLATFORM, __version__
|
||||||
from .auth import mfa_modules as auth_mfa_modules, providers as auth_providers
|
from .core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
|
||||||
from .const import (
|
from .core_config import _PACKAGE_DEFINITION_SCHEMA, _PACKAGES_CONFIG_SCHEMA
|
||||||
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 .exceptions import ConfigValidationError, HomeAssistantError
|
from .exceptions import ConfigValidationError, HomeAssistantError
|
||||||
from .generated.currencies import HISTORIC_CURRENCIES
|
from .helpers import config_validation as cv
|
||||||
from .helpers import config_validation as cv, issue_registry as ir
|
|
||||||
from .helpers.entity_values import EntityValues
|
|
||||||
from .helpers.translation import async_get_exception_message
|
from .helpers.translation import async_get_exception_message
|
||||||
from .helpers.typing import ConfigType
|
from .helpers.typing import ConfigType
|
||||||
from .loader import ComponentProtocol, Integration, IntegrationNotFound
|
from .loader import ComponentProtocol, Integration, IntegrationNotFound
|
||||||
from .requirements import RequirementsNotFound, async_get_integration_with_requirements
|
from .requirements import RequirementsNotFound, async_get_integration_with_requirements
|
||||||
from .util.async_ import create_eager_task
|
from .util.async_ import create_eager_task
|
||||||
from .util.hass_dict import HassKey
|
|
||||||
from .util.package import is_docker_env
|
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 import SECRET_YAML, Secrets, YamlTypeError, load_yaml_dict
|
||||||
from .util.yaml.objects import NodeStrClass
|
from .util.yaml.objects import NodeStrClass
|
||||||
|
|
||||||
@ -86,7 +44,6 @@ RE_ASCII = re.compile(r"\033\[[^m]*m")
|
|||||||
YAML_CONFIG_FILE = "configuration.yaml"
|
YAML_CONFIG_FILE = "configuration.yaml"
|
||||||
VERSION_FILE = ".HA_VERSION"
|
VERSION_FILE = ".HA_VERSION"
|
||||||
CONFIG_DIR_NAME = ".homeassistant"
|
CONFIG_DIR_NAME = ".homeassistant"
|
||||||
DATA_CUSTOMIZE: HassKey[EntityValues] = HassKey("hass_customize")
|
|
||||||
|
|
||||||
AUTOMATION_CONFIG_PATH = "automations.yaml"
|
AUTOMATION_CONFIG_PATH = "automations.yaml"
|
||||||
SCRIPT_CONFIG_PATH = "scripts.yaml"
|
SCRIPT_CONFIG_PATH = "scripts.yaml"
|
||||||
@ -97,10 +54,6 @@ INTEGRATION_LOAD_EXCEPTIONS = (IntegrationNotFound, RequirementsNotFound)
|
|||||||
|
|
||||||
SAFE_MODE_FILENAME = "safe-mode"
|
SAFE_MODE_FILENAME = "safe-mode"
|
||||||
|
|
||||||
CONF_CREDENTIAL: Final = "credential"
|
|
||||||
CONF_ICE_SERVERS: Final = "ice_servers"
|
|
||||||
CONF_WEBRTC: Final = "webrtc"
|
|
||||||
|
|
||||||
DEFAULT_CONFIG = f"""
|
DEFAULT_CONFIG = f"""
|
||||||
# Loads default set of integrations. Do not remove.
|
# Loads default set of integrations. Do not remove.
|
||||||
default_config:
|
default_config:
|
||||||
@ -179,229 +132,6 @@ class IntegrationConfigInfo:
|
|||||||
exception_info_list: list[ConfigExceptionInfo]
|
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:
|
def get_default_config_dir() -> str:
|
||||||
"""Put together the default configuration directory based on the OS."""
|
"""Put together the default configuration directory based on the OS."""
|
||||||
data_dir = os.path.expanduser("~")
|
data_dir = os.path.expanduser("~")
|
||||||
@ -847,141 +577,6 @@ def format_schema_error(
|
|||||||
return humanize_error(hass, exc, domain, config, link)
|
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(
|
def _log_pkg_error(
|
||||||
hass: HomeAssistant, package: str, component: str | None, config: dict, message: str
|
hass: HomeAssistant, package: str, component: str | None, config: dict, message: str
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -1046,7 +641,7 @@ def _identify_config_schema(module: ComponentProtocol) -> str | None:
|
|||||||
def _validate_package_definition(name: str, conf: Any) -> None:
|
def _validate_package_definition(name: str, conf: Any) -> None:
|
||||||
"""Validate basic package definition properties."""
|
"""Validate basic package definition properties."""
|
||||||
cv.slug(name)
|
cv.slug(name)
|
||||||
PACKAGE_DEFINITION_SCHEMA(conf)
|
_PACKAGE_DEFINITION_SCHEMA(conf)
|
||||||
|
|
||||||
|
|
||||||
def _recursive_merge(conf: dict[str, Any], package: dict[str, Any]) -> str | None:
|
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.
|
vol.Invalid if whole package config is invalid.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
PACKAGES_CONFIG_SCHEMA(packages)
|
_PACKAGES_CONFIG_SCHEMA(packages)
|
||||||
|
|
||||||
invalid_packages = []
|
invalid_packages = []
|
||||||
for pack_name, pack_conf in packages.items():
|
for pack_name, pack_conf in packages.items():
|
||||||
|
@ -3145,7 +3145,7 @@ class Config:
|
|||||||
async def async_update(self, **kwargs: Any) -> None:
|
async def async_update(self, **kwargs: Any) -> None:
|
||||||
"""Update the configuration from a dictionary."""
|
"""Update the configuration from a dictionary."""
|
||||||
# pylint: disable-next=import-outside-toplevel
|
# pylint: disable-next=import-outside-toplevel
|
||||||
from .config import (
|
from .core_config import (
|
||||||
_raise_issue_if_historic_currency,
|
_raise_issue_if_historic_currency,
|
||||||
_raise_issue_if_no_country,
|
_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 import loader
|
||||||
from homeassistant.config import ( # type: ignore[attr-defined]
|
from homeassistant.config import ( # type: ignore[attr-defined]
|
||||||
CONF_PACKAGES,
|
CONF_PACKAGES,
|
||||||
CORE_CONFIG_SCHEMA,
|
|
||||||
YAML_CONFIG_FILE,
|
YAML_CONFIG_FILE,
|
||||||
config_per_platform,
|
config_per_platform,
|
||||||
extract_domain_configs,
|
extract_domain_configs,
|
||||||
@ -23,6 +22,7 @@ from homeassistant.config import ( # type: ignore[attr-defined]
|
|||||||
merge_packages_config,
|
merge_packages_config,
|
||||||
)
|
)
|
||||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||||
|
from homeassistant.core_config import CORE_CONFIG_SCHEMA
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.requirements import (
|
from homeassistant.requirements import (
|
||||||
RequirementsNotFound,
|
RequirementsNotFound,
|
||||||
|
@ -21,7 +21,6 @@ from typing import TYPE_CHECKING, Any, Final, Literal, NotRequired, TypedDict, f
|
|||||||
from propcache import cached_property
|
from propcache import cached_property
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config import DATA_CUSTOMIZE
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ASSUMED_STATE,
|
ATTR_ASSUMED_STATE,
|
||||||
ATTR_ATTRIBUTION,
|
ATTR_ATTRIBUTION,
|
||||||
@ -49,6 +48,7 @@ from homeassistant.core import (
|
|||||||
get_hassjob_callable_job_type,
|
get_hassjob_callable_job_type,
|
||||||
get_release_channel,
|
get_release_channel,
|
||||||
)
|
)
|
||||||
|
from homeassistant.core_config import DATA_CUSTOMIZE
|
||||||
from homeassistant.exceptions import (
|
from homeassistant.exceptions import (
|
||||||
HomeAssistantError,
|
HomeAssistantError,
|
||||||
InvalidStateError,
|
InvalidStateError,
|
||||||
|
@ -10,7 +10,7 @@ from .model import Config, Integration
|
|||||||
|
|
||||||
CONFIG_SCHEMA_IGNORE = {
|
CONFIG_SCHEMA_IGNORE = {
|
||||||
# Configuration under the homeassistant key is a special case, it's handled by
|
# 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.
|
# a schema in the homeassistant integration.
|
||||||
HOMEASSISTANT_DOMAIN,
|
HOMEASSISTANT_DOMAIN,
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,6 @@ from homeassistant.components.cover import CoverDeviceClass, CoverEntityFeature
|
|||||||
from homeassistant.components.media_player import MediaPlayerEntityFeature
|
from homeassistant.components.media_player import MediaPlayerEntityFeature
|
||||||
from homeassistant.components.vacuum import VacuumEntityFeature
|
from homeassistant.components.vacuum import VacuumEntityFeature
|
||||||
from homeassistant.components.valve import SERVICE_STOP_VALVE, ValveEntityFeature
|
from homeassistant.components.valve import SERVICE_STOP_VALVE, ValveEntityFeature
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
SERVICE_CLOSE_VALVE,
|
SERVICE_CLOSE_VALVE,
|
||||||
SERVICE_OPEN_VALVE,
|
SERVICE_OPEN_VALVE,
|
||||||
@ -20,6 +19,7 @@ from homeassistant.const import (
|
|||||||
UnitOfTemperature,
|
UnitOfTemperature,
|
||||||
)
|
)
|
||||||
from homeassistant.core import Context, Event, HomeAssistant
|
from homeassistant.core import Context, Event, HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.helpers import entityfilter
|
from homeassistant.helpers import entityfilter
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
|
from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
|
||||||
|
@ -16,13 +16,13 @@ from homeassistant.components.camera.const import (
|
|||||||
PREF_PRELOAD_STREAM,
|
PREF_PRELOAD_STREAM,
|
||||||
)
|
)
|
||||||
from homeassistant.components.websocket_api import TYPE_RESULT
|
from homeassistant.components.websocket_api import TYPE_RESULT
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
EVENT_HOMEASSISTANT_STARTED,
|
EVENT_HOMEASSISTANT_STARTED,
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
@ -13,8 +13,8 @@ from homeassistant.components.camera.webrtc import (
|
|||||||
async_register_webrtc_provider,
|
async_register_webrtc_provider,
|
||||||
)
|
)
|
||||||
from homeassistant.components.websocket_api import TYPE_RESULT
|
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 import HomeAssistant, callback
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from tests.typing import WebSocketGenerator
|
from tests.typing import WebSocketGenerator
|
||||||
|
@ -5,8 +5,8 @@ from unittest.mock import patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.cast import DOMAIN, home_assistant_cast
|
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 import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
from tests.common import MockConfigEntry, async_mock_signal
|
from tests.common import MockConfigEntry, async_mock_signal
|
||||||
|
@ -27,13 +27,13 @@ from homeassistant.components.media_player import (
|
|||||||
MediaClass,
|
MediaClass,
|
||||||
MediaPlayerEntityFeature,
|
MediaPlayerEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
CAST_APP_ID_HOMEASSISTANT_LOVELACE,
|
CAST_APP_ID_HOMEASSISTANT_LOVELACE,
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er, network
|
from homeassistant.helpers import device_registry as dr, entity_registry as er, network
|
||||||
from homeassistant.helpers.dispatcher import (
|
from homeassistant.helpers.dispatcher import (
|
||||||
|
@ -25,9 +25,9 @@ from homeassistant.components.tts import (
|
|||||||
DOMAIN as TTS_DOMAIN,
|
DOMAIN as TTS_DOMAIN,
|
||||||
get_engine_instance,
|
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.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||||
from homeassistant.core import HomeAssistant
|
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 import issue_registry as ir
|
||||||
from homeassistant.helpers.entity_registry import EntityRegistry
|
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
@ -8,8 +8,8 @@ import pytest
|
|||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components import dialogflow, intent_script
|
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 import HomeAssistant, ServiceCall, callback
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
@ -32,9 +32,9 @@ from homeassistant.components.media_player import (
|
|||||||
DOMAIN as DOMAIN_MP,
|
DOMAIN as DOMAIN_MP,
|
||||||
SERVICE_PLAY_MEDIA,
|
SERVICE_PLAY_MEDIA,
|
||||||
)
|
)
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_API_KEY
|
from homeassistant.const import ATTR_ENTITY_ID, CONF_API_KEY
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
|
|
||||||
from .const import MOCK_MODELS, MOCK_VOICES
|
from .const import MOCK_MODELS, MOCK_VOICES
|
||||||
|
|
||||||
|
@ -10,7 +10,6 @@ from homeassistant import config_entries
|
|||||||
from homeassistant.components import zone
|
from homeassistant.components import zone
|
||||||
from homeassistant.components.device_tracker.legacy import Device
|
from homeassistant.components.device_tracker.legacy import Device
|
||||||
from homeassistant.components.geofency import CONF_MOBILE_BEACONS, DOMAIN
|
from homeassistant.components.geofency import CONF_MOBILE_BEACONS, DOMAIN
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_LATITUDE,
|
ATTR_LATITUDE,
|
||||||
ATTR_LONGITUDE,
|
ATTR_LONGITUDE,
|
||||||
@ -18,6 +17,7 @@ from homeassistant.const import (
|
|||||||
STATE_NOT_HOME,
|
STATE_NOT_HOME,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
@ -15,8 +15,8 @@ from homeassistant.components.google_assistant.const import (
|
|||||||
STORE_GOOGLE_LOCAL_WEBHOOK_ID,
|
STORE_GOOGLE_LOCAL_WEBHOOK_ID,
|
||||||
)
|
)
|
||||||
from homeassistant.components.matter import MatterDeviceInfo
|
from homeassistant.components.matter import MatterDeviceInfo
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.core import HomeAssistant, State
|
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.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
@ -32,7 +32,6 @@ from homeassistant.components.google_assistant import (
|
|||||||
smart_home as sh,
|
smart_home as sh,
|
||||||
trait,
|
trait,
|
||||||
)
|
)
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_UNIT_OF_MEASUREMENT,
|
ATTR_UNIT_OF_MEASUREMENT,
|
||||||
EVENT_CALL_SERVICE,
|
EVENT_CALL_SERVICE,
|
||||||
@ -41,6 +40,7 @@ from homeassistant.const import (
|
|||||||
__version__,
|
__version__,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, State
|
from homeassistant.core import HomeAssistant, State
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import (
|
||||||
area_registry as ar,
|
area_registry as ar,
|
||||||
device_registry as dr,
|
device_registry as dr,
|
||||||
|
@ -54,7 +54,6 @@ from homeassistant.components.media_player import (
|
|||||||
from homeassistant.components.vacuum import VacuumEntityFeature
|
from homeassistant.components.vacuum import VacuumEntityFeature
|
||||||
from homeassistant.components.valve import ValveEntityFeature
|
from homeassistant.components.valve import ValveEntityFeature
|
||||||
from homeassistant.components.water_heater import WaterHeaterEntityFeature
|
from homeassistant.components.water_heater import WaterHeaterEntityFeature
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ASSUMED_STATE,
|
ATTR_ASSUMED_STATE,
|
||||||
ATTR_BATTERY_LEVEL,
|
ATTR_BATTERY_LEVEL,
|
||||||
@ -77,6 +76,7 @@ from homeassistant.const import (
|
|||||||
UnitOfTemperature,
|
UnitOfTemperature,
|
||||||
)
|
)
|
||||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, State
|
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 import color, dt as dt_util
|
||||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||||
|
|
||||||
|
@ -14,9 +14,9 @@ import pytest
|
|||||||
from homeassistant.components import tts
|
from homeassistant.components import tts
|
||||||
from homeassistant.components.google_translate.const import CONF_TLD, DOMAIN
|
from homeassistant.components.google_translate.const import CONF_TLD, DOMAIN
|
||||||
from homeassistant.components.media_player import ATTR_MEDIA_CONTENT_ID
|
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.const import ATTR_ENTITY_ID, CONF_PLATFORM
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
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 import DOMAIN as DEVICE_TRACKER_DOMAIN
|
||||||
from homeassistant.components.device_tracker.legacy import Device
|
from homeassistant.components.device_tracker.legacy import Device
|
||||||
from homeassistant.components.gpslogger import DOMAIN, TRACKER_UPDATE
|
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.const import STATE_HOME, STATE_NOT_HOME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.helpers.dispatcher import DATA_DISPATCHER
|
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.config.os.path.isfile", Mock(return_value=True))
|
||||||
@patch("homeassistant.components.homeassistant._LOGGER.error")
|
@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(
|
async def test_reload_core_with_wrong_conf(
|
||||||
mock_process, mock_error, hass: HomeAssistant
|
mock_process, mock_error, hass: HomeAssistant
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components import ifttt
|
from homeassistant.components import ifttt
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
from tests.typing import ClientSessionGenerator
|
from tests.typing import ClientSessionGenerator
|
||||||
|
@ -7,8 +7,8 @@ import pytest
|
|||||||
|
|
||||||
from homeassistant.components import konnected
|
from homeassistant.components import konnected
|
||||||
from homeassistant.components.konnected import config_flow
|
from homeassistant.components.konnected import config_flow
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
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 import DOMAIN as DEVICE_TRACKER_DOMAIN
|
||||||
from homeassistant.components.device_tracker.legacy import Device
|
from homeassistant.components.device_tracker.legacy import Device
|
||||||
from homeassistant.components.locative import DOMAIN, TRACKER_UPDATE
|
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 import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
from homeassistant.helpers.dispatcher import DATA_DISPATCHER
|
from homeassistant.helpers.dispatcher import DATA_DISPATCHER
|
||||||
from homeassistant.setup import async_setup_component
|
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.lovelace import cast as lovelace_cast
|
||||||
from homeassistant.components.media_player import MediaClass
|
from homeassistant.components.media_player import MediaClass
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
@ -8,9 +8,9 @@ import pytest
|
|||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components import mailgun, webhook
|
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.const import CONF_API_KEY, CONF_DOMAIN
|
||||||
from homeassistant.core import Event, HomeAssistant, callback
|
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.data_entry_flow import FlowResultType
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
@ -7,8 +7,8 @@ import pytest
|
|||||||
from homeassistant.components.media_player.browse_media import (
|
from homeassistant.components.media_player.browse_media import (
|
||||||
async_process_play_media_url,
|
async_process_play_media_url,
|
||||||
)
|
)
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.network import NoURLAvailableError
|
from homeassistant.helpers.network import NoURLAvailableError
|
||||||
|
|
||||||
|
@ -11,8 +11,8 @@ import pytest
|
|||||||
|
|
||||||
from homeassistant.components import media_source, websocket_api
|
from homeassistant.components import media_source, websocket_api
|
||||||
from homeassistant.components.media_source import const
|
from homeassistant.components.media_source import const
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from tests.common import MockUser
|
from tests.common import MockUser
|
||||||
|
@ -8,9 +8,9 @@ import pytest
|
|||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.met.const import DOMAIN, HOME_LOCATION_NAME
|
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.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
from . import init_integration
|
from . import init_integration
|
||||||
|
@ -7,9 +7,9 @@ from homeassistant.components.met.const import (
|
|||||||
DEFAULT_HOME_LONGITUDE,
|
DEFAULT_HOME_LONGITUDE,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
|
||||||
from . import init_integration
|
from . import init_integration
|
||||||
|
@ -10,8 +10,8 @@ import pytest
|
|||||||
from homeassistant.components import tts
|
from homeassistant.components import tts
|
||||||
from homeassistant.components.media_player import ATTR_MEDIA_CONTENT_ID
|
from homeassistant.components.media_player import ATTR_MEDIA_CONTENT_ID
|
||||||
from homeassistant.components.microsoft.tts import SUPPORTED_LANGUAGES
|
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 import HomeAssistant, ServiceCall
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.exceptions import ServiceNotFound
|
from homeassistant.exceptions import ServiceNotFound
|
||||||
from homeassistant.setup import async_setup_component
|
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.const import DOMAIN
|
||||||
from homeassistant.components.motioneye.entity import get_motioneye_entity_unique_id
|
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.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_URL
|
from homeassistant.const import CONF_URL
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
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 import config_flow
|
||||||
from homeassistant.components.owntracks.config_flow import CONF_CLOUDHOOK, CONF_SECRET
|
from homeassistant.components.owntracks.config_flow import CONF_CLOUDHOOK, CONF_SECRET
|
||||||
from homeassistant.components.owntracks.const import DOMAIN
|
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.const import CONF_WEBHOOK_ID
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
@ -4,8 +4,8 @@ from datetime import timedelta
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import io
|
import io
|
||||||
|
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
@ -15,10 +15,10 @@ from homeassistant.components.reolink import (
|
|||||||
NUM_CRED_ERRORS,
|
NUM_CRED_ERRORS,
|
||||||
)
|
)
|
||||||
from homeassistant.components.reolink.const import DOMAIN
|
from homeassistant.components.reolink.const import DOMAIN
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.const import CONF_PORT, STATE_OFF, STATE_UNAVAILABLE, Platform
|
from homeassistant.const import CONF_PORT, STATE_OFF, STATE_UNAVAILABLE, Platform
|
||||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import (
|
||||||
device_registry as dr,
|
device_registry as dr,
|
||||||
entity_registry as er,
|
entity_registry as er,
|
||||||
|
@ -12,6 +12,7 @@ from homeassistant import config as hass_config
|
|||||||
from homeassistant.components.rest.const import DOMAIN
|
from homeassistant.components.rest.const import DOMAIN
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
|
CONF_PACKAGES,
|
||||||
SERVICE_RELOAD,
|
SERVICE_RELOAD,
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
UnitOfInformation,
|
UnitOfInformation,
|
||||||
@ -468,7 +469,7 @@ async def test_config_schema_via_packages(hass: HomeAssistant) -> None:
|
|||||||
"pack_11": {"rest": {"resource": "http://url1"}},
|
"pack_11": {"rest": {"resource": "http://url1"}},
|
||||||
"pack_list": {"rest": [{"resource": "http://url2"}]},
|
"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)
|
await hass_config.merge_packages_config(hass, config, packages)
|
||||||
|
|
||||||
assert len(config) == 2
|
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.stream import FORMAT_CONTENT_TYPE, HLS_PROVIDER
|
||||||
from homeassistant.components.websocket_api import TYPE_RESULT
|
from homeassistant.components.websocket_api import TYPE_RESULT
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
ATTR_NAME,
|
ATTR_NAME,
|
||||||
@ -60,6 +59,7 @@ from homeassistant.const import (
|
|||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
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.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
@ -38,7 +38,6 @@ from homeassistant.components.smartthings.const import (
|
|||||||
STORAGE_KEY,
|
STORAGE_KEY,
|
||||||
STORAGE_VERSION,
|
STORAGE_VERSION,
|
||||||
)
|
)
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
|
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_ACCESS_TOKEN,
|
CONF_ACCESS_TOKEN,
|
||||||
@ -47,6 +46,7 @@ from homeassistant.const import (
|
|||||||
CONF_WEBHOOK_ID,
|
CONF_WEBHOOK_ID,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
@ -16,9 +16,9 @@ from homeassistant.components.smartthings.const import (
|
|||||||
CONF_LOCATION_ID,
|
CONF_LOCATION_ID,
|
||||||
DOMAIN,
|
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.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
@ -23,8 +23,8 @@ from homeassistant.components.smartthings.const import (
|
|||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
SIGNAL_SMARTTHINGS_UPDATE,
|
SIGNAL_SMARTTHINGS_UPDATE,
|
||||||
)
|
)
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
|
|
||||||
|
@ -7,10 +7,10 @@ import pytest
|
|||||||
from toonapi import Agreement, ToonError
|
from toonapi import Agreement, ToonError
|
||||||
|
|
||||||
from homeassistant.components.toon.const import CONF_AGREEMENT, DOMAIN
|
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.config_entries import SOURCE_IMPORT, SOURCE_USER
|
||||||
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
from homeassistant.helpers import config_entry_oauth2_flow
|
from homeassistant.helpers import config_entry_oauth2_flow
|
||||||
from homeassistant.setup import async_setup_component
|
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 import DOMAIN as DEVICE_TRACKER_DOMAIN
|
||||||
from homeassistant.components.device_tracker.legacy import Device
|
from homeassistant.components.device_tracker.legacy import Device
|
||||||
from homeassistant.components.traccar import DOMAIN, TRACKER_UPDATE
|
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.const import STATE_HOME, STATE_NOT_HOME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.helpers.dispatcher import DATA_DISPATCHER
|
from homeassistant.helpers.dispatcher import DATA_DISPATCHER
|
||||||
|
@ -10,9 +10,9 @@ from unittest.mock import MagicMock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.config_entries import ConfigFlow
|
from homeassistant.config_entries import ConfigFlow
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
|
|
||||||
from .common import (
|
from .common import (
|
||||||
DEFAULT_LANG,
|
DEFAULT_LANG,
|
||||||
|
@ -9,8 +9,8 @@ from homeassistant.components.media_player import (
|
|||||||
DOMAIN as DOMAIN_MP,
|
DOMAIN as DOMAIN_MP,
|
||||||
SERVICE_PLAY_MEDIA,
|
SERVICE_PLAY_MEDIA,
|
||||||
)
|
)
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from .common import MockTTSEntity, mock_config_entry_setup
|
from .common import MockTTSEntity, mock_config_entry_setup
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components import twilio
|
from homeassistant.components import twilio
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
from tests.typing import ClientSessionGenerator
|
from tests.typing import ClientSessionGenerator
|
||||||
|
@ -9,8 +9,8 @@ from aiohttp.test_utils import TestClient
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components import webhook
|
from homeassistant.components import webhook
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
||||||
|
@ -10,8 +10,8 @@ from aiowithings import Activity, Device, Goals, MeasurementGroup, SleepSummary,
|
|||||||
from freezegun.api import FrozenDateTimeFactory
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
|
|
||||||
from homeassistant.components.webhook import async_generate_url
|
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 import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
|
|
||||||
from tests.common import (
|
from tests.common import (
|
||||||
MockConfigEntry,
|
MockConfigEntry,
|
||||||
|
@ -6,8 +6,8 @@ from unittest.mock import Mock, PropertyMock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant import config_entries, data_entry_flow, setup
|
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 import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.helpers import config_entry_flow
|
from homeassistant.helpers import config_entry_flow
|
||||||
|
|
||||||
from tests.common import MockConfigEntry, MockModule, mock_integration, mock_platform
|
from tests.common import MockConfigEntry, MockModule, mock_integration, mock_platform
|
||||||
|
@ -8,8 +8,8 @@ import pytest
|
|||||||
from yarl import URL
|
from yarl import URL
|
||||||
|
|
||||||
from homeassistant.components import cloud
|
from homeassistant.components import cloud
|
||||||
from homeassistant.config import async_process_ha_core_config
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.core_config import async_process_ha_core_config
|
||||||
from homeassistant.helpers.network import (
|
from homeassistant.helpers.network import (
|
||||||
NoURLAvailableError,
|
NoURLAvailableError,
|
||||||
_get_cloud_url,
|
_get_cloud_url,
|
||||||
|
@ -4,63 +4,32 @@ import asyncio
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
import contextlib
|
import contextlib
|
||||||
import copy
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from voluptuous import Invalid, MultipleInvalid
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from homeassistant import loader
|
from homeassistant import loader
|
||||||
import homeassistant.config as config_util
|
import homeassistant.config as config_util
|
||||||
from homeassistant.const import (
|
from homeassistant.const import CONF_PACKAGES, __version__
|
||||||
ATTR_ASSUMED_STATE,
|
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||||
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.exceptions import ConfigValidationError, HomeAssistantError
|
from homeassistant.exceptions import ConfigValidationError, HomeAssistantError
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import check_config, config_validation as cv
|
||||||
check_config,
|
|
||||||
config_validation as cv,
|
|
||||||
issue_registry as ir,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.entity import Entity
|
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.loader import Integration, async_get_integration
|
from homeassistant.loader import Integration, async_get_integration
|
||||||
from homeassistant.setup import async_setup_component
|
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 import SECRET_YAML
|
||||||
from homeassistant.util.yaml.objects import NodeDictClass
|
from homeassistant.util.yaml.objects import NodeDictClass
|
||||||
|
|
||||||
from .common import (
|
from .common import (
|
||||||
MockModule,
|
MockModule,
|
||||||
MockPlatform,
|
MockPlatform,
|
||||||
MockUser,
|
|
||||||
get_test_config_dir,
|
get_test_config_dir,
|
||||||
mock_integration,
|
mock_integration,
|
||||||
mock_platform,
|
mock_platform,
|
||||||
@ -510,198 +479,6 @@ async def test_create_default_config_returns_none_if_write_error(
|
|||||||
assert mock_print.called
|
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.shutil")
|
||||||
@patch("homeassistant.config.os")
|
@patch("homeassistant.config.os")
|
||||||
@patch("homeassistant.config.is_docker_env", return_value=False)
|
@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__)
|
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")
|
@patch("homeassistant.helpers.check_config.async_check_ha_config_file")
|
||||||
async def test_check_ha_config_file_correct(mock_check, hass: HomeAssistant) -> None:
|
async def test_check_ha_config_file_correct(mock_check, hass: HomeAssistant) -> None:
|
||||||
"""Check that restart propagates to stop."""
|
"""Check that restart propagates to stop."""
|
||||||
@ -1401,148 +819,6 @@ async def test_merge_duplicate_keys(
|
|||||||
assert len(config["input_select"]) == 1
|
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:
|
async def test_merge_split_component_definition(hass: HomeAssistant) -> None:
|
||||||
"""Test components with trailing description in packages are merged."""
|
"""Test components with trailing description in packages are merged."""
|
||||||
packages = {
|
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:
|
async def test_safe_mode(hass: HomeAssistant) -> None:
|
||||||
"""Test safe mode."""
|
"""Test safe mode."""
|
||||||
assert config_util.safe_mode_enabled(hass.config.config_dir) is False
|
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_int", "sensor"),
|
||||||
("platform_int2", "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…
x
Reference in New Issue
Block a user