Calculate suggested object id from entity properties

This commit is contained in:
Erik 2025-05-09 15:36:49 +02:00
parent 9f039002ff
commit 0924740cb4
7 changed files with 95 additions and 34 deletions

View File

@ -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(

View File

@ -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]:

View File

@ -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
)

View File

@ -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"],

View File

@ -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)

View File

@ -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,

View File

@ -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,