diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 2dbefe19965..5a92fa847ba 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -301,9 +301,9 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( ), AccuWeatherSensorDescription( key="PressureTendency", - device_class="accuweather__pressure_tendency", icon="mdi:gauge", name="Pressure tendency", + translation_key="pressure_tendency", value_fn=lambda data, _: cast(str, data["LocalizedText"]).lower(), ), AccuWeatherSensorDescription( diff --git a/homeassistant/components/accuweather/strings.json b/homeassistant/components/accuweather/strings.json index ba1bba21d9e..d37b5a10776 100644 --- a/homeassistant/components/accuweather/strings.json +++ b/homeassistant/components/accuweather/strings.json @@ -22,6 +22,17 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, + "entity": { + "sensor": { + "pressure_tendency": { + "state": { + "steady": "Steady", + "rising": "Rising", + "falling": "Falling" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/accuweather/strings.sensor.json b/homeassistant/components/accuweather/strings.sensor.json deleted file mode 100644 index 57cb89bcecf..00000000000 --- a/homeassistant/components/accuweather/strings.sensor.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "state": { - "accuweather__pressure_tendency": { - "steady": "Steady", - "rising": "Rising", - "falling": "Falling" - } - } -} diff --git a/homeassistant/components/accuweather/translations/en.json b/homeassistant/components/accuweather/translations/en.json index 7a8599fb562..8bbbd770388 100644 --- a/homeassistant/components/accuweather/translations/en.json +++ b/homeassistant/components/accuweather/translations/en.json @@ -22,6 +22,17 @@ } } }, + "entity": { + "sensor": { + "pressure_tendency": { + "state": { + "falling": "Falling", + "rising": "Rising", + "steady": "Steady" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index b4bd7403c43..5602a48c0a1 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -247,16 +247,17 @@ def _entry_dict(entry: er.RegistryEntry) -> dict[str, Any]: "config_entry_id": entry.config_entry_id, "device_id": entry.device_id, "disabled_by": entry.disabled_by, - "has_entity_name": entry.has_entity_name, "entity_category": entry.entity_category, "entity_id": entry.entity_id, + "has_entity_name": entry.has_entity_name, "hidden_by": entry.hidden_by, "icon": entry.icon, "id": entry.id, - "unique_id": entry.unique_id, "name": entry.name, "original_name": entry.original_name, "platform": entry.platform, + "translation_key": entry.translation_key, + "unique_id": entry.unique_id, } diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 255b0c2d834..f45022b82bb 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -223,6 +223,7 @@ class EntityDescription: icon: str | None = None has_entity_name: bool = False name: str | None = None + translation_key: str | None = None unit_of_measurement: str | None = None @@ -290,6 +291,7 @@ class Entity(ABC): _attr_should_poll: bool = True _attr_state: StateType = STATE_UNKNOWN _attr_supported_features: int | None = None + _attr_translation_key: str | None _attr_unique_id: str | None = None _attr_unit_of_measurement: str | None @@ -486,6 +488,15 @@ class Entity(ABC): return self.entity_description.entity_category return None + @property + def translation_key(self) -> str | None: + """Return the translation key to translate the entity's states.""" + if hasattr(self, "_attr_translation_key"): + return self._attr_translation_key + if hasattr(self, "entity_description"): + return self.entity_description.translation_key + return None + # DO NOT OVERWRITE # These properties and methods are either managed by Home Assistant or they # are used to perform a very specific function. Overwriting these may diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 9b8e1985930..d43de7dc641 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -616,6 +616,7 @@ class EntityPlatform: original_name=entity.name, suggested_object_id=suggested_object_id, supported_features=entity.supported_features, + translation_key=entity.translation_key, unit_of_measurement=entity.unit_of_measurement, ) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 77a0b5a0400..ce0d260beb3 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -61,7 +61,7 @@ SAVE_DELAY = 10 _LOGGER = logging.getLogger(__name__) STORAGE_VERSION_MAJOR = 1 -STORAGE_VERSION_MINOR = 8 +STORAGE_VERSION_MINOR = 9 STORAGE_KEY = "core.entity_registry" # Attributes relevant to describing entity @@ -125,6 +125,7 @@ class RegistryEntry: original_icon: str | None = attr.ib(default=None) original_name: str | None = attr.ib(default=None) supported_features: int = attr.ib(default=0) + translation_key: str | None = attr.ib(default=None) unit_of_measurement: str | None = attr.ib(default=None) @domain.default @@ -234,6 +235,11 @@ class EntityRegistryStore(storage.Store): continue entity["device_class"] = None + if old_major_version == 1 and old_minor_version < 9: + # Version 1.9 adds translation_key + for entity in data["entities"]: + entity["translation_key"] = entity.get("translation_key") + if old_major_version > 1: raise NotImplementedError return data @@ -412,6 +418,7 @@ class EntityRegistry: original_icon: str | None | UndefinedType = UNDEFINED, original_name: str | None | UndefinedType = UNDEFINED, supported_features: int | None | UndefinedType = UNDEFINED, + translation_key: str | None | UndefinedType = UNDEFINED, unit_of_measurement: str | None | UndefinedType = UNDEFINED, ) -> RegistryEntry: """Get entity. Create if it doesn't exist.""" @@ -437,6 +444,7 @@ class EntityRegistry: original_icon=original_icon, original_name=original_name, supported_features=supported_features, + translation_key=translation_key, unit_of_measurement=unit_of_measurement, ) @@ -487,6 +495,7 @@ class EntityRegistry: original_name=none_if_undefined(original_name), platform=platform, supported_features=none_if_undefined(supported_features) or 0, + translation_key=none_if_undefined(translation_key), unique_id=unique_id, unit_of_measurement=none_if_undefined(unit_of_measurement), ) @@ -592,13 +601,14 @@ class EntityRegistry: name: str | None | UndefinedType = UNDEFINED, new_entity_id: str | UndefinedType = UNDEFINED, new_unique_id: str | UndefinedType = UNDEFINED, + options: EntityOptionsType | UndefinedType = UNDEFINED, original_device_class: str | None | UndefinedType = UNDEFINED, original_icon: str | None | UndefinedType = UNDEFINED, original_name: str | None | UndefinedType = UNDEFINED, - supported_features: int | UndefinedType = UNDEFINED, - unit_of_measurement: str | None | UndefinedType = UNDEFINED, platform: str | None | UndefinedType = UNDEFINED, - options: EntityOptionsType | UndefinedType = UNDEFINED, + supported_features: int | UndefinedType = UNDEFINED, + translation_key: str | None | UndefinedType = UNDEFINED, + unit_of_measurement: str | None | UndefinedType = UNDEFINED, ) -> RegistryEntry: """Private facing update properties method.""" old = self.entities[entity_id] @@ -640,13 +650,14 @@ class EntityRegistry: ("icon", icon), ("has_entity_name", has_entity_name), ("name", name), + ("options", options), ("original_device_class", original_device_class), ("original_icon", original_icon), ("original_name", original_name), - ("supported_features", supported_features), - ("unit_of_measurement", unit_of_measurement), ("platform", platform), - ("options", options), + ("supported_features", supported_features), + ("translation_key", translation_key), + ("unit_of_measurement", unit_of_measurement), ): if value is not UNDEFINED and value != getattr(old, attr_name): new_values[attr_name] = value @@ -720,6 +731,7 @@ class EntityRegistry: original_icon: str | None | UndefinedType = UNDEFINED, original_name: str | None | UndefinedType = UNDEFINED, supported_features: int | UndefinedType = UNDEFINED, + translation_key: str | None | UndefinedType = UNDEFINED, unit_of_measurement: str | None | UndefinedType = UNDEFINED, ) -> RegistryEntry: """Update properties of an entity.""" @@ -742,6 +754,7 @@ class EntityRegistry: original_icon=original_icon, original_name=original_name, supported_features=supported_features, + translation_key=translation_key, unit_of_measurement=unit_of_measurement, ) @@ -831,6 +844,7 @@ class EntityRegistry: original_name=entity["original_name"], platform=entity["platform"], supported_features=entity["supported_features"], + translation_key=entity["translation_key"], unique_id=entity["unique_id"], unit_of_measurement=entity["unit_of_measurement"], ) @@ -868,6 +882,7 @@ class EntityRegistry: "original_name": entry.original_name, "platform": entry.platform, "supported_features": entry.supported_features, + "translation_key": entry.translation_key, "unique_id": entry.unique_id, "unit_of_measurement": entry.unit_of_measurement, } diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index 824ebc6b825..31c49042c1d 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -255,6 +255,13 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: ), ) }, + vol.Optional("entity"): { + str: { + str: vol.Schema( + {vol.Optional("state"): {str: cv.string_with_no_html}} + ) + } + }, } ) diff --git a/tests/common.py b/tests/common.py index 46022022df6..638bd5551eb 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1113,6 +1113,11 @@ class MockEntity(entity.Entity): """Info about supported features.""" return self._handle("supported_features") + @property + def translation_key(self): + """Return the translation key.""" + return self._handle("translation_key") + @property def unique_id(self): """Return the unique ID of the entity.""" diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index 55052b5ca3e..cb5e8428a3c 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -73,12 +73,13 @@ async def test_sensor_without_forecast(hass): assert state.state == "falling" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_ICON) == "mdi:gauge" - assert state.attributes.get(ATTR_DEVICE_CLASS) == "accuweather__pressure_tendency" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_pressure_tendency") assert entry assert entry.unique_id == "0123456-pressuretendency" + assert entry.translation_key == "pressure_tendency" state = hass.states.get("sensor.home_realfeel_temperature") assert state diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index ee46838b01c..761636f186e 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -70,10 +70,11 @@ async def test_list_entities(hass, client): "hidden_by": None, "icon": None, "id": ANY, - "unique_id": ANY, "name": "Hello World", "original_name": None, "platform": "test_platform", + "translation_key": None, + "unique_id": ANY, }, { "area_id": None, @@ -86,10 +87,11 @@ async def test_list_entities(hass, client): "hidden_by": None, "icon": None, "id": ANY, - "unique_id": ANY, "name": None, "original_name": None, "platform": "test_platform", + "translation_key": None, + "unique_id": ANY, }, ] @@ -124,10 +126,11 @@ async def test_list_entities(hass, client): "hidden_by": None, "icon": None, "id": ANY, - "unique_id": ANY, "name": "Hello World", "original_name": None, "platform": "test_platform", + "translation_key": None, + "unique_id": ANY, }, ] @@ -165,16 +168,17 @@ async def test_get_entity(hass, client): "disabled_by": None, "entity_category": None, "entity_id": "test_domain.name", + "has_entity_name": False, "hidden_by": None, "icon": None, "id": ANY, - "has_entity_name": False, "name": "Hello World", "options": {}, "original_device_class": None, "original_icon": None, "original_name": None, "platform": "test_platform", + "translation_key": None, "unique_id": "1234", } @@ -196,16 +200,17 @@ async def test_get_entity(hass, client): "disabled_by": None, "entity_category": None, "entity_id": "test_domain.no_name", + "has_entity_name": False, "hidden_by": None, "icon": None, "id": ANY, - "has_entity_name": False, "name": None, "options": {}, "original_device_class": None, "original_icon": None, "original_name": None, "platform": "test_platform", + "translation_key": None, "unique_id": "6789", } @@ -260,16 +265,17 @@ async def test_update_entity(hass, client): "disabled_by": None, "entity_category": None, "entity_id": "test_domain.world", + "has_entity_name": False, "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", "id": ANY, - "has_entity_name": False, "name": "after update", "options": {}, "original_device_class": None, "original_icon": None, "original_name": None, "platform": "test_platform", + "translation_key": None, "unique_id": "1234", } } @@ -333,16 +339,17 @@ async def test_update_entity(hass, client): "disabled_by": None, "entity_category": None, "entity_id": "test_domain.world", + "has_entity_name": False, "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", "id": ANY, - "has_entity_name": False, "name": "after update", "options": {}, "original_device_class": None, "original_icon": None, "original_name": None, "platform": "test_platform", + "translation_key": None, "unique_id": "1234", }, "require_restart": True, @@ -371,16 +378,17 @@ async def test_update_entity(hass, client): "disabled_by": None, "entity_category": None, "entity_id": "test_domain.world", + "has_entity_name": False, "hidden_by": "user", # We exchange strings over the WS API, not enums "icon": "icon:after update", "id": ANY, - "has_entity_name": False, "name": "after update", "options": {"sensor": {"unit_of_measurement": "beard_second"}}, "original_device_class": None, "original_icon": None, "original_name": None, "platform": "test_platform", + "translation_key": None, "unique_id": "1234", }, } @@ -421,16 +429,17 @@ async def test_update_entity_require_restart(hass, client): "disabled_by": None, "entity_category": None, "entity_id": entity_id, + "has_entity_name": False, + "hidden_by": None, "icon": None, "id": ANY, - "hidden_by": None, - "has_entity_name": False, "name": None, "options": {}, "original_device_class": None, "original_icon": None, "original_name": None, "platform": "test_platform", + "translation_key": None, "unique_id": "1234", }, "require_restart": True, @@ -527,16 +536,17 @@ async def test_update_entity_no_changes(hass, client): "disabled_by": None, "entity_category": None, "entity_id": "test_domain.world", + "has_entity_name": False, "hidden_by": None, "icon": None, "id": ANY, - "has_entity_name": False, "name": "name of entity", "options": {}, "original_device_class": None, "original_icon": None, "original_name": None, "platform": "test_platform", + "translation_key": None, "unique_id": "1234", } } @@ -614,16 +624,17 @@ async def test_update_entity_id(hass, client): "disabled_by": None, "entity_category": None, "entity_id": "test_domain.planet", + "has_entity_name": False, "hidden_by": None, "icon": None, "id": ANY, - "has_entity_name": False, "name": None, "options": {}, "original_device_class": None, "original_icon": None, "original_name": None, "platform": "test_platform", + "translation_key": None, "unique_id": "1234", } } diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 698d3cfe98a..d359efd6325 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -934,3 +934,23 @@ async def test_friendly_name( assert len(hass.states.async_entity_ids()) == 1 state = hass.states.async_all()[0] assert state.attributes.get(ATTR_FRIENDLY_NAME) == expected_friendly_name + + +async def test_translation_key(hass): + """Test translation key property.""" + mock_entity1 = entity.Entity() + mock_entity1.hass = hass + mock_entity1.entity_description = entity.EntityDescription( + key="abc", translation_key="from_entity_description" + ) + mock_entity1.entity_id = "hello.world" + mock_entity1._attr_translation_key = "from_attr" + assert mock_entity1.translation_key == "from_attr" + + mock_entity2 = entity.Entity() + mock_entity2.hass = hass + mock_entity2.entity_description = entity.EntityDescription( + key="abc", translation_key="from_entity_description" + ) + mock_entity2.entity_id = "hello.world" + assert mock_entity2.translation_key == "from_entity_description" diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index b2af85ca631..c7721f7f78c 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1227,6 +1227,7 @@ async def test_entity_info_added_to_entity_registry(hass): icon="nice:icon", name="best name", supported_features=5, + translation_key="my_translation_key", unique_id="default", unit_of_measurement=PERCENTAGE, ) @@ -1251,6 +1252,7 @@ async def test_entity_info_added_to_entity_registry(hass): original_icon="nice:icon", original_name="best name", supported_features=5, + translation_key="my_translation_key", unit_of_measurement=PERCENTAGE, ) diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 5538950260c..6db1b59b2c0 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -84,6 +84,7 @@ def test_get_or_create_updates_data(registry): original_icon="initial-original_icon", original_name="initial-original_name", supported_features=5, + translation_key="initial-translation_key", unit_of_measurement="initial-unit_of_measurement", ) @@ -106,6 +107,7 @@ def test_get_or_create_updates_data(registry): original_icon="initial-original_icon", original_name="initial-original_name", supported_features=5, + translation_key="initial-translation_key", unit_of_measurement="initial-unit_of_measurement", ) @@ -126,6 +128,7 @@ def test_get_or_create_updates_data(registry): original_icon="updated-original_icon", original_name="updated-original_name", supported_features=10, + translation_key="updated-translation_key", unit_of_measurement="updated-unit_of_measurement", ) @@ -149,6 +152,7 @@ def test_get_or_create_updates_data(registry): original_icon="updated-original_icon", original_name="updated-original_name", supported_features=10, + translation_key="updated-translation_key", unit_of_measurement="updated-unit_of_measurement", ) @@ -167,6 +171,7 @@ def test_get_or_create_updates_data(registry): original_icon=None, original_name=None, supported_features=None, + translation_key=None, unit_of_measurement=None, ) @@ -190,6 +195,7 @@ def test_get_or_create_updates_data(registry): original_icon=None, original_name=None, supported_features=0, # supported_features is stored as an int + translation_key=None, unit_of_measurement=None, ) @@ -242,6 +248,7 @@ async def test_loading_saving_data(hass, registry): original_icon="hass:original-icon", original_name="Original Name", supported_features=5, + translation_key="initial-translation_key", unit_of_measurement="initial-unit_of_measurement", ) registry.async_update_entity( @@ -287,6 +294,7 @@ async def test_loading_saving_data(hass, registry): assert new_entry2.original_icon == "hass:original-icon" assert new_entry2.original_name == "Original Name" assert new_entry2.supported_features == 5 + assert new_entry2.translation_key == "initial-translation_key" assert new_entry2.unit_of_measurement == "initial-unit_of_measurement"