diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 94b7f0851e4..02486bc33a0 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -13,6 +13,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, + entity_component, entity_registry as er, ) from homeassistant.helpers.json import json_dumps @@ -345,7 +346,8 @@ def websocket_get_automatic_entity_ids( continue automatic_entity_ids[entity_id] = registry.async_generate_entity_id( entry.domain, - entry.suggested_object_id or f"{entry.platform}_{entry.unique_id}", + entity_component.async_get_entity_suggested_object_id(hass, entity_id) + or f"{entry.platform}_{entry.unique_id}", ) connection.send_message( diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 02508e9ee9e..823e346b038 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -29,20 +29,27 @@ from homeassistant.core import ( from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import async_get_integration, bind_hass from homeassistant.setup import async_prepare_setup_platform +from homeassistant.util.hass_dict import HassKey -from . import config_validation as cv, discovery, entity, service -from .entity_platform import EntityPlatform +from . import ( + config_validation as cv, + device_registry as dr, + discovery, + entity, + entity_registry as er, + service, +) +from .entity_platform import EntityPlatform, async_calculate_suggested_object_id from .typing import ConfigType, DiscoveryInfoType, VolDictType, VolSchemaType DEFAULT_SCAN_INTERVAL = timedelta(seconds=15) -DATA_INSTANCES = "entity_components" +DATA_INSTANCES: HassKey[dict[str, EntityComponent]] = HassKey("entity_components") @bind_hass async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None: """Trigger an update for an entity.""" domain = entity_id.partition(".")[0] - entity_comp: EntityComponent[entity.Entity] | None entity_comp = hass.data.get(DATA_INSTANCES, {}).get(domain) if entity_comp is None: @@ -60,6 +67,34 @@ async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None: await entity_obj.async_update_ha_state(True) +@callback +def async_get_entity_suggested_object_id( + hass: HomeAssistant, entity_id: str +) -> str | None: + """Get the suggested object id for an entity. + + Raises HomeAssistantError if the entity is not in the registry. + """ + entity_registry = er.async_get(hass) + if not (entity_entry := entity_registry.async_get(entity_id)): + raise HomeAssistantError(f"Entity {entity_id} is not in the registry.") + + domain = entity_id.partition(".")[0] + + if entity_entry.suggested_object_id: + return entity_entry.suggested_object_id + + entity_comp = hass.data.get(DATA_INSTANCES, {}).get(domain) + entity_obj = entity_comp.get_entity(entity_id) if entity_comp else None + if entity_obj: + device: dr.DeviceEntry | None = None + if device_id := entity_entry.device_id: + device = dr.async_get(hass).async_get(device_id) + return async_calculate_suggested_object_id(entity_obj, device) + + return entity_entry.suggested_object_id + + class EntityComponent[_EntityT: entity.Entity = entity.Entity]: """The EntityComponent manages platforms that manage entities. @@ -95,7 +130,7 @@ class EntityComponent[_EntityT: entity.Entity = entity.Entity]: self.async_add_entities = domain_platform.async_add_entities self.add_entities = domain_platform.add_entities self._entities: dict[str, entity.Entity] = domain_platform.domain_entities - hass.data.setdefault(DATA_INSTANCES, {})[domain] = self + hass.data.setdefault(DATA_INSTANCES, {})[domain] = self # type: ignore[assignment] @property def entities(self) -> Iterable[_EntityT]: diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index f543891d3f3..cd732b5dd87 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -764,7 +764,7 @@ class EntityPlatform: already_exists = True return (already_exists, restored) - async def _async_add_entity( # noqa: C901 + async def _async_add_entity( self, entity: Entity, update_before_add: bool, @@ -843,31 +843,18 @@ class EntityPlatform: else: device = None - if not registered_entity_id: - # Do not bother working out a suggested_object_id - # if the entity is already registered as it will - # be ignored. - # - # An entity may suggest the entity_id by setting entity_id itself - suggested_entity_id: str | None = entity.entity_id - if suggested_entity_id is not None: - suggested_object_id = split_entity_id(entity.entity_id)[1] - else: - if device and entity.has_entity_name: - device_name = device.name_by_user or device.name - if entity.use_device_name: - suggested_object_id = device_name - else: - suggested_object_id = ( - f"{device_name} {entity.suggested_object_id}" - ) - if not suggested_object_id: - suggested_object_id = entity.suggested_object_id + # An entity may suggest the entity_id by setting entity_id itself + calculated_object_id: str | None = None + suggested_entity_id: str | None = entity.entity_id + if suggested_entity_id is not None: + suggested_object_id = split_entity_id(entity.entity_id)[1] + else: + calculated_object_id = async_calculate_suggested_object_id( + entity, device + ) - if self.entity_namespace is not None: - suggested_object_id = ( - f"{self.entity_namespace} {suggested_object_id}" - ) + if self.entity_namespace is not None and suggested_object_id is not None: + suggested_object_id = f"{self.entity_namespace} {suggested_object_id}" disabled_by: RegistryEntryDisabler | None = None if not entity.entity_registry_enabled_default: @@ -881,6 +868,7 @@ class EntityPlatform: self.domain, self.platform_name, entity.unique_id, + calculated_object_id=calculated_object_id, capabilities=entity.capability_attributes, config_entry=self.config_entry, config_subentry_id=config_subentry_id, @@ -1124,6 +1112,27 @@ class EntityPlatform: await asyncio.gather(*tasks) +@callback +def async_calculate_suggested_object_id( + entity: Entity, device: dev_reg.DeviceEntry | None +) -> str | None: + """Calculate the suggested object ID for an entity.""" + calculated_object_id: str | None = None + if device and entity.has_entity_name: + device_name = device.name_by_user or device.name + if entity.use_device_name: + calculated_object_id = device_name + else: + calculated_object_id = f"{device_name} {entity.suggested_object_id}" + if not calculated_object_id: + calculated_object_id = entity.suggested_object_id + + if (platform := entity.platform) and platform.entity_namespace is not None: + calculated_object_id = f"{platform.entity_namespace} {calculated_object_id}" + + return calculated_object_id + + current_platform: ContextVar[EntityPlatform | None] = ContextVar( "current_platform", default=None ) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 530f1f24be5..12c4848a346 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -195,6 +195,7 @@ class RegistryEntry: name: str | None = attr.ib(default=None) options: ReadOnlyEntityOptionsType = attr.ib(converter=_protect_entity_options) # As set by integration + calculated_object_id: str | None = attr.ib() original_device_class: str | None = attr.ib() original_icon: str | None = attr.ib() original_name: str | None = attr.ib() @@ -338,6 +339,7 @@ class RegistryEntry: { "aliases": list(self.aliases), "area_id": self.area_id, + "calculated_object_id": self.calculated_object_id, "categories": self.categories, "capabilities": self.capabilities, "config_entry_id": self.config_entry_id, @@ -551,8 +553,9 @@ class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): entity["config_subentry_id"] = None if old_minor_version < 17: - # Version 1.17 adds suggested_object_id + # Version 1.17 adds calculated_object_id and suggested_object_id for entity in data["entities"]: + entity["calculated_object_id"] = None entity["suggested_object_id"] = None if old_major_version > 1: @@ -843,6 +846,7 @@ class EntityRegistry(BaseRegistry): unique_id: str, *, # To influence entity ID generation + calculated_object_id: str | None = None, suggested_object_id: str | None = None, # To disable or hide an entity if it gets created disabled_by: RegistryEntryDisabler | None = None, @@ -915,7 +919,7 @@ class EntityRegistry(BaseRegistry): entity_id = self.async_generate_entity_id( domain, - suggested_object_id or f"{platform}_{unique_id}", + suggested_object_id or calculated_object_id or f"{platform}_{unique_id}", ) if ( @@ -949,6 +953,7 @@ class EntityRegistry(BaseRegistry): original_icon=none_if_undefined(original_icon), original_name=none_if_undefined(original_name), platform=platform, + calculated_object_id=calculated_object_id, suggested_object_id=suggested_object_id, supported_features=none_if_undefined(supported_features) or 0, translation_key=none_if_undefined(translation_key), @@ -1358,6 +1363,7 @@ class EntityRegistry(BaseRegistry): entities[entity["entity_id"]] = RegistryEntry( aliases=set(entity["aliases"]), area_id=entity["area_id"], + calculated_object_id=entity["calculated_object_id"], categories=entity["categories"], capabilities=entity["capabilities"], config_entry_id=entity["config_entry_id"], diff --git a/tests/common.py b/tests/common.py index bfc8ef65063..41d197cb7e3 100644 --- a/tests/common.py +++ b/tests/common.py @@ -651,6 +651,7 @@ class RegistryEntryWithDefaults(er.RegistryEntry): """Helper to create a registry entry with defaults.""" capabilities: Mapping[str, Any] | None = attr.ib(default=None) + calculated_object_id: str | None = attr.ib(default=None) config_entry_id: str | None = attr.ib(default=None) config_subentry_id: str | None = attr.ib(default=None) created_at: datetime = attr.ib(factory=dt_util.utcnow) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 49839abc2fb..ba9b49a95c4 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -1532,6 +1532,7 @@ async def test_entity_info_added_to_entity_registry( entity_id="test_domain.best_name", unique_id="default", platform="test_domain", + calculated_object_id="best name", capabilities={"max": 100}, config_entry_id=None, config_subentry_id=None, @@ -1550,7 +1551,7 @@ async def test_entity_info_added_to_entity_registry( original_icon="nice:icon", original_name="best name", options=None, - suggested_object_id="best name", + suggested_object_id=None, 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 72628debb58..b9cbec97631 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -126,6 +126,7 @@ def test_get_or_create_updates_data( entity_id="light.hue_5678", unique_id="5678", platform="hue", + calculated_object_id=None, capabilities={"max": 100}, config_entry_id=orig_config_entry.entry_id, config_subentry_id=config_subentry_id, @@ -185,6 +186,7 @@ def test_get_or_create_updates_data( platform="hue", aliases=set(), area_id=None, + calculated_object_id=None, capabilities={"new-max": 150}, config_entry_id=new_config_entry.entry_id, config_subentry_id=None, @@ -238,6 +240,7 @@ def test_get_or_create_updates_data( platform="hue", aliases=set(), area_id=None, + calculated_object_id=None, capabilities=None, config_entry_id=None, config_subentry_id=None, @@ -517,6 +520,7 @@ async def test_load_bad_data( { "aliases": [], "area_id": None, + "calculated_object_id": None, "capabilities": None, "categories": {}, "config_entry_id": None, @@ -549,6 +553,7 @@ async def test_load_bad_data( { "aliases": [], "area_id": None, + "calculated_object_id": None, "capabilities": None, "categories": {}, "config_entry_id": None, @@ -905,6 +910,7 @@ async def test_migration_1_1(hass: HomeAssistant, hass_storage: dict[str, Any]) { "aliases": [], "area_id": None, + "calculated_object_id": None, "capabilities": {}, "categories": {}, "config_entry_id": None, @@ -1085,6 +1091,7 @@ async def test_migration_1_11( { "aliases": [], "area_id": None, + "calculated_object_id": None, "capabilities": {}, "categories": {}, "config_entry_id": None,