Add setting to customise entity ID generation (#177140)

This commit is contained in:
Artur Pragacz
2026-07-28 22:45:02 +02:00
committed by GitHub
parent 07a2383bdc
commit 110106bf2c
5 changed files with 670 additions and 9 deletions
@@ -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})
+87 -9
View File
@@ -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
+1
View File
@@ -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}
+428
View File
@@ -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},
},
}