Compare commits

..

1 Commits

Author SHA1 Message Date
Paul Bottein
df06a5878c Add windows 98 labs feature 2026-03-10 09:41:49 +01:00
7 changed files with 14 additions and 285 deletions

View File

@@ -19,7 +19,7 @@
],
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"preview_features": { "windows_98": {}, "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260304.0"]
}

View File

@@ -1,5 +1,11 @@
{
"preview_features": {
"windows_98": {
"description": "Transforms your dashboard with a nostalgic Windows 98 look.",
"disable_confirmation": "Your dashboard will return to its normal look. You can re-enable this at any time in Labs settings.",
"enable_confirmation": "Your dashboard will be transformed with a Windows 98 theme. You can turn this off at any time in Labs settings.",
"name": "Windows 98"
},
"winter_mode": {
"description": "Adds falling snowflakes on your screen. Get your home ready for winter! ❄️\n\nIf you have animations disabled in your device accessibility settings, this feature will not work.",
"disable_confirmation": "Snowflakes will no longer fall on your screen. You can re-enable this at any time in Labs settings.",

View File

@@ -19,6 +19,11 @@ LABS_PREVIEW_FEATURES = {
},
},
"frontend": {
"windows_98": {
"feedback_url": "",
"learn_more_url": "",
"report_issue_url": "",
},
"winter_mode": {
"feedback_url": "",
"learn_more_url": "",

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
from ast import literal_eval
import asyncio
from enum import Enum
import collections.abc
from collections.abc import Callable, Generator, Iterable
from copy import deepcopy
@@ -58,10 +57,7 @@ from homeassistant.core import (
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import entity_registry as er, location as loc_helper
from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.translation import (
async_translate_state,
async_translate_state_attr,
)
from homeassistant.helpers.translation import async_translate_state
from homeassistant.helpers.typing import TemplateVarsType
from homeassistant.util import convert, location as location_util
from homeassistant.util.async_ import run_callback_threadsafe
@@ -811,48 +807,6 @@ class StateTranslated:
return "<template StateTranslated>"
class StateAttrTranslated:
"""Class to represent a translated state attribute value in a template."""
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize."""
self._hass = hass
def __call__(self, entity_id: str, attribute: str) -> Any:
"""Retrieve translated state attribute value if available."""
state = _get_state_if_valid(self._hass, entity_id)
if state is None:
return None
attr_value = state.attributes.get(attribute)
if attr_value is None:
return None
if not isinstance(attr_value, str | Enum):
return attr_value
domain = state.domain
device_class = state.attributes.get("device_class")
entry = er.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_attr(
self._hass,
str(attr_value),
domain,
platform,
translation_key,
device_class,
attribute,
)
def __repr__(self) -> str:
"""Representation of Translated state attribute."""
return "<template StateAttrTranslated>"
class DomainStates:
"""Class to expose a specific HA domain as attributes."""
@@ -2035,7 +1989,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
"is_state_attr",
"is_state",
"state_attr",
"state_attr_translated",
"state_translated",
"states",
]
@@ -2083,11 +2036,9 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.globals["is_state_attr"] = hassfunction(is_state_attr)
self.globals["is_state"] = hassfunction(is_state)
self.globals["state_attr"] = hassfunction(state_attr)
self.globals["state_attr_translated"] = StateAttrTranslated(hass)
self.globals["state_translated"] = StateTranslated(hass)
self.globals["states"] = AllStates(hass)
self.filters["state_attr"] = self.globals["state_attr"]
self.filters["state_attr_translated"] = self.globals["state_attr_translated"]
self.filters["state_translated"] = self.globals["state_translated"]
self.filters["states"] = self.globals["states"]
self.tests["is_state_attr"] = hassfunction(is_state_attr, pass_eval_context)
@@ -2096,7 +2047,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
def is_safe_callable(self, obj):
"""Test if callback is safe."""
return isinstance(
obj, (AllStates, StateAttrTranslated, StateTranslated)
obj, (AllStates, StateTranslated)
) or super().is_safe_callable(obj)
def is_safe_attribute(self, obj, attr, value):

View File

@@ -492,43 +492,3 @@ def async_translate_state(
return translations[localize_key]
return state
@callback
def async_translate_state_attr(
hass: HomeAssistant,
attr_value: str,
domain: str,
platform: str | None,
translation_key: str | None,
device_class: str | None,
attribute_name: str,
) -> str:
"""Translate provided state attribute value using cached translations for currently selected language."""
language = hass.config.language
if platform is not None and translation_key is not None:
localize_key = (
f"component.{platform}.entity.{domain}"
f".{translation_key}.state_attributes.{attribute_name}"
f".state.{attr_value}"
)
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}"
f".state_attributes.{attribute_name}.state.{attr_value}"
)
if localize_key in translations:
return translations[localize_key]
localize_key = (
f"component.{domain}.entity_component._"
f".state_attributes.{attribute_name}.state.{attr_value}"
)
if localize_key in translations:
return translations[localize_key]
return attr_value

View File

@@ -1043,113 +1043,6 @@ async def test_state_translated(
assert result == "unknown"
async def test_state_attr_translated(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test state_attr_translated method."""
await translation._async_get_translations_cache(hass).async_load("en", set())
hass.states.async_set(
"climate.living_room",
"heat",
attributes={"fan_mode": "auto", "hvac_action": "heating"},
)
hass.states.async_set(
"switch.test",
"on",
attributes={"some_attr": "some_value", "numeric_attr": 42, "bool_attr": True},
)
config_entry = MockConfigEntry(domain="climate")
config_entry.add_to_hass(hass)
entity_registry.async_get_or_create(
"climate",
"test_platform",
"5678",
config_entry=config_entry,
translation_key="my_climate",
)
hass.states.async_set(
"climate.test_platform_5678",
"heat",
attributes={"fan_mode": "auto"},
)
result = render(
hass,
'{{ state_attr_translated("switch.test", "some_attr") }}',
)
assert result == "some_value"
# Non-string attributes should be returned as-is without type conversion
result = render(
hass,
'{{ state_attr_translated("switch.test", "numeric_attr") }}',
)
assert result == 42
assert isinstance(result, int)
result = render(
hass,
'{{ state_attr_translated("switch.test", "bool_attr") }}',
)
assert result is True
result = render(
hass,
'{{ state_attr_translated("climate.non_existent", "fan_mode") }}',
)
assert result is None
with pytest.raises(TemplateError):
render(hass, '{{ state_attr_translated("-invalid", "fan_mode") }}')
result = render(
hass,
'{{ state_attr_translated("climate.living_room", "non_existent") }}',
)
assert result is None
def mock_get_cached_translations(
_hass: HomeAssistant,
_language: str,
category: str,
_integrations: Iterable[str] | None = None,
):
if category == "entity":
return {
"component.test_platform.entity.climate.my_climate.state_attributes.fan_mode.state.auto": "Platform Automatic",
}
if category == "entity_component":
return {
"component.climate.entity_component._.state_attributes.fan_mode.state.auto": "Automatic",
"component.climate.entity_component._.state_attributes.hvac_action.state.heating": "Heating",
}
return {}
with patch(
"homeassistant.helpers.translation.async_get_cached_translations",
side_effect=mock_get_cached_translations,
):
result = render(
hass,
'{{ state_attr_translated("climate.living_room", "fan_mode") }}',
)
assert result == "Automatic"
result = render(
hass,
'{{ state_attr_translated("climate.living_room", "hvac_action") }}',
)
assert result == "Heating"
result = render(
hass,
'{{ state_attr_translated("climate.test_platform_5678", "fan_mode") }}',
)
assert result == "Platform Automatic"
def test_has_value(hass: HomeAssistant) -> None:
"""Test has_value method."""
hass.states.async_set("test.value1", 1)

View File

@@ -683,92 +683,6 @@ async def test_translate_state(hass: HomeAssistant) -> None:
assert result == "on"
async def test_translate_state_attr(hass: HomeAssistant) -> None:
"""Test the state attribute translation helper."""
with patch(
"homeassistant.helpers.translation.async_get_cached_translations",
return_value={
"component.platform.entity.climate.translation_key.state_attributes.fan_mode.state.auto": "TRANSLATED"
},
) as mock:
result = translation.async_translate_state_attr(
hass,
"auto",
"climate",
"platform",
"translation_key",
None,
"fan_mode",
)
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.climate.entity_component.device_class.state_attributes.fan_mode.state.auto": "TRANSLATED"
},
) as mock:
result = translation.async_translate_state_attr(
hass,
"auto",
"climate",
"platform",
None,
"device_class",
"fan_mode",
)
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.climate.entity_component._.state_attributes.fan_mode.state.auto": "TRANSLATED"
},
) as mock:
result = translation.async_translate_state_attr(
hass, "auto", "climate", "platform", None, None, "fan_mode"
)
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={},
) as mock:
result = translation.async_translate_state_attr(
hass, "auto", "climate", "platform", None, None, "fan_mode"
)
mock.assert_has_calls(
[
call(hass, hass.config.language, "entity_component"),
]
)
assert result == "auto"
with patch(
"homeassistant.helpers.translation.async_get_cached_translations",
return_value={},
) as mock:
result = translation.async_translate_state_attr(
hass,
"auto",
"climate",
"platform",
"translation_key",
"device_class",
"fan_mode",
)
mock.assert_has_calls(
[
call(hass, hass.config.language, "entity"),
call(hass, hass.config.language, "entity_component"),
]
)
assert result == "auto"
async def test_get_translations_still_has_title_without_translations_files(
hass: HomeAssistant, mock_config_flows
) -> None: