mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 18:57:06 +00:00
Add state_translated function to jinja templates (#96906)
* Add state_translated jinja function * Add tests for load_state_translations_to_cache and get_cached_translations * Cleanup state_translated template * Add tests for state_translated jinja function * Apply black formatting * Improve code quality * Apply suggestions from code review Co-authored-by: Erik Montnemery <erik@montnemery.com> * Apply suggestions from code review * Prevent invalid components from loading translations * Refactor loading translations to cache * Adjust code issues * Update homeassistant/helpers/translation.py Co-authored-by: Erik Montnemery <erik@montnemery.com> * Refactor listeners that trigger translation loading * Apply suggestions from code review Co-authored-by: Erik Montnemery <erik@montnemery.com> * Apply suggestions from code review * Adjust invalid function calls, fix code styling * Adjust code quality * Extract async_translate_state function * Apply suggestions from code review Co-authored-by: Erik Montnemery <erik@montnemery.com> * Apply suggestions from code review * Fix tests * Fix tests --------- Co-authored-by: Piotr Machowski <PiotrMachowski@users.noreply.github.com> Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
d1f098c11f
commit
a2f4e99994
@ -35,6 +35,7 @@ from .helpers import (
|
|||||||
recorder,
|
recorder,
|
||||||
restore_state,
|
restore_state,
|
||||||
template,
|
template,
|
||||||
|
translation,
|
||||||
)
|
)
|
||||||
from .helpers.dispatcher import async_dispatcher_send
|
from .helpers.dispatcher import async_dispatcher_send
|
||||||
from .helpers.typing import ConfigType
|
from .helpers.typing import ConfigType
|
||||||
@ -291,6 +292,7 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
|
|||||||
platform.uname().processor # pylint: disable=expression-not-assigned
|
platform.uname().processor # pylint: disable=expression-not-assigned
|
||||||
|
|
||||||
# Load the registries and cache the result of platform.uname().processor
|
# Load the registries and cache the result of platform.uname().processor
|
||||||
|
translation.async_setup(hass)
|
||||||
entity.async_setup(hass)
|
entity.async_setup(hass)
|
||||||
template.async_setup(hass)
|
template.async_setup(hass)
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
|
@ -80,6 +80,7 @@ from homeassistant.util.thread import ThreadWithException
|
|||||||
|
|
||||||
from . import area_registry, device_registry, entity_registry, location as loc_helper
|
from . import area_registry, device_registry, entity_registry, location as loc_helper
|
||||||
from .singleton import singleton
|
from .singleton import singleton
|
||||||
|
from .translation import async_translate_state
|
||||||
from .typing import TemplateVarsType
|
from .typing import TemplateVarsType
|
||||||
|
|
||||||
# mypy: allow-untyped-defs, no-check-untyped-defs
|
# mypy: allow-untyped-defs, no-check-untyped-defs
|
||||||
@ -894,6 +895,36 @@ class AllStates:
|
|||||||
return "<template AllStates>"
|
return "<template AllStates>"
|
||||||
|
|
||||||
|
|
||||||
|
class StateTranslated:
|
||||||
|
"""Class to represent a translated state in a template."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
|
"""Initialize all states."""
|
||||||
|
self._hass = hass
|
||||||
|
|
||||||
|
def __call__(self, entity_id: str) -> str | None:
|
||||||
|
"""Retrieve translated state if available."""
|
||||||
|
state = _get_state_if_valid(self._hass, entity_id)
|
||||||
|
|
||||||
|
if state is None:
|
||||||
|
return STATE_UNKNOWN
|
||||||
|
|
||||||
|
state_value = state.state
|
||||||
|
domain = state.domain
|
||||||
|
device_class = state.attributes.get("device_class")
|
||||||
|
entry = entity_registry.async_get(self._hass).async_get(entity_id)
|
||||||
|
platform = None if entry is None else entry.platform
|
||||||
|
translation_key = None if entry is None else entry.translation_key
|
||||||
|
|
||||||
|
return async_translate_state(
|
||||||
|
self._hass, state_value, domain, platform, translation_key, device_class
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
"""Representation of Translated state."""
|
||||||
|
return "<template StateTranslated>"
|
||||||
|
|
||||||
|
|
||||||
class DomainStates:
|
class DomainStates:
|
||||||
"""Class to expose a specific HA domain as attributes."""
|
"""Class to expose a specific HA domain as attributes."""
|
||||||
|
|
||||||
@ -2626,6 +2657,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
|||||||
"is_state_attr",
|
"is_state_attr",
|
||||||
"state_attr",
|
"state_attr",
|
||||||
"states",
|
"states",
|
||||||
|
"state_translated",
|
||||||
"has_value",
|
"has_value",
|
||||||
"utcnow",
|
"utcnow",
|
||||||
"now",
|
"now",
|
||||||
@ -2676,6 +2708,8 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
|||||||
self.filters["state_attr"] = self.globals["state_attr"]
|
self.filters["state_attr"] = self.globals["state_attr"]
|
||||||
self.globals["states"] = AllStates(hass)
|
self.globals["states"] = AllStates(hass)
|
||||||
self.filters["states"] = self.globals["states"]
|
self.filters["states"] = self.globals["states"]
|
||||||
|
self.globals["state_translated"] = StateTranslated(hass)
|
||||||
|
self.filters["state_translated"] = self.globals["state_translated"]
|
||||||
self.globals["has_value"] = hassfunction(has_value)
|
self.globals["has_value"] = hassfunction(has_value)
|
||||||
self.filters["has_value"] = self.globals["has_value"]
|
self.filters["has_value"] = self.globals["has_value"]
|
||||||
self.tests["has_value"] = hassfunction(has_value, pass_eval_context)
|
self.tests["has_value"] = hassfunction(has_value, pass_eval_context)
|
||||||
@ -2688,7 +2722,9 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
|
|||||||
|
|
||||||
def is_safe_callable(self, obj):
|
def is_safe_callable(self, obj):
|
||||||
"""Test if callback is safe."""
|
"""Test if callback is safe."""
|
||||||
return isinstance(obj, AllStates) or super().is_safe_callable(obj)
|
return isinstance(
|
||||||
|
obj, (AllStates, StateTranslated)
|
||||||
|
) or super().is_safe_callable(obj)
|
||||||
|
|
||||||
def is_safe_attribute(self, obj, attr, value):
|
def is_safe_attribute(self, obj, attr, value):
|
||||||
"""Test if attribute is safe."""
|
"""Test if attribute is safe."""
|
||||||
|
@ -7,7 +7,13 @@ import logging
|
|||||||
import string
|
import string
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.const import (
|
||||||
|
EVENT_COMPONENT_LOADED,
|
||||||
|
EVENT_CORE_CONFIG_UPDATE,
|
||||||
|
STATE_UNAVAILABLE,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
)
|
||||||
|
from homeassistant.core import Event, HomeAssistant, callback
|
||||||
from homeassistant.loader import (
|
from homeassistant.loader import (
|
||||||
Integration,
|
Integration,
|
||||||
async_get_config_flows,
|
async_get_config_flows,
|
||||||
@ -199,12 +205,11 @@ class _TranslationCache:
|
|||||||
self.cache: dict[str, dict[str, dict[str, dict[str, str]]]] = {}
|
self.cache: dict[str, dict[str, dict[str, dict[str, str]]]] = {}
|
||||||
self.lock = asyncio.Lock()
|
self.lock = asyncio.Lock()
|
||||||
|
|
||||||
async def async_fetch(
|
async def async_load(
|
||||||
self,
|
self,
|
||||||
language: str,
|
language: str,
|
||||||
category: str,
|
|
||||||
components: set[str],
|
components: set[str],
|
||||||
) -> dict[str, str]:
|
) -> None:
|
||||||
"""Load resources into the cache."""
|
"""Load resources into the cache."""
|
||||||
loaded = self.loaded.setdefault(language, set())
|
loaded = self.loaded.setdefault(language, set())
|
||||||
if components_to_load := components - loaded:
|
if components_to_load := components - loaded:
|
||||||
@ -218,6 +223,24 @@ class _TranslationCache:
|
|||||||
if components_to_load := components - loaded:
|
if components_to_load := components - loaded:
|
||||||
await self._async_load(language, components_to_load)
|
await self._async_load(language, components_to_load)
|
||||||
|
|
||||||
|
async def async_fetch(
|
||||||
|
self,
|
||||||
|
language: str,
|
||||||
|
category: str,
|
||||||
|
components: set[str],
|
||||||
|
) -> dict[str, str]:
|
||||||
|
"""Load resources into the cache and return them."""
|
||||||
|
await self.async_load(language, components)
|
||||||
|
|
||||||
|
return self.get_cached(language, category, components)
|
||||||
|
|
||||||
|
def get_cached(
|
||||||
|
self,
|
||||||
|
language: str,
|
||||||
|
category: str,
|
||||||
|
components: set[str],
|
||||||
|
) -> dict[str, str]:
|
||||||
|
"""Read resources from the cache."""
|
||||||
category_cache = self.cache.get(language, {}).get(category, {})
|
category_cache = self.cache.get(language, {}).get(category, {})
|
||||||
# If only one component was requested, return it directly
|
# If only one component was requested, return it directly
|
||||||
# to avoid merging the dictionaries and keeping additional
|
# to avoid merging the dictionaries and keeping additional
|
||||||
@ -354,14 +377,67 @@ async def async_get_translations(
|
|||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
"""Return all backend translations.
|
"""Return all backend translations.
|
||||||
|
|
||||||
If integration specified, load it for that one.
|
If integration is specified, load it for that one.
|
||||||
Otherwise default to loaded integrations combined with config flow
|
Otherwise, default to loaded integrations combined with config flow
|
||||||
integrations if config_flow is true.
|
integrations if config_flow is true.
|
||||||
"""
|
"""
|
||||||
|
if integrations is None and config_flow:
|
||||||
|
components = (await async_get_config_flows(hass)) - hass.config.components
|
||||||
|
else:
|
||||||
|
components = _async_get_components(hass, category, integrations)
|
||||||
|
|
||||||
|
cache: _TranslationCache = hass.data[TRANSLATION_FLATTEN_CACHE]
|
||||||
|
|
||||||
|
return await cache.async_fetch(language, category, components)
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_load_translations(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
language: str,
|
||||||
|
category: str,
|
||||||
|
integration: str | None,
|
||||||
|
) -> None:
|
||||||
|
"""Prime backend translation cache.
|
||||||
|
|
||||||
|
If integration is not specified, translation cache is primed for all loaded integrations.
|
||||||
|
"""
|
||||||
|
components = _async_get_components(
|
||||||
|
hass, category, [integration] if integration is not None else None
|
||||||
|
)
|
||||||
|
|
||||||
|
cache = hass.data[TRANSLATION_FLATTEN_CACHE]
|
||||||
|
await cache.async_load(language, components)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_get_cached_translations(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
language: str,
|
||||||
|
category: str,
|
||||||
|
integration: str | None = None,
|
||||||
|
) -> dict[str, str]:
|
||||||
|
"""Return all cached backend translations.
|
||||||
|
|
||||||
|
If integration is specified, return translations for it.
|
||||||
|
Otherwise, default to all loaded integrations.
|
||||||
|
"""
|
||||||
|
components = _async_get_components(
|
||||||
|
hass, category, [integration] if integration is not None else None
|
||||||
|
)
|
||||||
|
|
||||||
|
cache: _TranslationCache = hass.data[TRANSLATION_FLATTEN_CACHE]
|
||||||
|
return cache.get_cached(language, category, components)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_get_components(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
category: str,
|
||||||
|
integrations: Iterable[str] | None = None,
|
||||||
|
) -> set[str]:
|
||||||
|
"""Return a set of components for which translations should be loaded."""
|
||||||
if integrations is not None:
|
if integrations is not None:
|
||||||
components = set(integrations)
|
components = set(integrations)
|
||||||
elif config_flow:
|
|
||||||
components = (await async_get_config_flows(hass)) - hass.config.components
|
|
||||||
elif category in ("state", "entity_component", "services"):
|
elif category in ("state", "entity_component", "services"):
|
||||||
components = hass.config.components
|
components = hass.config.components
|
||||||
else:
|
else:
|
||||||
@ -369,10 +445,91 @@ async def async_get_translations(
|
|||||||
components = {
|
components = {
|
||||||
component for component in hass.config.components if "." not in component
|
component for component in hass.config.components if "." not in component
|
||||||
}
|
}
|
||||||
|
return components
|
||||||
|
|
||||||
if TRANSLATION_FLATTEN_CACHE in hass.data:
|
|
||||||
cache: _TranslationCache = hass.data[TRANSLATION_FLATTEN_CACHE]
|
|
||||||
else:
|
|
||||||
cache = hass.data[TRANSLATION_FLATTEN_CACHE] = _TranslationCache(hass)
|
|
||||||
|
|
||||||
return await cache.async_fetch(language, category, components)
|
async def _async_load_state_translations_to_cache(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
language: str,
|
||||||
|
integration: str | None,
|
||||||
|
) -> None:
|
||||||
|
"""Load state translations to cache."""
|
||||||
|
await _async_load_translations(hass, language, "entity", integration)
|
||||||
|
await _async_load_translations(hass, language, "state", integration)
|
||||||
|
await _async_load_translations(hass, language, "entity_component", integration)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_setup(hass: HomeAssistant) -> None:
|
||||||
|
"""Create translation cache and register listeners for translation loaders.
|
||||||
|
|
||||||
|
Listeners load translations for every loaded component and after config change.
|
||||||
|
"""
|
||||||
|
|
||||||
|
hass.data[TRANSLATION_FLATTEN_CACHE] = _TranslationCache(hass)
|
||||||
|
|
||||||
|
async def load_translations(event: Event) -> None:
|
||||||
|
if "language" in event.data:
|
||||||
|
language = hass.config.language
|
||||||
|
_LOGGER.debug("Loading translations for language: %s", language)
|
||||||
|
await _async_load_state_translations_to_cache(hass, language, None)
|
||||||
|
|
||||||
|
async def load_translations_for_component(event: Event) -> None:
|
||||||
|
component = event.data.get("component")
|
||||||
|
# Platforms don't have their own translations, skip them
|
||||||
|
if component is None or "." in str(component):
|
||||||
|
return
|
||||||
|
language = hass.config.language
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Loading translations for language: %s and component: %s",
|
||||||
|
hass.config.language,
|
||||||
|
component,
|
||||||
|
)
|
||||||
|
await _async_load_state_translations_to_cache(hass, language, component)
|
||||||
|
|
||||||
|
hass.bus.async_listen(EVENT_COMPONENT_LOADED, load_translations_for_component)
|
||||||
|
hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, load_translations)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_translate_state(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
state: str,
|
||||||
|
domain: str,
|
||||||
|
platform: str | None,
|
||||||
|
translation_key: str | None,
|
||||||
|
device_class: str | None,
|
||||||
|
) -> str:
|
||||||
|
"""Translate provided state using cached translations for currently selected language."""
|
||||||
|
if state in [STATE_UNAVAILABLE, STATE_UNKNOWN]:
|
||||||
|
return state
|
||||||
|
language = hass.config.language
|
||||||
|
if platform is not None and translation_key is not None:
|
||||||
|
localize_key = (
|
||||||
|
f"component.{platform}.entity.{domain}.{translation_key}.state.{state}"
|
||||||
|
)
|
||||||
|
translations = async_get_cached_translations(hass, language, "entity")
|
||||||
|
if localize_key in translations:
|
||||||
|
return translations[localize_key]
|
||||||
|
|
||||||
|
translations = async_get_cached_translations(hass, language, "entity_component")
|
||||||
|
if device_class is not None:
|
||||||
|
localize_key = (
|
||||||
|
f"component.{domain}.entity_component.{device_class}.state.{state}"
|
||||||
|
)
|
||||||
|
if localize_key in translations:
|
||||||
|
return translations[localize_key]
|
||||||
|
localize_key = f"component.{domain}.entity_component._.state.{state}"
|
||||||
|
if localize_key in translations:
|
||||||
|
return translations[localize_key]
|
||||||
|
|
||||||
|
translations = async_get_cached_translations(hass, language, "state", domain)
|
||||||
|
if device_class is not None:
|
||||||
|
localize_key = f"component.{domain}.state.{device_class}.{state}"
|
||||||
|
if localize_key in translations:
|
||||||
|
return translations[localize_key]
|
||||||
|
localize_key = f"component.{domain}.state._.{state}"
|
||||||
|
if localize_key in translations:
|
||||||
|
return translations[localize_key]
|
||||||
|
|
||||||
|
return state
|
||||||
|
@ -69,6 +69,7 @@ from homeassistant.helpers import (
|
|||||||
restore_state,
|
restore_state,
|
||||||
restore_state as rs,
|
restore_state as rs,
|
||||||
storage,
|
storage,
|
||||||
|
translation,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.dispatcher import (
|
from homeassistant.helpers.dispatcher import (
|
||||||
async_dispatcher_connect,
|
async_dispatcher_connect,
|
||||||
@ -267,6 +268,11 @@ async def async_test_home_assistant(event_loop, load_registries=True):
|
|||||||
# Load the registries
|
# Load the registries
|
||||||
entity.async_setup(hass)
|
entity.async_setup(hass)
|
||||||
loader.async_setup(hass)
|
loader.async_setup(hass)
|
||||||
|
|
||||||
|
# setup translation cache instead of calling translation.async_setup(hass)
|
||||||
|
hass.data[translation.TRANSLATION_FLATTEN_CACHE] = translation._TranslationCache(
|
||||||
|
hass
|
||||||
|
)
|
||||||
if load_registries:
|
if load_registries:
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.helpers.storage.Store.async_load", return_value=None
|
"homeassistant.helpers.storage.Store.async_load", return_value=None
|
||||||
|
@ -38,6 +38,7 @@ from homeassistant.helpers import (
|
|||||||
entity,
|
entity,
|
||||||
entity_registry as er,
|
entity_registry as er,
|
||||||
template,
|
template,
|
||||||
|
translation,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.entity_platform import EntityPlatform
|
from homeassistant.helpers.entity_platform import EntityPlatform
|
||||||
from homeassistant.helpers.json import json_dumps
|
from homeassistant.helpers.json import json_dumps
|
||||||
@ -1955,6 +1956,129 @@ def test_states_function(hass: HomeAssistant) -> None:
|
|||||||
assert tpl.async_render() == "available"
|
assert tpl.async_render() == "available"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_state_translated(
|
||||||
|
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||||
|
):
|
||||||
|
"""Test state_translated method."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"binary_sensor",
|
||||||
|
{
|
||||||
|
"binary_sensor": {
|
||||||
|
"platform": "group",
|
||||||
|
"name": "Grouped",
|
||||||
|
"entities": ["binary_sensor.first", "binary_sensor.second"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await translation._async_load_state_translations_to_cache(hass, "en", None)
|
||||||
|
|
||||||
|
hass.states.async_set("switch.without_translations", "on", attributes={})
|
||||||
|
hass.states.async_set("binary_sensor.without_device_class", "on", attributes={})
|
||||||
|
hass.states.async_set(
|
||||||
|
"binary_sensor.with_device_class", "on", attributes={"device_class": "motion"}
|
||||||
|
)
|
||||||
|
hass.states.async_set(
|
||||||
|
"binary_sensor.with_unknown_device_class",
|
||||||
|
"on",
|
||||||
|
attributes={"device_class": "unknown_class"},
|
||||||
|
)
|
||||||
|
hass.states.async_set(
|
||||||
|
"some_domain.with_device_class_1",
|
||||||
|
"off",
|
||||||
|
attributes={"device_class": "some_device_class"},
|
||||||
|
)
|
||||||
|
hass.states.async_set(
|
||||||
|
"some_domain.with_device_class_2",
|
||||||
|
"foo",
|
||||||
|
attributes={"device_class": "some_device_class"},
|
||||||
|
)
|
||||||
|
hass.states.async_set("domain.is_unavailable", "unavailable", attributes={})
|
||||||
|
hass.states.async_set("domain.is_unknown", "unknown", attributes={})
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(domain="light")
|
||||||
|
entity_registry.async_get_or_create(
|
||||||
|
"light",
|
||||||
|
"hue",
|
||||||
|
"5678",
|
||||||
|
config_entry=config_entry,
|
||||||
|
translation_key="translation_key",
|
||||||
|
)
|
||||||
|
hass.states.async_set("light.hue_5678", "on", attributes={})
|
||||||
|
|
||||||
|
tpl = template.Template(
|
||||||
|
'{{ state_translated("switch.without_translations") }}', hass
|
||||||
|
)
|
||||||
|
assert tpl.async_render() == "on"
|
||||||
|
|
||||||
|
tp2 = template.Template(
|
||||||
|
'{{ state_translated("binary_sensor.without_device_class") }}', hass
|
||||||
|
)
|
||||||
|
assert tp2.async_render() == "On"
|
||||||
|
|
||||||
|
tpl3 = template.Template(
|
||||||
|
'{{ state_translated("binary_sensor.with_device_class") }}', hass
|
||||||
|
)
|
||||||
|
assert tpl3.async_render() == "Detected"
|
||||||
|
|
||||||
|
tpl4 = template.Template(
|
||||||
|
'{{ state_translated("binary_sensor.with_unknown_device_class") }}', hass
|
||||||
|
)
|
||||||
|
assert tpl4.async_render() == "On"
|
||||||
|
|
||||||
|
with pytest.raises(TemplateError):
|
||||||
|
template.Template(
|
||||||
|
'{{ state_translated("contextfunction") }}', hass
|
||||||
|
).async_render()
|
||||||
|
|
||||||
|
tpl6 = template.Template('{{ state_translated("switch.invalid") }}', hass)
|
||||||
|
assert tpl6.async_render() == "unknown"
|
||||||
|
|
||||||
|
with pytest.raises(TemplateError):
|
||||||
|
template.Template('{{ state_translated("-invalid") }}', hass).async_render()
|
||||||
|
|
||||||
|
def mock_get_cached_translations(
|
||||||
|
_hass: HomeAssistant,
|
||||||
|
_language: str,
|
||||||
|
category: str,
|
||||||
|
_integrations: Iterable[str] | None = None,
|
||||||
|
):
|
||||||
|
if category == "entity":
|
||||||
|
return {
|
||||||
|
"component.hue.entity.light.translation_key.state.on": "state_is_on"
|
||||||
|
}
|
||||||
|
if category == "state":
|
||||||
|
return {
|
||||||
|
"component.some_domain.state.some_device_class.off": "state_is_off",
|
||||||
|
"component.some_domain.state._.foo": "state_is_foo",
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.translation.async_get_cached_translations",
|
||||||
|
side_effect=mock_get_cached_translations,
|
||||||
|
):
|
||||||
|
tpl8 = template.Template('{{ state_translated("light.hue_5678") }}', hass)
|
||||||
|
assert tpl8.async_render() == "state_is_on"
|
||||||
|
|
||||||
|
tpl9 = template.Template(
|
||||||
|
'{{ state_translated("some_domain.with_device_class_1") }}', hass
|
||||||
|
)
|
||||||
|
assert tpl9.async_render() == "state_is_off"
|
||||||
|
|
||||||
|
tpl10 = template.Template(
|
||||||
|
'{{ state_translated("some_domain.with_device_class_2") }}', hass
|
||||||
|
)
|
||||||
|
assert tpl10.async_render() == "state_is_foo"
|
||||||
|
|
||||||
|
tpl11 = template.Template('{{ state_translated("domain.is_unavailable") }}', hass)
|
||||||
|
assert tpl11.async_render() == "unavailable"
|
||||||
|
|
||||||
|
tpl12 = template.Template('{{ state_translated("domain.is_unknown") }}', hass)
|
||||||
|
assert tpl12.async_render() == "unknown"
|
||||||
|
|
||||||
|
|
||||||
def test_has_value(hass: HomeAssistant) -> None:
|
def test_has_value(hass: HomeAssistant) -> None:
|
||||||
"""Test has_value method."""
|
"""Test has_value method."""
|
||||||
hass.states.async_set("test.value1", 1)
|
hass.states.async_set("test.value1", 1)
|
||||||
|
@ -2,10 +2,11 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from os import path
|
from os import path
|
||||||
import pathlib
|
import pathlib
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, call, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.const import EVENT_COMPONENT_LOADED, EVENT_CORE_CONFIG_UPDATE
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.generated import config_flows
|
from homeassistant.generated import config_flows
|
||||||
from homeassistant.helpers import translation
|
from homeassistant.helpers import translation
|
||||||
@ -510,3 +511,217 @@ async def test_custom_component_translations(
|
|||||||
hass.config.components.add("test_embedded")
|
hass.config.components.add("test_embedded")
|
||||||
hass.config.components.add("test_package")
|
hass.config.components.add("test_package")
|
||||||
assert await translation.async_get_translations(hass, "en", "state") == {}
|
assert await translation.async_get_translations(hass, "en", "state") == {}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_load_state_translations_to_cache(
|
||||||
|
hass: HomeAssistant, mock_config_flows, enable_custom_integrations: None
|
||||||
|
):
|
||||||
|
"""Test the load state translations to cache helper."""
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.translation._async_load_translations",
|
||||||
|
) as mock:
|
||||||
|
await translation._async_load_state_translations_to_cache(hass, "en", None)
|
||||||
|
mock.assert_has_calls(
|
||||||
|
[
|
||||||
|
call(hass, "en", "entity", None),
|
||||||
|
call(hass, "en", "state", None),
|
||||||
|
call(hass, "en", "entity_component", None),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.translation._async_load_translations",
|
||||||
|
) as mock:
|
||||||
|
await translation._async_load_state_translations_to_cache(
|
||||||
|
hass, "en", "some_integration"
|
||||||
|
)
|
||||||
|
mock.assert_has_calls(
|
||||||
|
[
|
||||||
|
call(hass, "en", "entity", "some_integration"),
|
||||||
|
call(hass, "en", "state", "some_integration"),
|
||||||
|
call(hass, "en", "entity_component", "some_integration"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_cached_translations(
|
||||||
|
hass: HomeAssistant, mock_config_flows, enable_custom_integrations: None
|
||||||
|
):
|
||||||
|
"""Test the get cached translations helper."""
|
||||||
|
translations = translation.async_get_cached_translations(hass, "en", "state")
|
||||||
|
assert translations == {}
|
||||||
|
|
||||||
|
assert await async_setup_component(hass, "switch", {"switch": {"platform": "test"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await translation._async_load_state_translations_to_cache(hass, "en", None)
|
||||||
|
translations = translation.async_get_cached_translations(hass, "en", "state")
|
||||||
|
|
||||||
|
assert translations["component.switch.state.string1"] == "Value 1"
|
||||||
|
assert translations["component.switch.state.string2"] == "Value 2"
|
||||||
|
|
||||||
|
await translation._async_load_state_translations_to_cache(hass, "de", None)
|
||||||
|
translations = translation.async_get_cached_translations(hass, "de", "state")
|
||||||
|
assert "component.switch.something" not in translations
|
||||||
|
assert translations["component.switch.state.string1"] == "German Value 1"
|
||||||
|
assert translations["component.switch.state.string2"] == "German Value 2"
|
||||||
|
|
||||||
|
# Test a partial translation
|
||||||
|
await translation._async_load_state_translations_to_cache(hass, "es", None)
|
||||||
|
translations = translation.async_get_cached_translations(hass, "es", "state")
|
||||||
|
assert translations["component.switch.state.string1"] == "Spanish Value 1"
|
||||||
|
assert translations["component.switch.state.string2"] == "Value 2"
|
||||||
|
|
||||||
|
# Test that an untranslated language falls back to English.
|
||||||
|
await translation._async_load_state_translations_to_cache(
|
||||||
|
hass, "invalid-language", None
|
||||||
|
)
|
||||||
|
translations = translation.async_get_cached_translations(
|
||||||
|
hass, "invalid-language", "state"
|
||||||
|
)
|
||||||
|
assert translations["component.switch.state.string1"] == "Value 1"
|
||||||
|
assert translations["component.switch.state.string2"] == "Value 2"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup(hass: HomeAssistant):
|
||||||
|
"""Test the setup load listeners helper."""
|
||||||
|
translation.async_setup(hass)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.translation._async_load_state_translations_to_cache",
|
||||||
|
) as mock:
|
||||||
|
hass.bus.async_fire(EVENT_COMPONENT_LOADED, {"component": "loaded_component"})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
mock.assert_called_once_with(hass, hass.config.language, "loaded_component")
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.translation._async_load_state_translations_to_cache",
|
||||||
|
) as mock:
|
||||||
|
hass.bus.async_fire(EVENT_COMPONENT_LOADED, {"component": "config.component"})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
mock.assert_not_called()
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.translation._async_load_state_translations_to_cache",
|
||||||
|
) as mock:
|
||||||
|
hass.bus.async_fire(EVENT_CORE_CONFIG_UPDATE, {"language": "en"})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
mock.assert_called_once_with(hass, hass.config.language, None)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.translation._async_load_state_translations_to_cache",
|
||||||
|
) as mock:
|
||||||
|
hass.bus.async_fire(EVENT_CORE_CONFIG_UPDATE, {})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_translate_state(hass: HomeAssistant):
|
||||||
|
"""Test the state translation helper."""
|
||||||
|
result = translation.async_translate_state(
|
||||||
|
hass, "unavailable", "binary_sensor", "platform", "translation_key", None
|
||||||
|
)
|
||||||
|
assert result == "unavailable"
|
||||||
|
|
||||||
|
result = translation.async_translate_state(
|
||||||
|
hass, "unknown", "binary_sensor", "platform", "translation_key", None
|
||||||
|
)
|
||||||
|
assert result == "unknown"
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.translation.async_get_cached_translations",
|
||||||
|
return_value={
|
||||||
|
"component.platform.entity.binary_sensor.translation_key.state.on": "TRANSLATED"
|
||||||
|
},
|
||||||
|
) as mock:
|
||||||
|
result = translation.async_translate_state(
|
||||||
|
hass, "on", "binary_sensor", "platform", "translation_key", None
|
||||||
|
)
|
||||||
|
mock.assert_called_once_with(hass, hass.config.language, "entity")
|
||||||
|
assert result == "TRANSLATED"
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.translation.async_get_cached_translations",
|
||||||
|
return_value={
|
||||||
|
"component.binary_sensor.entity_component.device_class.state.on": "TRANSLATED"
|
||||||
|
},
|
||||||
|
) as mock:
|
||||||
|
result = translation.async_translate_state(
|
||||||
|
hass, "on", "binary_sensor", "platform", None, "device_class"
|
||||||
|
)
|
||||||
|
mock.assert_called_once_with(hass, hass.config.language, "entity_component")
|
||||||
|
assert result == "TRANSLATED"
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.translation.async_get_cached_translations",
|
||||||
|
return_value={
|
||||||
|
"component.binary_sensor.entity_component._.state.on": "TRANSLATED"
|
||||||
|
},
|
||||||
|
) as mock:
|
||||||
|
result = translation.async_translate_state(
|
||||||
|
hass, "on", "binary_sensor", "platform", None, None
|
||||||
|
)
|
||||||
|
mock.assert_called_once_with(hass, hass.config.language, "entity_component")
|
||||||
|
assert result == "TRANSLATED"
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.translation.async_get_cached_translations",
|
||||||
|
return_value={"component.binary_sensor.state.device_class.on": "TRANSLATED"},
|
||||||
|
) as mock:
|
||||||
|
result = translation.async_translate_state(
|
||||||
|
hass, "on", "binary_sensor", "platform", None, "device_class"
|
||||||
|
)
|
||||||
|
mock.assert_has_calls(
|
||||||
|
[
|
||||||
|
call(hass, hass.config.language, "entity_component"),
|
||||||
|
call(hass, hass.config.language, "state", "binary_sensor"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert result == "TRANSLATED"
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.translation.async_get_cached_translations",
|
||||||
|
return_value={"component.binary_sensor.state._.on": "TRANSLATED"},
|
||||||
|
) as mock:
|
||||||
|
result = translation.async_translate_state(
|
||||||
|
hass, "on", "binary_sensor", "platform", None, None
|
||||||
|
)
|
||||||
|
mock.assert_has_calls(
|
||||||
|
[
|
||||||
|
call(hass, hass.config.language, "entity_component"),
|
||||||
|
call(hass, hass.config.language, "state", "binary_sensor"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert result == "TRANSLATED"
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.translation.async_get_cached_translations",
|
||||||
|
return_value={},
|
||||||
|
) as mock:
|
||||||
|
result = translation.async_translate_state(
|
||||||
|
hass, "on", "binary_sensor", "platform", None, None
|
||||||
|
)
|
||||||
|
mock.assert_has_calls(
|
||||||
|
[
|
||||||
|
call(hass, hass.config.language, "entity_component"),
|
||||||
|
call(hass, hass.config.language, "state", "binary_sensor"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert result == "on"
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.translation.async_get_cached_translations",
|
||||||
|
return_value={},
|
||||||
|
) as mock:
|
||||||
|
result = translation.async_translate_state(
|
||||||
|
hass, "on", "binary_sensor", "platform", "translation_key", "device_class"
|
||||||
|
)
|
||||||
|
mock.assert_has_calls(
|
||||||
|
[
|
||||||
|
call(hass, hass.config.language, "entity"),
|
||||||
|
call(hass, hass.config.language, "entity_component"),
|
||||||
|
call(hass, hass.config.language, "state", "binary_sensor"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert result == "on"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user