Allow configuring country and language in core config (#81734)

* Allow configuring country and language in core config

* Add script for updating list of countries

* Use black for formatting

* Fix quoting

* Move country codes to a separate file

* Address review comments

* Add generated/countries.py

* Get default language from owner account

* Remove unused variable

* Add script to generate list of supported languages

* Add tests

* Fix stale docsring

* Use format_python_namespace

* Correct async_user_store

* Improve typing

* Fix with_store decorator

* Initialize language in core store migration

* Fix startup

* Tweak

* Apply suggestions from code review

Co-authored-by: Franck Nijhof <git@frenck.dev>

* Update storage.py

Co-authored-by: Franck Nijhof <git@frenck.dev>
This commit is contained in:
Erik Montnemery 2022-11-24 23:25:50 +01:00 committed by GitHub
parent 09c3df7eb2
commit e1338adf1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 623 additions and 37 deletions

View file

@ -24,7 +24,7 @@ repos:
hooks:
- id: codespell
args:
- --ignore-words-list=alot,ba,bre,datas,dof,dur,ether,farenheit,haa,hass,hist,iam,iff,iif,incomfort,ines,ist,lightsensor,mut,nd,pres,pullrequests,referer,rime,ser,serie,sur,te,technik,ue,uint,visability,wan,wanna,withing
- --ignore-words-list=alot,ba,bre,datas,dof,dur,ether,farenheit,fo,haa,hass,hist,iam,iff,iif,incomfort,ines,ist,lightsensor,mut,nd,pres,pullrequests,referer,rime,ser,serie,sur,te,technik,ue,uint,visability,wan,wanna,withing
- --skip="./.*,*.csv,*.json"
- --quiet-level=2
exclude_types: [csv, json]

View file

@ -53,6 +53,7 @@ ERROR_LOG_FILENAME = "home-assistant.log"
# hass.data key for logging information.
DATA_LOGGING = "logging"
DATA_REGISTRIES_LOADED = "bootstrap_registries_loaded"
LOG_SLOW_STARTUP_INTERVAL = 60
SLOW_STARTUP_CHECK_INTERVAL = 1
@ -216,6 +217,32 @@ def open_hass_ui(hass: core.HomeAssistant) -> None:
)
async def load_registries(hass: core.HomeAssistant) -> None:
"""Load the registries and cache the result of platform.uname().processor."""
if DATA_REGISTRIES_LOADED in hass.data:
return
hass.data[DATA_REGISTRIES_LOADED] = None
def _cache_uname_processor() -> None:
"""Cache the result of platform.uname().processor in the executor.
Multiple modules call this function at startup which
executes a blocking subprocess call. This is a problem for the
asyncio event loop. By primeing the cache of uname we can
avoid the blocking call in the event loop.
"""
platform.uname().processor # pylint: disable=expression-not-assigned
# Load the registries and cache the result of platform.uname().processor
await asyncio.gather(
area_registry.async_load(hass),
device_registry.async_load(hass),
entity_registry.async_load(hass),
issue_registry.async_load(hass),
hass.async_add_executor_job(_cache_uname_processor),
)
async def async_from_config_dict(
config: ConfigType, hass: core.HomeAssistant
) -> core.HomeAssistant | None:
@ -228,6 +255,7 @@ async def async_from_config_dict(
hass.config_entries = config_entries.ConfigEntries(hass, config)
await hass.config_entries.async_initialize()
await load_registries(hass)
# Set up core.
_LOGGER.debug("Setting up %s", CORE_INTEGRATIONS)
@ -530,25 +558,6 @@ async def _async_set_up_integrations(
_LOGGER.info("Domains to be set up: %s", domains_to_setup)
def _cache_uname_processor() -> None:
"""Cache the result of platform.uname().processor in the executor.
Multiple modules call this function at startup which
executes a blocking subprocess call. This is a problem for the
asyncio event loop. By primeing the cache of uname we can
avoid the blocking call in the event loop.
"""
platform.uname().processor # pylint: disable=expression-not-assigned
# Load the registries and cache the result of platform.uname().processor
await asyncio.gather(
area_registry.async_load(hass),
device_registry.async_load(hass),
entity_registry.async_load(hass),
issue_registry.async_load(hass),
hass.async_add_executor_job(_cache_uname_processor),
)
# Initialize recorder
if "recorder" in domains_to_setup:
recorder.async_initialize_recorder(hass)

View file

@ -49,6 +49,8 @@ class CheckConfigView(HomeAssistantView):
vol.Optional("external_url"): vol.Any(cv.url_no_path, None),
vol.Optional("internal_url"): vol.Any(cv.url_no_path, None),
vol.Optional("currency"): cv.currency,
vol.Optional("country"): cv.country,
vol.Optional("language"): cv.language,
}
)
@websocket_api.async_response

View file

@ -9,20 +9,47 @@ import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.components.websocket_api.connection import ActiveConnection
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.storage import Store
DATA_STORAGE = "frontend_storage"
STORAGE_VERSION_USER_DATA = 1
@callback
def _initialize_frontend_storage(hass: HomeAssistant) -> None:
"""Set up frontend storage."""
if DATA_STORAGE in hass.data:
return
hass.data[DATA_STORAGE] = ({}, {})
async def async_setup_frontend_storage(hass: HomeAssistant) -> None:
"""Set up frontend storage."""
hass.data[DATA_STORAGE] = ({}, {})
_initialize_frontend_storage(hass)
websocket_api.async_register_command(hass, websocket_set_user_data)
websocket_api.async_register_command(hass, websocket_get_user_data)
async def async_user_store(
hass: HomeAssistant, user_id: str
) -> tuple[Store, dict[str, Any]]:
"""Access a user store."""
_initialize_frontend_storage(hass)
stores, data = hass.data[DATA_STORAGE]
if (store := stores.get(user_id)) is None:
store = stores[user_id] = Store(
hass,
STORAGE_VERSION_USER_DATA,
f"frontend.user_data_{user_id}",
)
if user_id not in data:
data[user_id] = await store.async_load() or {}
return store, data[user_id]
def with_store(orig_func: Callable) -> Callable:
"""Decorate function to provide data."""
@ -31,20 +58,11 @@ def with_store(orig_func: Callable) -> Callable:
hass: HomeAssistant, connection: ActiveConnection, msg: dict
) -> None:
"""Provide user specific data and store to function."""
stores, data = hass.data[DATA_STORAGE]
user_id = connection.user.id
if (store := stores.get(user_id)) is None:
store = stores[user_id] = Store(
hass,
STORAGE_VERSION_USER_DATA,
f"frontend.user_data_{connection.user.id}",
)
store, user_data = await async_user_store(hass, user_id)
if user_id not in data:
data[user_id] = await store.async_load() or {}
await orig_func(hass, connection, msg, store, data[user_id])
await orig_func(hass, connection, msg, store, user_data)
return with_store_func

View file

@ -27,6 +27,7 @@ from .const import (
CONF_ALLOWLIST_EXTERNAL_URLS,
CONF_AUTH_MFA_MODULES,
CONF_AUTH_PROVIDERS,
CONF_COUNTRY,
CONF_CURRENCY,
CONF_CUSTOMIZE,
CONF_CUSTOMIZE_DOMAIN,
@ -35,6 +36,7 @@ from .const import (
CONF_EXTERNAL_URL,
CONF_ID,
CONF_INTERNAL_URL,
CONF_LANGUAGE,
CONF_LATITUDE,
CONF_LEGACY_TEMPLATES,
CONF_LONGITUDE,
@ -281,6 +283,8 @@ CORE_CONFIG_SCHEMA = vol.All(
vol.Optional(CONF_MEDIA_DIRS): cv.schema_with_slug_keys(vol.IsDir()),
vol.Optional(CONF_LEGACY_TEMPLATES): cv.boolean,
vol.Optional(CONF_CURRENCY): _validate_currency,
vol.Optional(CONF_COUNTRY): cv.country,
vol.Optional(CONF_LANGUAGE): cv.language,
}
),
_filter_bad_internal_external_urls,
@ -560,6 +564,8 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non
CONF_EXTERNAL_URL,
CONF_INTERNAL_URL,
CONF_CURRENCY,
CONF_COUNTRY,
CONF_LANGUAGE,
)
):
hac.config_source = ConfigSource.YAML
@ -574,6 +580,8 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non
(CONF_MEDIA_DIRS, "media_dirs"),
(CONF_LEGACY_TEMPLATES, "legacy_templates"),
(CONF_CURRENCY, "currency"),
(CONF_COUNTRY, "country"),
(CONF_LANGUAGE, "language"),
):
if key in config:
setattr(hac, attr, config[key])

View file

@ -121,6 +121,7 @@ CONF_CONDITIONS: Final = "conditions"
CONF_CONTINUE_ON_ERROR: Final = "continue_on_error"
CONF_CONTINUE_ON_TIMEOUT: Final = "continue_on_timeout"
CONF_COUNT: Final = "count"
CONF_COUNTRY: Final = "country"
CONF_COVERS: Final = "covers"
CONF_CURRENCY: Final = "currency"
CONF_CUSTOMIZE: Final = "customize"
@ -175,6 +176,7 @@ CONF_IF: Final = "if"
CONF_INCLUDE: Final = "include"
CONF_INTERNAL_URL: Final = "internal_url"
CONF_IP_ADDRESS: Final = "ip_address"
CONF_LANGUAGE: Final = "language"
CONF_LATITUDE: Final = "latitude"
CONF_LEGACY_TEMPLATES: Final = "legacy_templates"
CONF_LIGHTS: Final = "lights"

View file

@ -15,6 +15,7 @@ from collections.abc import (
Iterable,
Mapping,
)
from contextlib import suppress
from contextvars import ContextVar
import datetime
import enum
@ -113,7 +114,7 @@ CALLBACK_TYPE = Callable[[], None] # pylint: disable=invalid-name
CORE_STORAGE_KEY = "core.config"
CORE_STORAGE_VERSION = 1
CORE_STORAGE_MINOR_VERSION = 2
CORE_STORAGE_MINOR_VERSION = 3
DOMAIN = "homeassistant"
@ -1807,6 +1808,8 @@ class Config:
self.internal_url: str | None = None
self.external_url: str | None = None
self.currency: str = "EUR"
self.country: str | None = None
self.language: str = "en"
self.config_source: ConfigSource = ConfigSource.DEFAULT
@ -1913,6 +1916,8 @@ class Config:
"external_url": self.external_url,
"internal_url": self.internal_url,
"currency": self.currency,
"country": self.country,
"language": self.language,
}
def set_time_zone(self, time_zone_str: str) -> None:
@ -1938,6 +1943,8 @@ class Config:
external_url: str | dict[Any, Any] | None = _UNDEF,
internal_url: str | dict[Any, Any] | None = _UNDEF,
currency: str | None = None,
country: str | dict[Any, Any] | None = _UNDEF,
language: str | None = None,
) -> None:
"""Update the configuration from a dictionary."""
self.config_source = source
@ -1962,6 +1969,10 @@ class Config:
self.internal_url = cast(Optional[str], internal_url)
if currency is not None:
self.currency = currency
if country is not _UNDEF:
self.country = cast(Optional[str], country)
if language is not None:
self.language = language
async def async_update(self, **kwargs: Any) -> None:
"""Update the configuration from a dictionary."""
@ -1999,6 +2010,8 @@ class Config:
external_url=data.get("external_url", _UNDEF),
internal_url=data.get("internal_url", _UNDEF),
currency=data.get("currency"),
country=data.get("country"),
language=data.get("language"),
)
async def _async_store(self) -> None:
@ -2015,6 +2028,8 @@ class Config:
"external_url": self.external_url,
"internal_url": self.internal_url,
"currency": self.currency,
"country": self.country,
"language": self.language,
}
await self._store.async_save(data)
@ -2053,6 +2068,35 @@ class Config:
data["unit_system_v2"] = self._original_unit_system
if data["unit_system_v2"] == _CONF_UNIT_SYSTEM_IMPERIAL:
data["unit_system_v2"] = _CONF_UNIT_SYSTEM_US_CUSTOMARY
if old_major_version == 1 and old_minor_version < 3:
# In 1.3, we add the key "language", initialize it from the owner account
data["language"] = "en"
try:
owner = await self.hass.auth.async_get_owner()
if owner is not None:
# pylint: disable-next=import-outside-toplevel
from .components.frontend import storage as frontend_store
# pylint: disable-next=import-outside-toplevel
from .helpers import config_validation as cv
_, owner_data = await frontend_store.async_user_store(
self.hass, owner.id
)
if (
"language" in owner_data
and "language" in owner_data["language"]
):
with suppress(vol.InInvalid):
# pylint: disable-next=protected-access
data["language"] = cv.language(
owner_data["language"]["language"]
)
# pylint: disable-next=broad-except
except Exception:
_LOGGER.exception("Unexpected error during core config migration")
if old_major_version > 1:
raise NotImplementedError
return data

View file

@ -0,0 +1,260 @@
"""This file is automatically generated.
To update, run python3 -m script.countries
The values are directly corresponding to the ISO 3166 standard. If you need changes
to the political situation in the world, please contact the ISO 3166 working group.
"""
COUNTRIES = {
"AD",
"AE",
"AF",
"AG",
"AI",
"AL",
"AM",
"AO",
"AQ",
"AR",
"AS",
"AT",
"AU",
"AW",
"AX",
"AZ",
"BA",
"BB",
"BD",
"BE",
"BF",
"BG",
"BH",
"BI",
"BJ",
"BL",
"BM",
"BN",
"BO",
"BQ",
"BR",
"BS",
"BT",
"BV",
"BW",
"BY",
"BZ",
"CA",
"CC",
"CD",
"CF",
"CG",
"CH",
"CI",
"CK",
"CL",
"CM",
"CN",
"CO",
"CR",
"CU",
"CV",
"CW",
"CX",
"CY",
"CZ",
"DE",
"DJ",
"DK",
"DM",
"DO",
"DZ",
"EC",
"EE",
"EG",
"EH",
"ER",
"ES",
"ET",
"FI",
"FJ",
"FK",
"FM",
"FO",
"FR",
"GA",
"GB",
"GD",
"GE",
"GF",
"GG",
"GH",
"GI",
"GL",
"GM",
"GN",
"GP",
"GQ",
"GR",
"GS",
"GT",
"GU",
"GW",
"GY",
"HK",
"HM",
"HN",
"HR",
"HT",
"HU",
"ID",
"IE",
"IL",
"IM",
"IN",
"IO",
"IQ",
"IR",
"IS",
"IT",
"JE",
"JM",
"JO",
"JP",
"KE",
"KG",
"KH",
"KI",
"KM",
"KN",
"KP",
"KR",
"KW",
"KY",
"KZ",
"LA",
"LB",
"LC",
"LI",
"LK",
"LR",
"LS",
"LT",
"LU",
"LV",
"LY",
"MA",
"MC",
"MD",
"ME",
"MF",
"MG",
"MH",
"MK",
"ML",
"MM",
"MN",
"MO",
"MP",
"MQ",
"MR",
"MS",
"MT",
"MU",
"MV",
"MW",
"MX",
"MY",
"MZ",
"NA",
"NC",
"NE",
"NF",
"NG",
"NI",
"NL",
"NO",
"NP",
"NR",
"NU",
"NZ",
"OM",
"PA",
"PE",
"PF",
"PG",
"PH",
"PK",
"PL",
"PM",
"PN",
"PR",
"PS",
"PT",
"PW",
"PY",
"QA",
"RE",
"RO",
"RS",
"RU",
"RW",
"SA",
"SB",
"SC",
"SD",
"SE",
"SG",
"SH",
"SI",
"SJ",
"SK",
"SL",
"SM",
"SN",
"SO",
"SR",
"SS",
"ST",
"SV",
"SX",
"SY",
"SZ",
"TC",
"TD",
"TF",
"TG",
"TH",
"TJ",
"TK",
"TL",
"TM",
"TN",
"TO",
"TR",
"TT",
"TV",
"TW",
"TZ",
"UA",
"UG",
"UM",
"US",
"UY",
"UZ",
"VA",
"VC",
"VE",
"VG",
"VI",
"VN",
"VU",
"WF",
"WS",
"YE",
"YT",
"ZA",
"ZM",
"ZW",
}

View file

@ -0,0 +1,68 @@
"""This file is automatically generated.
To update, run python3 -m script.languages [frontend_tag]
"""
LANGUAGES = {
"af",
"ar",
"bg",
"bn",
"bs",
"ca",
"cs",
"cy",
"da",
"de",
"el",
"en",
"en-GB",
"eo",
"es",
"es-419",
"et",
"eu",
"fa",
"fi",
"fr",
"fy",
"gl",
"gsw",
"he",
"hi",
"hr",
"hu",
"hy",
"id",
"is",
"it",
"ja",
"ka",
"ko",
"lb",
"lt",
"lv",
"ml",
"nb",
"nl",
"nn",
"pl",
"pt",
"pt-BR",
"ro",
"ru",
"sk",
"sl",
"sr",
"sr-Latn",
"sv",
"ta",
"te",
"th",
"tr",
"uk",
"ur",
"vi",
"zh-Hans",
"zh-Hant",
}

View file

@ -89,6 +89,8 @@ from homeassistant.const import (
from homeassistant.core import split_entity_id, valid_entity_id
from homeassistant.exceptions import TemplateError
from homeassistant.generated import currencies
from homeassistant.generated.countries import COUNTRIES
from homeassistant.generated.languages import LANGUAGES
from homeassistant.util import raise_if_invalid_path, slugify as util_slugify
import homeassistant.util.dt as dt_util
@ -1662,3 +1664,7 @@ currency = vol.In(
historic_currency = vol.In(
currencies.HISTORIC_CURRENCIES, msg="invalid ISO 4217 formatted historic currency"
)
country = vol.In(COUNTRIES, msg="invalid ISO 3166 formatted country")
language = vol.In(LANGUAGES, msg="invalid RFC 5646 formatted language")

27
script/countries.py Normal file
View file

@ -0,0 +1,27 @@
"""Helper script to update country list.
ISO does not publish a machine readable list free of charge, so the list is generated
with help of the pycountry package.
"""
from pathlib import Path
import pycountry
from .hassfest.serializer import format_python_namespace
countries = {x.alpha_2 for x in pycountry.countries}
generator_string = """script.countries
The values are directly corresponding to the ISO 3166 standard. If you need changes
to the political situation in the world, please contact the ISO 3166 working group.
"""
Path("homeassistant/generated/countries.py").write_text(
format_python_namespace(
{
"COUNTRIES": countries,
},
generator=generator_string,
)
)

25
script/languages.py Normal file
View file

@ -0,0 +1,25 @@
"""Helper script to update language list from the frontend source."""
import json
from pathlib import Path
import sys
import requests
from .hassfest.serializer import format_python_namespace
tag = sys.argv[1] if len(sys.argv) > 1 else "dev"
req = requests.get(
f"https://raw.githubusercontent.com/home-assistant/frontend/{tag}/src/translations/translationMetadata.json"
)
data = json.loads(req.content)
languages = set(data.keys())
Path("homeassistant/generated/languages.py").write_text(
format_python_namespace(
{
"LANGUAGES": languages,
},
generator="script.languages [frontend_tag]",
)
)

View file

@ -22,7 +22,7 @@ from unittest.mock import AsyncMock, Mock, patch
from aiohttp.test_utils import unused_port as get_test_instance_port # noqa: F401
import voluptuous as vol
from homeassistant import auth, config_entries, core as ha, loader
from homeassistant import auth, bootstrap, config_entries, core as ha, loader
from homeassistant.auth import (
auth_store,
models as auth_models,
@ -306,6 +306,7 @@ async def async_test_home_assistant(loop, load_registries=True):
issue_registry.async_load(hass),
)
await hass.async_block_till_done()
hass.data[bootstrap.DATA_REGISTRIES_LOADED] = None
hass.state = ha.CoreState.running

View file

@ -60,6 +60,8 @@ async def test_websocket_core_update(hass, client):
assert hass.config.external_url != "https://www.example.com"
assert hass.config.internal_url != "http://example.com"
assert hass.config.currency == "EUR"
assert hass.config.country != "SE"
assert hass.config.language != "sv"
with patch("homeassistant.util.dt.set_default_time_zone") as mock_set_tz:
await client.send_json(
@ -75,6 +77,8 @@ async def test_websocket_core_update(hass, client):
"external_url": "https://www.example.com",
"internal_url": "http://example.local",
"currency": "USD",
"country": "SE",
"language": "sv",
}
)

View file

@ -1344,3 +1344,27 @@ def test_historic_currency():
for value in ("DEM", "NLG"):
assert schema(value)
def test_country():
"""Test country validator."""
schema = vol.Schema(cv.country)
for value in (None, "Candyland", "USA"):
with pytest.raises(vol.MultipleInvalid):
schema(value)
for value in ("NL", "SE"):
assert schema(value)
def test_language():
"""Test language validator."""
schema = vol.Schema(cv.language)
for value in (None, "Klingon", "english"):
with pytest.raises(vol.MultipleInvalid):
schema(value)
for value in ("en", "sv"):
assert schema(value)

View file

@ -40,7 +40,7 @@ from homeassistant.util.unit_system import (
)
from homeassistant.util.yaml import SECRET_YAML
from tests.common import get_test_config_dir, patch_yaml_files
from tests.common import MockUser, get_test_config_dir, patch_yaml_files
CONFIG_DIR = get_test_config_dir()
YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE)
@ -214,6 +214,8 @@ def test_core_config_schema():
{"customize": "bla"},
{"customize": {"light.sensor": 100}},
{"customize": {"entity_id": []}},
{"country": "xx"},
{"language": "xx"},
):
with pytest.raises(MultipleInvalid):
config_util.CORE_CONFIG_SCHEMA(value)
@ -228,6 +230,8 @@ def test_core_config_schema():
CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC,
"currency": "USD",
"customize": {"sensor.temperature": {"hidden": True}},
"country": "SE",
"language": "sv",
}
)
@ -393,9 +397,12 @@ async def test_loading_configuration_from_storage(hass, hass_storage):
"external_url": "https://www.example.com",
"internal_url": "http://example.local",
"currency": "EUR",
"country": "SE",
"language": "sv",
},
"key": "core.config",
"version": 1,
"minor_version": 3,
}
await config_util.async_process_ha_core_config(
hass, {"allowlist_external_dirs": "/etc"}
@ -410,6 +417,8 @@ async def test_loading_configuration_from_storage(hass, hass_storage):
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 len(hass.config.allowlist_external_dirs) == 3
assert "/etc" in hass.config.allowlist_external_dirs
assert hass.config.config_source is ConfigSource.STORAGE
@ -475,10 +484,15 @@ async def test_migration_and_updating_configuration(hass, hass_storage):
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"
expected_new_core_data["minor_version"] = 2
expected_new_core_data["minor_version"] = 3
# defaults for country and language
expected_new_core_data["data"]["country"] = None
expected_new_core_data["data"]["language"] = "en"
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"
async def test_override_stored_configuration(hass, hass_storage):
@ -527,6 +541,8 @@ async def test_loading_configuration(hass):
"media_dirs": {"mymedia": "/usr"},
"legacy_templates": True,
"currency": "EUR",
"country": "SE",
"language": "sv",
},
)
@ -545,6 +561,74 @@ async def test_loading_configuration(hass):
assert hass.config.config_source is ConfigSource.YAML
assert hass.config.legacy_templates is True
assert hass.config.currency == "EUR"
assert hass.config.country == "SE"
assert hass.config.language == "sv"
@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, hass_storage, minor_version, users, user_data, default_language
):
"""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):

View file

@ -948,6 +948,8 @@ async def test_config_defaults():
assert config.safe_mode is False
assert config.legacy_templates is False
assert config.currency == "EUR"
assert config.country is None
assert config.language == "en"
async def test_config_path_with_file():
@ -989,6 +991,8 @@ async def test_config_as_dict():
"external_url": None,
"internal_url": None,
"currency": "EUR",
"country": None,
"language": "en",
}
assert expected == config.as_dict()