Skip to content

Commit

Permalink
Allow configuring country and language in core config (home-assistant…
Browse files Browse the repository at this point in the history
…#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 <[email protected]>

* Update storage.py

Co-authored-by: Franck Nijhof <[email protected]>
  • Loading branch information
emontnemery and frenck authored Nov 24, 2022
1 parent 09c3df7 commit e1338ad
Show file tree
Hide file tree
Showing 17 changed files with 623 additions and 37 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
47 changes: 28 additions & 19 deletions homeassistant/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@

# 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
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/config/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ async def post(self, request):
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
Expand Down
44 changes: 31 additions & 13 deletions homeassistant/components/frontend/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,47 @@

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


async def async_setup_frontend_storage(hass: HomeAssistant) -> None:
@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."""
_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."""

Expand All @@ -31,20 +58,11 @@ async def with_store_func(
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}",
)

if user_id not in data:
data[user_id] = await store.async_load() or {}
store, user_data = await async_user_store(hass, user_id)

await orig_func(hass, connection, msg, store, data[user_id])
await orig_func(hass, connection, msg, store, user_data)

return with_store_func

Expand Down
8 changes: 8 additions & 0 deletions homeassistant/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
CONF_ALLOWLIST_EXTERNAL_URLS,
CONF_AUTH_MFA_MODULES,
CONF_AUTH_PROVIDERS,
CONF_COUNTRY,
CONF_CURRENCY,
CONF_CUSTOMIZE,
CONF_CUSTOMIZE_DOMAIN,
Expand All @@ -35,6 +36,7 @@
CONF_EXTERNAL_URL,
CONF_ID,
CONF_INTERNAL_URL,
CONF_LANGUAGE,
CONF_LATITUDE,
CONF_LEGACY_TEMPLATES,
CONF_LONGITUDE,
Expand Down Expand Up @@ -281,6 +283,8 @@ def _validate_currency(data: Any) -> Any:
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,
Expand Down Expand Up @@ -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
Expand All @@ -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])
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ class Platform(StrEnum):
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"
Expand Down Expand Up @@ -175,6 +176,7 @@ class Platform(StrEnum):
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"
Expand Down
46 changes: 45 additions & 1 deletion homeassistant/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Iterable,
Mapping,
)
from contextlib import suppress
from contextvars import ContextVar
import datetime
import enum
Expand Down Expand Up @@ -113,7 +114,7 @@

CORE_STORAGE_KEY = "core.config"
CORE_STORAGE_VERSION = 1
CORE_STORAGE_MINOR_VERSION = 2
CORE_STORAGE_MINOR_VERSION = 3

DOMAIN = "homeassistant"

Expand Down Expand Up @@ -1807,6 +1808,8 @@ def __init__(self, hass: HomeAssistant) -> None:
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

Expand Down Expand Up @@ -1913,6 +1916,8 @@ def as_dict(self) -> dict[str, Any]:
"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:
Expand All @@ -1938,6 +1943,8 @@ def _update(
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
Expand All @@ -1962,6 +1969,10 @@ def _update(
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."""
Expand Down Expand Up @@ -1999,6 +2010,8 @@ async def async_load(self) -> None:
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:
Expand All @@ -2015,6 +2028,8 @@ async def _async_store(self) -> None:
"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)
Expand Down Expand Up @@ -2053,6 +2068,35 @@ async def _async_migrate_func(
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
Expand Down
Loading

0 comments on commit e1338ad

Please sign in to comment.