mirror of
https://github.com/home-assistant/core.git
synced 2026-08-03 20:24:55 +02:00
Add setting to customise entity ID generation (#177140)
This commit is contained in:
@@ -30,6 +30,8 @@ def async_setup(hass: HomeAssistant) -> bool:
|
||||
websocket_api.async_register_command(hass, websocket_list_entities)
|
||||
websocket_api.async_register_command(hass, websocket_remove_entity)
|
||||
websocket_api.async_register_command(hass, websocket_update_entity)
|
||||
websocket_api.async_register_command(hass, websocket_get_settings)
|
||||
websocket_api.async_register_command(hass, websocket_update_settings)
|
||||
return True
|
||||
|
||||
|
||||
@@ -362,3 +364,49 @@ def websocket_get_automatic_entity_ids(
|
||||
connection.send_message(
|
||||
websocket_api.result_message(msg["id"], automatic_entity_ids)
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{vol.Required("type"): "config/entity_registry/settings/get"}
|
||||
)
|
||||
@callback
|
||||
def websocket_get_settings(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Handle get entity registry settings command."""
|
||||
registry = er.async_get(hass)
|
||||
connection.send_result(
|
||||
msg["id"], {"entity_id_parts": registry.settings.entity_id_parts}
|
||||
)
|
||||
|
||||
|
||||
@require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "config/entity_registry/settings/update",
|
||||
vol.Optional("entity_id_parts"): vol.Any(
|
||||
None,
|
||||
vol.All(
|
||||
[vol.Coerce(er.EntityNamePart)],
|
||||
vol.Unique(),
|
||||
vol.Contains(er.EntityNamePart.ENTITY),
|
||||
vol.Contains(er.EntityNamePart.DEVICE),
|
||||
),
|
||||
),
|
||||
}
|
||||
)
|
||||
@callback
|
||||
def websocket_update_settings(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Handle update entity registry settings command."""
|
||||
registry = er.async_get(hass)
|
||||
changes: dict[str, Any] = {}
|
||||
if "entity_id_parts" in msg:
|
||||
changes["entity_id_parts"] = msg["entity_id_parts"]
|
||||
settings = registry.async_update_settings(**changes)
|
||||
connection.send_result(msg["id"], {"entity_id_parts": settings.entity_id_parts})
|
||||
|
||||
@@ -9,7 +9,9 @@ timer.
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable, Hashable, KeysView, Mapping
|
||||
from collections.abc import Callable, Hashable, KeysView, Mapping, Sequence
|
||||
import dataclasses
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum, StrEnum
|
||||
import logging
|
||||
@@ -45,7 +47,7 @@ from homeassistant.util.hass_dict import HassKey
|
||||
from homeassistant.util.json import format_unserializable_data
|
||||
from homeassistant.util.read_only_dict import ReadOnlyDict
|
||||
|
||||
from . import area_registry as ar, device_registry as dr, storage
|
||||
from . import area_registry as ar, device_registry as dr, floor_registry as fr, storage
|
||||
from .device_registry import (
|
||||
EVENT_DEVICE_REGISTRY_UPDATED,
|
||||
EventDeviceRegistryUpdatedData,
|
||||
@@ -72,7 +74,7 @@ EVENT_ENTITY_REGISTRY_UPDATED: EventType[EventEntityRegistryUpdatedData] = Event
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STORAGE_VERSION_MAJOR = 1
|
||||
STORAGE_VERSION_MINOR = 22
|
||||
STORAGE_VERSION_MINOR = 23
|
||||
STORAGE_KEY = "core.entity_registry"
|
||||
|
||||
CLEANUP_INTERVAL = 3600 * 24
|
||||
@@ -135,6 +137,22 @@ class RegistryEntryHider(StrEnum):
|
||||
USER = "user"
|
||||
|
||||
|
||||
class EntityNamePart(StrEnum):
|
||||
"""Parts a generated full entity name can be composed of."""
|
||||
|
||||
AREA = "area"
|
||||
DEVICE = "device"
|
||||
ENTITY = "entity"
|
||||
FLOOR = "floor"
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True, slots=True)
|
||||
class EntityRegistrySettings:
|
||||
"""Global entity registry settings."""
|
||||
|
||||
entity_id_parts: tuple[EntityNamePart, ...] | None = None
|
||||
|
||||
|
||||
class _EventEntityRegistryUpdatedData_CreateRemove(TypedDict):
|
||||
"""EventEntityRegistryUpdated data for action type 'create' and 'remove'."""
|
||||
|
||||
@@ -475,7 +493,7 @@ def async_get_unprefixed_name(hass: HomeAssistant, entry: RegistryEntry) -> str:
|
||||
def _async_get_full_entity_name(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
area_id: str | None | UndefinedType = UNDEFINED,
|
||||
area_id: str | None,
|
||||
device_id: str | None,
|
||||
fallback: str,
|
||||
has_entity_name: bool,
|
||||
@@ -483,6 +501,7 @@ def _async_get_full_entity_name(
|
||||
original_name: str | None,
|
||||
original_name_unprefixed: str | None | UndefinedType = UNDEFINED,
|
||||
overridden_name: str | None = None,
|
||||
parts: Sequence[EntityNamePart],
|
||||
unprefix_name: bool = False,
|
||||
use_legacy_naming: bool = False,
|
||||
) -> str:
|
||||
@@ -506,12 +525,20 @@ def _async_get_full_entity_name(
|
||||
area_id = device.area_id
|
||||
|
||||
area_name: str | None = None
|
||||
floor_name: str | None = None
|
||||
if (
|
||||
area_id is not UNDEFINED
|
||||
(EntityNamePart.AREA in parts or EntityNamePart.FLOOR in parts)
|
||||
and area_id is not None
|
||||
and (area := ar.async_get(hass).async_get_area(area_id)) is not None
|
||||
):
|
||||
area_name = area.name
|
||||
if (
|
||||
EntityNamePart.FLOOR in parts
|
||||
and area.floor_id is not None
|
||||
and (floor := fr.async_get(hass).async_get_floor(area.floor_id))
|
||||
is not None
|
||||
):
|
||||
floor_name = floor.name
|
||||
|
||||
entity_name = name
|
||||
if entity_name is None:
|
||||
@@ -532,8 +559,14 @@ def _async_get_full_entity_name(
|
||||
if unprefixed_name is not None:
|
||||
entity_name = unprefixed_name
|
||||
|
||||
part_names = {
|
||||
EntityNamePart.AREA: area_name,
|
||||
EntityNamePart.DEVICE: device_name,
|
||||
EntityNamePart.ENTITY: entity_name,
|
||||
EntityNamePart.FLOOR: floor_name,
|
||||
}
|
||||
full_name = " ".join(
|
||||
part for part in (area_name, device_name, entity_name) if part
|
||||
part_name for part in parts if (part_name := part_names[part])
|
||||
)
|
||||
|
||||
else:
|
||||
@@ -559,12 +592,14 @@ def async_get_full_entity_name(
|
||||
|
||||
return _async_get_full_entity_name(
|
||||
hass,
|
||||
area_id=entry.area_id,
|
||||
device_id=entry.device_id,
|
||||
fallback="",
|
||||
has_entity_name=entry.has_entity_name,
|
||||
name=entry.name,
|
||||
original_name=original_name,
|
||||
original_name_unprefixed=original_name_unprefixed,
|
||||
parts=(EntityNamePart.DEVICE, EntityNamePart.ENTITY),
|
||||
use_legacy_naming=True,
|
||||
)
|
||||
|
||||
@@ -739,7 +774,7 @@ class DeletedRegistryEntry:
|
||||
)
|
||||
|
||||
|
||||
class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
|
||||
class EntityRegistryStore(storage.Store[dict[str, Any]]):
|
||||
"""Store entity registry data."""
|
||||
|
||||
@override
|
||||
@@ -747,7 +782,7 @@ class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
|
||||
self,
|
||||
old_major_version: int,
|
||||
old_minor_version: int,
|
||||
old_data: dict[str, list[dict[str, Any]]],
|
||||
old_data: dict[str, Any],
|
||||
) -> dict:
|
||||
"""Migrate to the new version."""
|
||||
data = old_data
|
||||
@@ -915,6 +950,10 @@ class EntityRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
|
||||
for entity in data["deleted_entities"]:
|
||||
entity["aliases_v2"] = [None, *entity["aliases"]]
|
||||
|
||||
if old_minor_version < 23:
|
||||
# Version 1.23 adds settings
|
||||
data["settings"] = {"entity_id_parts": None}
|
||||
|
||||
if old_major_version > 1:
|
||||
raise NotImplementedError
|
||||
return data
|
||||
@@ -1163,6 +1202,7 @@ class EntityRegistry(BaseRegistry):
|
||||
|
||||
deleted_entities: dict[tuple[str, str, str], DeletedRegistryEntry]
|
||||
entities: EntityRegistryItems
|
||||
settings: EntityRegistrySettings
|
||||
_entities_data: dict[str, RegistryEntry]
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
@@ -1322,6 +1362,9 @@ class EntityRegistry(BaseRegistry):
|
||||
Entity ID conflicts are checked against registered and currently
|
||||
existing entities, as well as provided `reserved_entity_ids`.
|
||||
"""
|
||||
parts = self.settings.entity_id_parts
|
||||
if parts is None:
|
||||
parts = (EntityNamePart.AREA, EntityNamePart.DEVICE, EntityNamePart.ENTITY)
|
||||
object_id = _async_get_full_entity_name(
|
||||
self.hass,
|
||||
area_id=area_id,
|
||||
@@ -1331,6 +1374,7 @@ class EntityRegistry(BaseRegistry):
|
||||
name=name,
|
||||
original_name=object_id_base,
|
||||
overridden_name=suggested_object_id,
|
||||
parts=parts,
|
||||
unprefix_name=True,
|
||||
)
|
||||
return self.async_get_available_entity_id(
|
||||
@@ -2076,6 +2120,32 @@ class EntityRegistry(BaseRegistry):
|
||||
new_options[domain] = options
|
||||
return self._async_update_entity(entity_id, options=new_options)
|
||||
|
||||
@callback
|
||||
def async_update_settings(
|
||||
self,
|
||||
*,
|
||||
entity_id_parts: list[EntityNamePart] | None | UndefinedType = UNDEFINED,
|
||||
) -> EntityRegistrySettings:
|
||||
"""Update entity registry settings."""
|
||||
self.hass.verify_event_loop_thread("entity_registry.async_update_settings")
|
||||
|
||||
old = self.settings
|
||||
new = old
|
||||
if entity_id_parts is not UNDEFINED:
|
||||
new = dataclasses.replace(
|
||||
new,
|
||||
entity_id_parts=None
|
||||
if entity_id_parts is None
|
||||
else tuple(entity_id_parts),
|
||||
)
|
||||
|
||||
if new == old:
|
||||
return old
|
||||
|
||||
self.settings = new
|
||||
self.async_schedule_save()
|
||||
return new
|
||||
|
||||
@override
|
||||
async def _async_load(self) -> None:
|
||||
"""Load the entity registry."""
|
||||
@@ -2092,6 +2162,7 @@ class EntityRegistry(BaseRegistry):
|
||||
data = await self._store.async_load()
|
||||
entities = EntityRegistryItems(self.hass)
|
||||
deleted_entities: dict[tuple[str, str, str], DeletedRegistryEntry] = {}
|
||||
settings = EntityRegistrySettings()
|
||||
|
||||
# Move entities to the correct device when a pre-migration composite device was
|
||||
# split into one device per config entry. This can be removed 12 months after
|
||||
@@ -2276,8 +2347,14 @@ class EntityRegistry(BaseRegistry):
|
||||
unique_id=entity["unique_id"],
|
||||
)
|
||||
|
||||
self.deleted_entities = deleted_entities
|
||||
if (parts_data := data["settings"]["entity_id_parts"]) is not None:
|
||||
settings = EntityRegistrySettings(
|
||||
entity_id_parts=tuple(EntityNamePart(part) for part in parts_data)
|
||||
)
|
||||
|
||||
self.entities = entities
|
||||
self.deleted_entities = deleted_entities
|
||||
self.settings = settings
|
||||
self._entities_data = entities.data
|
||||
|
||||
# Persist entities moved off a split pre-migration composite device
|
||||
@@ -2297,6 +2374,7 @@ class EntityRegistry(BaseRegistry):
|
||||
entry.as_storage_fragment
|
||||
for entry in list(self.deleted_entities.values())
|
||||
],
|
||||
"settings": {"entity_id_parts": self.settings.entity_id_parts},
|
||||
}
|
||||
|
||||
@callback
|
||||
|
||||
@@ -679,6 +679,7 @@ def mock_registry(
|
||||
mock_entries = {}
|
||||
registry.deleted_entities = {}
|
||||
registry.entities = er.EntityRegistryItems(hass)
|
||||
registry.settings = er.EntityRegistrySettings()
|
||||
registry._entities_data = registry.entities.data
|
||||
for key, entry in mock_entries.items():
|
||||
registry.entities[key] = entry
|
||||
|
||||
@@ -24,6 +24,7 @@ from tests.common import (
|
||||
MockConfigEntry,
|
||||
MockEntity,
|
||||
MockEntityPlatform,
|
||||
MockUser,
|
||||
RegistryEntryWithDefaults,
|
||||
mock_registry,
|
||||
)
|
||||
@@ -1576,3 +1577,108 @@ async def test_get_automatic_entity_ids(
|
||||
# no test_domain.unknown in registry
|
||||
"test_domain.unknown": None,
|
||||
}
|
||||
|
||||
|
||||
async def test_get_settings(client: MockHAClientWebSocket) -> None:
|
||||
"""Test get settings."""
|
||||
await client.send_json_auto_id({"type": "config/entity_registry/settings/get"})
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert msg["success"]
|
||||
assert msg["result"] == {"entity_id_parts": None}
|
||||
|
||||
|
||||
async def test_update_settings(
|
||||
client: MockHAClientWebSocket,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test update settings."""
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "config/entity_registry/settings/update",
|
||||
"entity_id_parts": ["floor", "area", "device", "entity"],
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert msg["success"]
|
||||
assert msg["result"] == {"entity_id_parts": ["floor", "area", "device", "entity"]}
|
||||
assert entity_registry.settings.entity_id_parts == (
|
||||
er.EntityNamePart.FLOOR,
|
||||
er.EntityNamePart.AREA,
|
||||
er.EntityNamePart.DEVICE,
|
||||
er.EntityNamePart.ENTITY,
|
||||
)
|
||||
|
||||
await client.send_json_auto_id({"type": "config/entity_registry/settings/get"})
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert msg["success"]
|
||||
assert msg["result"] == {"entity_id_parts": ["floor", "area", "device", "entity"]}
|
||||
|
||||
# Clear the override
|
||||
await client.send_json_auto_id(
|
||||
{"type": "config/entity_registry/settings/update", "entity_id_parts": None}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert msg["success"]
|
||||
assert msg["result"] == {"entity_id_parts": None}
|
||||
assert entity_registry.settings.entity_id_parts is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"entity_id_parts",
|
||||
[
|
||||
pytest.param(["entity", "device", "bad_part"], id="unknown_part"),
|
||||
pytest.param(["entity", "device", "entity"], id="duplicate"),
|
||||
pytest.param(["entity"], id="missing_device"),
|
||||
pytest.param(["device"], id="missing_entity"),
|
||||
pytest.param([], id="empty"),
|
||||
],
|
||||
)
|
||||
async def test_update_settings_invalid(
|
||||
client: MockHAClientWebSocket,
|
||||
entity_registry: er.EntityRegistry,
|
||||
entity_id_parts: list[str],
|
||||
) -> None:
|
||||
"""Test update settings with an invalid parts list."""
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "config/entity_registry/settings/update",
|
||||
"entity_id_parts": entity_id_parts,
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert not msg["success"]
|
||||
assert msg["error"]["code"] == "invalid_format"
|
||||
assert entity_registry.settings.entity_id_parts is None
|
||||
|
||||
|
||||
async def test_update_settings_requires_admin(
|
||||
client: MockHAClientWebSocket,
|
||||
entity_registry: er.EntityRegistry,
|
||||
hass_admin_user: MockUser,
|
||||
) -> None:
|
||||
"""Test update settings fails for non admin."""
|
||||
hass_admin_user.groups = []
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "config/entity_registry/settings/update",
|
||||
"entity_id_parts": ["device", "entity"],
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert not msg["success"]
|
||||
assert msg["error"]["code"] == "unauthorized"
|
||||
assert entity_registry.settings.entity_id_parts is None
|
||||
|
||||
# Reading settings is not restricted
|
||||
await client.send_json_auto_id({"type": "config/entity_registry/settings/get"})
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert msg["success"]
|
||||
assert msg["result"] == {"entity_id_parts": None}
|
||||
|
||||
@@ -23,6 +23,7 @@ from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
floor_registry as fr,
|
||||
)
|
||||
from homeassistant.helpers.event import async_track_entity_registry_updated_event
|
||||
from homeassistant.helpers.typing import UNDEFINED
|
||||
@@ -467,6 +468,10 @@ async def test_loading_saving_data(
|
||||
assert len(entity_registry.entities) == 2
|
||||
assert len(entity_registry.deleted_entities) == 2
|
||||
|
||||
entity_registry.async_update_settings(
|
||||
entity_id_parts=[er.EntityNamePart.ENTITY, er.EntityNamePart.DEVICE]
|
||||
)
|
||||
|
||||
# Now load written data in new registry
|
||||
registry2 = er.EntityRegistry(hass)
|
||||
await flush_store(entity_registry._store)
|
||||
@@ -475,6 +480,11 @@ async def test_loading_saving_data(
|
||||
# Ensure same order
|
||||
assert list(entity_registry.entities) == list(registry2.entities)
|
||||
assert list(entity_registry.deleted_entities) == list(registry2.deleted_entities)
|
||||
assert registry2.settings == entity_registry.settings
|
||||
assert registry2.settings.entity_id_parts == (
|
||||
er.EntityNamePart.ENTITY,
|
||||
er.EntityNamePart.DEVICE,
|
||||
)
|
||||
new_entry1 = entity_registry.async_get_or_create("light", "hue", "1234")
|
||||
new_entry2 = entity_registry.async_get_or_create("light", "hue", "5678")
|
||||
new_entry3 = entity_registry.async_get_or_create("light", "hue", "ABCD")
|
||||
@@ -779,6 +789,419 @@ def test_generate_entity_id(
|
||||
assert new_entity_id == expected_entity_id
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"entity_id_parts",
|
||||
"device_name",
|
||||
"device_area_name",
|
||||
"floor_name",
|
||||
"has_entity_name",
|
||||
"object_id_base",
|
||||
"suggested_object_id",
|
||||
"user_name",
|
||||
"expected_initial_entity_id",
|
||||
"expected_entity_id",
|
||||
),
|
||||
[
|
||||
pytest.param(
|
||||
[
|
||||
er.EntityNamePart.FLOOR,
|
||||
er.EntityNamePart.AREA,
|
||||
er.EntityNamePart.DEVICE,
|
||||
er.EntityNamePart.ENTITY,
|
||||
],
|
||||
"Lamp",
|
||||
"Kitchen",
|
||||
"First Floor",
|
||||
True,
|
||||
"Temperature",
|
||||
None,
|
||||
None,
|
||||
"sensor.first_floor_kitchen_lamp_temperature",
|
||||
"sensor.first_floor_kitchen_lamp_temperature",
|
||||
id="all_parts",
|
||||
),
|
||||
pytest.param(
|
||||
[
|
||||
er.EntityNamePart.FLOOR,
|
||||
er.EntityNamePart.AREA,
|
||||
er.EntityNamePart.DEVICE,
|
||||
er.EntityNamePart.ENTITY,
|
||||
],
|
||||
"Lamp",
|
||||
"Kitchen",
|
||||
None,
|
||||
True,
|
||||
"Temperature",
|
||||
None,
|
||||
None,
|
||||
"sensor.kitchen_lamp_temperature",
|
||||
"sensor.kitchen_lamp_temperature",
|
||||
id="area_without_floor",
|
||||
),
|
||||
pytest.param(
|
||||
[er.EntityNamePart.DEVICE, er.EntityNamePart.ENTITY],
|
||||
"Lamp",
|
||||
"Kitchen",
|
||||
"First Floor",
|
||||
True,
|
||||
"Temperature",
|
||||
None,
|
||||
None,
|
||||
"sensor.lamp_temperature",
|
||||
"sensor.lamp_temperature",
|
||||
id="area_not_included",
|
||||
),
|
||||
pytest.param(
|
||||
[
|
||||
er.EntityNamePart.FLOOR,
|
||||
er.EntityNamePart.DEVICE,
|
||||
er.EntityNamePart.ENTITY,
|
||||
],
|
||||
"Lamp",
|
||||
"Kitchen",
|
||||
"First Floor",
|
||||
True,
|
||||
"Temperature",
|
||||
None,
|
||||
None,
|
||||
"sensor.first_floor_lamp_temperature",
|
||||
"sensor.first_floor_lamp_temperature",
|
||||
id="floor_without_area",
|
||||
),
|
||||
pytest.param(
|
||||
[er.EntityNamePart.ENTITY, er.EntityNamePart.DEVICE],
|
||||
"Lamp",
|
||||
"Kitchen",
|
||||
None,
|
||||
True,
|
||||
"Temperature",
|
||||
None,
|
||||
None,
|
||||
"sensor.temperature_lamp",
|
||||
"sensor.temperature_lamp",
|
||||
id="reordered",
|
||||
),
|
||||
pytest.param(
|
||||
[er.EntityNamePart.DEVICE, er.EntityNamePart.ENTITY],
|
||||
"Lamp",
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
"Lamp Temperature",
|
||||
None,
|
||||
None,
|
||||
"sensor.lamp_temperature",
|
||||
"sensor.lamp_temperature",
|
||||
id="legacy_name_stripped",
|
||||
),
|
||||
pytest.param(
|
||||
[er.EntityNamePart.ENTITY, er.EntityNamePart.DEVICE],
|
||||
"Lamp",
|
||||
None,
|
||||
None,
|
||||
False,
|
||||
"Lamp Temperature",
|
||||
None,
|
||||
None,
|
||||
"sensor.temperature_lamp",
|
||||
"sensor.temperature_lamp",
|
||||
id="legacy_name_reordered",
|
||||
),
|
||||
pytest.param(
|
||||
[er.EntityNamePart.DEVICE, er.EntityNamePart.ENTITY],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
True,
|
||||
"My Sensor",
|
||||
None,
|
||||
None,
|
||||
"sensor.my_sensor",
|
||||
"sensor.my_sensor",
|
||||
id="no_device",
|
||||
),
|
||||
pytest.param(
|
||||
[er.EntityNamePart.DEVICE, er.EntityNamePart.ENTITY],
|
||||
"Lamp",
|
||||
None,
|
||||
None,
|
||||
True,
|
||||
"Temperature",
|
||||
"custom_id",
|
||||
None,
|
||||
"sensor.custom_id",
|
||||
"sensor.custom_id",
|
||||
id="suggested_object_id_respected",
|
||||
),
|
||||
pytest.param(
|
||||
[er.EntityNamePart.ENTITY, er.EntityNamePart.DEVICE],
|
||||
"Lamp",
|
||||
None,
|
||||
None,
|
||||
True,
|
||||
"Temperature",
|
||||
None,
|
||||
"Lamp Humidity",
|
||||
"sensor.temperature_lamp",
|
||||
"sensor.humidity_lamp",
|
||||
id="user_name_unprefixed",
|
||||
),
|
||||
pytest.param(
|
||||
[er.EntityNamePart.ENTITY, er.EntityNamePart.DEVICE],
|
||||
"Lamp",
|
||||
None,
|
||||
None,
|
||||
True,
|
||||
"Temperature",
|
||||
"custom_id",
|
||||
"Lamp Humidity",
|
||||
"sensor.custom_id",
|
||||
"sensor.humidity_lamp",
|
||||
id="user_name_over_suggested_object_id",
|
||||
),
|
||||
pytest.param(
|
||||
[er.EntityNamePart.DEVICE, er.EntityNamePart.ENTITY],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
True,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
"sensor.test_1234",
|
||||
"sensor.test_1234",
|
||||
id="all_parts_empty",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_generate_entity_id_parts(
|
||||
hass: HomeAssistant,
|
||||
area_registry: ar.AreaRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
floor_registry: fr.FloorRegistry,
|
||||
entity_id_parts: list[er.EntityNamePart],
|
||||
device_name: str | None,
|
||||
device_area_name: str | None,
|
||||
floor_name: str | None,
|
||||
has_entity_name: bool,
|
||||
object_id_base: str | None,
|
||||
suggested_object_id: str | None,
|
||||
user_name: str | None,
|
||||
expected_initial_entity_id: str,
|
||||
expected_entity_id: str,
|
||||
) -> None:
|
||||
"""Test generating and regenerating entity IDs with configured parts."""
|
||||
config_entry = MockConfigEntry(domain="sensor")
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
entity_registry.async_update_settings(entity_id_parts=entity_id_parts)
|
||||
|
||||
device_id: str | None = None
|
||||
if device_name is not None:
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
|
||||
name=device_name,
|
||||
)
|
||||
device_id = device_entry.id
|
||||
if device_area_name is not None:
|
||||
device_area = area_registry.async_create(device_area_name)
|
||||
if floor_name is not None:
|
||||
floor = floor_registry.async_create(floor_name)
|
||||
area_registry.async_update(device_area.id, floor_id=floor.floor_id)
|
||||
device_registry.async_update_device(device_id, area_id=device_area.id)
|
||||
|
||||
entry = entity_registry.async_get_or_create(
|
||||
"sensor",
|
||||
"test",
|
||||
"1234",
|
||||
config_entry=config_entry,
|
||||
device_id=device_id,
|
||||
has_entity_name=has_entity_name,
|
||||
object_id_base=object_id_base,
|
||||
original_name=object_id_base,
|
||||
suggested_object_id=suggested_object_id,
|
||||
)
|
||||
assert entry.entity_id == expected_initial_entity_id
|
||||
|
||||
if user_name is not None:
|
||||
entry = entity_registry.async_update_entity(entry.entity_id, name=user_name)
|
||||
|
||||
new_entity_id = entity_registry.async_regenerate_entity_id(entry)
|
||||
assert new_entity_id == expected_entity_id
|
||||
|
||||
|
||||
def test_generate_entity_id_parts_entity_area(
|
||||
hass: HomeAssistant,
|
||||
area_registry: ar.AreaRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
floor_registry: fr.FloorRegistry,
|
||||
) -> None:
|
||||
"""Test the entity area override drives the floor part."""
|
||||
config_entry = MockConfigEntry(domain="sensor")
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
entity_registry.async_update_settings(
|
||||
entity_id_parts=[
|
||||
er.EntityNamePart.FLOOR,
|
||||
er.EntityNamePart.AREA,
|
||||
er.EntityNamePart.DEVICE,
|
||||
er.EntityNamePart.ENTITY,
|
||||
]
|
||||
)
|
||||
|
||||
first_floor = floor_registry.async_create("First Floor")
|
||||
second_floor = floor_registry.async_create("Second Floor")
|
||||
kitchen = area_registry.async_create("Kitchen")
|
||||
area_registry.async_update(kitchen.id, floor_id=first_floor.floor_id)
|
||||
garage = area_registry.async_create("Garage")
|
||||
area_registry.async_update(garage.id, floor_id=second_floor.floor_id)
|
||||
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
|
||||
name="Lamp",
|
||||
)
|
||||
device_registry.async_update_device(device_entry.id, area_id=kitchen.id)
|
||||
|
||||
entry = entity_registry.async_get_or_create(
|
||||
"sensor",
|
||||
"test",
|
||||
"1234",
|
||||
config_entry=config_entry,
|
||||
device_id=device_entry.id,
|
||||
has_entity_name=True,
|
||||
object_id_base="Temperature",
|
||||
original_name="Temperature",
|
||||
)
|
||||
assert entry.entity_id == "sensor.first_floor_kitchen_lamp_temperature"
|
||||
|
||||
entry = entity_registry.async_update_entity(entry.entity_id, area_id=garage.id)
|
||||
|
||||
new_entity_id = entity_registry.async_regenerate_entity_id(entry)
|
||||
assert new_entity_id == "sensor.second_floor_garage_lamp_temperature"
|
||||
|
||||
|
||||
def test_regenerate_entity_id_after_settings_change(
|
||||
hass: HomeAssistant,
|
||||
area_registry: ar.AreaRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test regenerating an entity ID after the parts setting changed."""
|
||||
config_entry = MockConfigEntry(domain="sensor")
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
kitchen = area_registry.async_create("Kitchen")
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
|
||||
name="Lamp",
|
||||
)
|
||||
device_registry.async_update_device(device_entry.id, area_id=kitchen.id)
|
||||
|
||||
entry = entity_registry.async_get_or_create(
|
||||
"sensor",
|
||||
"test",
|
||||
"1234",
|
||||
config_entry=config_entry,
|
||||
device_id=device_entry.id,
|
||||
has_entity_name=True,
|
||||
object_id_base="Temperature",
|
||||
original_name="Temperature",
|
||||
)
|
||||
assert entry.entity_id == "sensor.kitchen_lamp_temperature"
|
||||
|
||||
entity_registry.async_update_settings(
|
||||
entity_id_parts=[er.EntityNamePart.DEVICE, er.EntityNamePart.ENTITY]
|
||||
)
|
||||
|
||||
new_entity_id = entity_registry.async_regenerate_entity_id(entry)
|
||||
assert new_entity_id == "sensor.lamp_temperature"
|
||||
|
||||
|
||||
def test_entity_id_parts_do_not_affect_full_entity_name(
|
||||
hass: HomeAssistant,
|
||||
area_registry: ar.AreaRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
floor_registry: fr.FloorRegistry,
|
||||
) -> None:
|
||||
"""Test the entity ID parts setting does not affect the full entity name."""
|
||||
config_entry = MockConfigEntry(domain="sensor")
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
entity_registry.async_update_settings(
|
||||
entity_id_parts=[
|
||||
er.EntityNamePart.FLOOR,
|
||||
er.EntityNamePart.AREA,
|
||||
er.EntityNamePart.ENTITY,
|
||||
er.EntityNamePart.DEVICE,
|
||||
]
|
||||
)
|
||||
|
||||
floor = floor_registry.async_create("First Floor")
|
||||
kitchen = area_registry.async_create("Kitchen")
|
||||
area_registry.async_update(kitchen.id, floor_id=floor.floor_id)
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
|
||||
name="Lamp",
|
||||
)
|
||||
device_registry.async_update_device(device_entry.id, area_id=kitchen.id)
|
||||
|
||||
entry = entity_registry.async_get_or_create(
|
||||
"sensor",
|
||||
"test",
|
||||
"1234",
|
||||
config_entry=config_entry,
|
||||
device_id=device_entry.id,
|
||||
has_entity_name=True,
|
||||
object_id_base="Temperature",
|
||||
original_name="Temperature",
|
||||
)
|
||||
assert entry.entity_id == "sensor.first_floor_kitchen_temperature_lamp"
|
||||
|
||||
assert er.async_get_full_entity_name(hass, entry) == "Lamp Temperature"
|
||||
|
||||
|
||||
def test_update_settings(entity_registry: er.EntityRegistry) -> None:
|
||||
"""Test updating entity registry settings."""
|
||||
assert entity_registry.settings == er.EntityRegistrySettings(entity_id_parts=None)
|
||||
|
||||
with patch.object(entity_registry, "async_schedule_save") as mock_schedule_save:
|
||||
settings = entity_registry.async_update_settings(
|
||||
entity_id_parts=[er.EntityNamePart.DEVICE, er.EntityNamePart.ENTITY]
|
||||
)
|
||||
assert settings.entity_id_parts == (
|
||||
er.EntityNamePart.DEVICE,
|
||||
er.EntityNamePart.ENTITY,
|
||||
)
|
||||
assert entity_registry.settings == settings
|
||||
assert len(mock_schedule_save.mock_calls) == 1
|
||||
|
||||
# Unchanged settings don't trigger a save
|
||||
with patch.object(entity_registry, "async_schedule_save") as mock_schedule_save:
|
||||
settings = entity_registry.async_update_settings(
|
||||
entity_id_parts=[er.EntityNamePart.DEVICE, er.EntityNamePart.ENTITY]
|
||||
)
|
||||
entity_registry.async_update_settings()
|
||||
assert settings.entity_id_parts == (
|
||||
er.EntityNamePart.DEVICE,
|
||||
er.EntityNamePart.ENTITY,
|
||||
)
|
||||
assert len(mock_schedule_save.mock_calls) == 0
|
||||
|
||||
with patch.object(entity_registry, "async_schedule_save") as mock_schedule_save:
|
||||
settings = entity_registry.async_update_settings(entity_id_parts=None)
|
||||
assert settings.entity_id_parts is None
|
||||
assert entity_registry.settings == settings
|
||||
assert len(mock_schedule_save.mock_calls) == 1
|
||||
|
||||
|
||||
def test_is_registered(entity_registry: er.EntityRegistry) -> None:
|
||||
"""Test that is_registered works."""
|
||||
entry = entity_registry.async_get_or_create("light", "hue", "1234")
|
||||
@@ -995,6 +1418,7 @@ async def test_load_bad_data(
|
||||
"unique_id": ["also", "not", "valid"], # Should not load
|
||||
},
|
||||
],
|
||||
"settings": {"entity_id_parts": None},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1422,6 +1846,7 @@ async def test_migration_1_1(hass: HomeAssistant, hass_storage: dict[str, Any])
|
||||
}
|
||||
],
|
||||
"deleted_entities": [],
|
||||
"settings": {"entity_id_parts": None},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1649,6 +2074,7 @@ async def test_migration_1_11(
|
||||
"unique_id": "very_very_unique",
|
||||
}
|
||||
],
|
||||
"settings": {"entity_id_parts": None},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1819,6 +2245,7 @@ async def test_migration_1_18(
|
||||
"unique_id": "very_very_unique",
|
||||
}
|
||||
],
|
||||
"settings": {"entity_id_parts": None},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2021,6 +2448,7 @@ async def test_migration_1_21(
|
||||
},
|
||||
],
|
||||
"deleted_entities": [],
|
||||
"settings": {"entity_id_parts": None},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user