diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 43196acf319..1040a500f3c 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -177,6 +177,7 @@ def _entry_dict(entry): "name": entry.name, "icon": entry.icon, "platform": entry.platform, + "entity_category": entry.entity_category, } diff --git a/homeassistant/const.py b/homeassistant/const.py index 483109737ed..f4387c9bbce 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -704,3 +704,6 @@ CLOUD_NEVER_EXPOSED_ENTITIES: Final[list[str]] = ["group.all_locks"] # The ID of the Home Assistant Cast App CAST_APP_ID_HOMEASSISTANT: Final = "B12CE3CA" + +ENTITY_CATEGORY_CONFIG: Final = "config" +ENTITY_CATEGORY_DIAGNOSTIC: Final = "diagnostic" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 1d3b331b8ab..584fa4f0554 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -11,7 +11,7 @@ import logging import math import sys from timeit import default_timer as timer -from typing import Any, TypedDict, final +from typing import Any, Literal, TypedDict, final from homeassistant.config import DATA_CUSTOMIZE from homeassistant.const import ( @@ -180,6 +180,7 @@ class EntityDescription: key: str device_class: str | None = None + entity_category: Literal["config", "diagnostic"] | None = None entity_registry_enabled_default: bool = True force_update: bool = False icon: str | None = None @@ -238,6 +239,7 @@ class Entity(ABC): _attr_context_recent_time: timedelta = timedelta(seconds=5) _attr_device_class: str | None _attr_device_info: DeviceInfo | None = None + _attr_entity_category: str | None _attr_entity_picture: str | None = None _attr_entity_registry_enabled_default: bool _attr_extra_state_attributes: MutableMapping[str, Any] @@ -404,6 +406,15 @@ class Entity(ABC): """Return the attribution.""" return self._attr_attribution + @property + def entity_category(self) -> str | None: + """Return the category of the entity, if any.""" + if hasattr(self, "_attr_entity_category"): + return self._attr_entity_category + if hasattr(self, "entity_description"): + return self.entity_description.entity_category + 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 831f6c9a4b6..5fb095c4f02 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -501,6 +501,7 @@ class EntityPlatform: unit_of_measurement=entity.unit_of_measurement, original_name=entity.name, original_icon=entity.icon, + entity_category=entity.entity_category, ) entity.registry_entry = entry diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 88233b30df7..bedbdc51785 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -106,6 +106,7 @@ class RegistryEntry: # As set by integration original_name: str | None = attr.ib(default=None) original_icon: str | None = attr.ib(default=None) + entity_category: str | None = attr.ib(default=None) domain: str = attr.ib(init=False, repr=False) @domain.default @@ -256,6 +257,7 @@ class EntityRegistry: unit_of_measurement: str | None = None, original_name: str | None = None, original_icon: str | None = None, + entity_category: str | None = None, ) -> RegistryEntry: """Get entity. Create if it doesn't exist.""" config_entry_id = None @@ -276,6 +278,7 @@ class EntityRegistry: unit_of_measurement=unit_of_measurement or UNDEFINED, original_name=original_name or UNDEFINED, original_icon=original_icon or UNDEFINED, + entity_category=entity_category or UNDEFINED, # When we changed our slugify algorithm, we invalidated some # stored entity IDs with either a __ or ending in _. # Fix introduced in 0.86 (Jan 23, 2019). Next line can be @@ -310,6 +313,7 @@ class EntityRegistry: unit_of_measurement=unit_of_measurement, original_name=original_name, original_icon=original_icon, + entity_category=entity_category, ) self._register_entry(entity) _LOGGER.info("Registered new %s.%s entity: %s", domain, platform, entity_id) @@ -418,6 +422,7 @@ class EntityRegistry: unit_of_measurement: str | None | UndefinedType = UNDEFINED, original_name: str | None | UndefinedType = UNDEFINED, original_icon: str | None | UndefinedType = UNDEFINED, + entity_category: str | None | UndefinedType = UNDEFINED, ) -> RegistryEntry: """Private facing update properties method.""" old = self.entities[entity_id] @@ -438,6 +443,7 @@ class EntityRegistry: ("unit_of_measurement", unit_of_measurement), ("original_name", original_name), ("original_icon", original_icon), + ("entity_category", entity_category), ): if value is not UNDEFINED and value != getattr(old, attr_name): new_values[attr_name] = value @@ -523,6 +529,7 @@ class EntityRegistry: unit_of_measurement=entity.get("unit_of_measurement"), original_name=entity.get("original_name"), original_icon=entity.get("original_icon"), + entity_category=entity.get("entity_category"), ) self.entities = entities @@ -555,6 +562,7 @@ class EntityRegistry: "unit_of_measurement": entry.unit_of_measurement, "original_name": entry.original_name, "original_icon": entry.original_icon, + "entity_category": entry.entity_category, } for entry in self.entities.values() ] diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 3d5861c2db3..3faff1222d4 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -1,6 +1,4 @@ """Test entity_registry API.""" -from collections import OrderedDict - import pytest from homeassistant.components.config import entity_registry @@ -31,18 +29,22 @@ def device_registry(hass): async def test_list_entities(hass, client): """Test list entries.""" - entities = OrderedDict() - entities["test_domain.name"] = RegistryEntry( - entity_id="test_domain.name", - unique_id="1234", - platform="test_platform", - name="Hello World", + mock_registry( + hass, + { + "test_domain.name": RegistryEntry( + entity_id="test_domain.name", + unique_id="1234", + platform="test_platform", + name="Hello World", + ), + "test_domain.no_name": RegistryEntry( + entity_id="test_domain.no_name", + unique_id="6789", + platform="test_platform", + ), + }, ) - entities["test_domain.no_name"] = RegistryEntry( - entity_id="test_domain.no_name", unique_id="6789", platform="test_platform" - ) - - mock_registry(hass, entities) await client.send_json({"id": 5, "type": "config/entity_registry/list"}) msg = await client.receive_json() @@ -57,6 +59,7 @@ async def test_list_entities(hass, client): "name": "Hello World", "icon": None, "platform": "test_platform", + "entity_category": None, }, { "config_entry_id": None, @@ -67,6 +70,7 @@ async def test_list_entities(hass, client): "name": None, "icon": None, "platform": "test_platform", + "entity_category": None, }, ] @@ -108,6 +112,7 @@ async def test_get_entity(hass, client): "original_icon": None, "capabilities": None, "unique_id": "1234", + "entity_category": None, } await client.send_json( @@ -132,6 +137,7 @@ async def test_get_entity(hass, client): "original_icon": None, "capabilities": None, "unique_id": "6789", + "entity_category": None, } @@ -187,6 +193,7 @@ async def test_update_entity(hass, client): "original_icon": None, "capabilities": None, "unique_id": "1234", + "entity_category": None, } } @@ -235,6 +242,7 @@ async def test_update_entity(hass, client): "original_icon": None, "capabilities": None, "unique_id": "1234", + "entity_category": None, }, "reload_delay": 30, } @@ -289,6 +297,7 @@ async def test_update_entity_require_restart(hass, client): "original_icon": None, "capabilities": None, "unique_id": "1234", + "entity_category": None, }, "require_restart": True, } @@ -390,6 +399,7 @@ async def test_update_entity_no_changes(hass, client): "original_icon": None, "capabilities": None, "unique_id": "1234", + "entity_category": None, } } @@ -470,6 +480,7 @@ async def test_update_entity_id(hass, client): "original_icon": None, "capabilities": None, "unique_id": "1234", + "entity_category": None, } } diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index bdb7a2782a3..e334e6d2c56 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -809,3 +809,23 @@ async def test_attribution_attribute(hass): state = hass.states.get(mock_entity.entity_id) assert state.attributes.get(ATTR_ATTRIBUTION) == "Home Assistant" + + +async def test_entity_category_property(hass): + """Test entity category property.""" + mock_entity1 = entity.Entity() + mock_entity1.hass = hass + mock_entity1.entity_description = entity.EntityDescription( + key="abc", entity_category="ignore_me" + ) + mock_entity1.entity_id = "hello.world" + mock_entity1._attr_entity_category = "config" + assert mock_entity1.entity_category == "config" + + mock_entity2 = entity.Entity() + mock_entity2.hass = hass + mock_entity2.entity_description = entity.EntityDescription( + key="abc", entity_category="config" + ) + mock_entity2.entity_id = "hello.world" + assert mock_entity2.entity_category == "config"