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:
parent
09c3df7eb2
commit
e1338adf1a
17 changed files with 623 additions and 37 deletions
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
260
homeassistant/generated/countries.py
Normal file
260
homeassistant/generated/countries.py
Normal 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",
|
||||
}
|
68
homeassistant/generated/languages.py
Normal file
68
homeassistant/generated/languages.py
Normal 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",
|
||||
}
|
|
@ -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
27
script/countries.py
Normal 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
25
script/languages.py
Normal 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]",
|
||||
)
|
||||
)
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue