mirror of
https://github.com/home-assistant/core.git
synced 2026-08-03 20:24:55 +02:00
Add user-defined unique_id to KNX YAML entities
Allow assigning a manual `unique_id` to any KNX YAML entity via a new optional config key. When the key is added to an existing entity, its auto-generated registry entry is migrated to the user-defined id so history, area and customizations are preserved. Removing the key again cannot be migrated back (documented). The key is added once to the shared entity base schema, so all YAML platforms support it. Duplicate user ids are rejected within a platform; the same id on different platforms is allowed, matching the entity registry's per-domain uniqueness scope. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,12 @@ from typing import TYPE_CHECKING, Any, override
|
||||
from xknx.devices import Device as XknxDevice
|
||||
from xknx.telegram.address import DeviceGroupAddress, GroupAddress
|
||||
|
||||
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, EntityCategory
|
||||
from homeassistant.const import (
|
||||
CONF_ENTITY_CATEGORY,
|
||||
CONF_NAME,
|
||||
CONF_UNIQUE_ID,
|
||||
EntityCategory,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -181,15 +186,21 @@ class KnxYamlEntity(_KnxEntityBase):
|
||||
"""Initialize the YAML entity.
|
||||
|
||||
`unique_id` is the `(new_stable_id, legacy_id)` tuple from
|
||||
`build_yaml_unique_id`; the legacy id is migrated to the stable one.
|
||||
`build_yaml_unique_id`; the legacy id is migrated to the stable one. A
|
||||
user-defined `unique_id` in the config takes precedence, and an existing
|
||||
auto-generated entity is migrated to it so history is preserved. Removing
|
||||
a user-defined `unique_id` again cannot be migrated back.
|
||||
"""
|
||||
new_unique_id, legacy_unique_id = unique_id
|
||||
platform = async_get_current_platform().domain
|
||||
async_migrate_yaml_unique_id(
|
||||
knx_module.hass,
|
||||
async_get_current_platform().domain,
|
||||
legacy_unique_id,
|
||||
new_unique_id,
|
||||
knx_module.hass, platform, legacy_unique_id, new_unique_id
|
||||
)
|
||||
if user_unique_id := entity_config.get(CONF_UNIQUE_ID):
|
||||
async_migrate_yaml_unique_id(
|
||||
knx_module.hass, platform, new_unique_id, user_unique_id
|
||||
)
|
||||
new_unique_id = user_unique_id
|
||||
self._knx_module = knx_module
|
||||
self._attr_name = entity_config[CONF_NAME] or None
|
||||
self._attr_unique_id = new_unique_id
|
||||
|
||||
@@ -40,6 +40,7 @@ from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_PAYLOAD,
|
||||
CONF_TYPE,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
Platform,
|
||||
@@ -188,6 +189,22 @@ class EventSchema:
|
||||
#############
|
||||
|
||||
|
||||
def _unique_id_duplicate_validator(entities: list[dict]) -> list[dict]:
|
||||
"""Validate that user-defined unique_ids are unique within a platform.
|
||||
|
||||
The same unique_id on different platforms is allowed - the entity registry
|
||||
scopes uniqueness per (entity domain, integration).
|
||||
"""
|
||||
seen: set[str] = set()
|
||||
for entity in entities:
|
||||
if (unique_id := entity.get(CONF_UNIQUE_ID)) is None:
|
||||
continue
|
||||
if unique_id in seen:
|
||||
raise vol.Invalid(f"duplicate 'unique_id' not allowed: {unique_id}")
|
||||
seen.add(unique_id)
|
||||
return entities
|
||||
|
||||
|
||||
class KNXPlatformSchema(ABC):
|
||||
"""Voluptuous schema for KNX platform entity configuration."""
|
||||
|
||||
@@ -199,7 +216,7 @@ class KNXPlatformSchema(ABC):
|
||||
"""Return a schema node for the platform."""
|
||||
return {
|
||||
vol.Optional(str(cls.PLATFORM)): vol.All(
|
||||
cv.ensure_list, [cls.ENTITY_SCHEMA]
|
||||
cv.ensure_list, [cls.ENTITY_SCHEMA], _unique_id_duplicate_validator
|
||||
)
|
||||
}
|
||||
|
||||
@@ -213,6 +230,7 @@ def _entity_base_schema(platform: Platform) -> vol.Schema:
|
||||
cv.entity_id, cv.entity_domain(platform)
|
||||
),
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
vol.Optional(CONF_UNIQUE_ID): vol.All(cv.string, vol.Length(min=1)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ from xknx.telegram.address import GroupAddress, GroupAddressType, InternalGroupA
|
||||
|
||||
from homeassistant.components.knx.const import DOMAIN, KNX_ADDRESS
|
||||
from homeassistant.components.knx.entity import build_yaml_unique_id
|
||||
from homeassistant.components.knx.schema import SwitchSchema
|
||||
from homeassistant.const import CONF_NAME, Platform
|
||||
from homeassistant.components.knx.schema import LightSchema, SwitchSchema
|
||||
from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
@@ -117,3 +117,125 @@ async def test_yaml_unique_id_migration_collision(
|
||||
Platform.SWITCH, DOMAIN, legacy_unique_id
|
||||
)
|
||||
assert entity_registry.async_get(legacy_entry.entity_id) is None
|
||||
|
||||
|
||||
async def test_yaml_user_unique_id(
|
||||
hass: HomeAssistant, knx: KNXTestKit, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""A user-defined unique_id is used verbatim instead of the derived one."""
|
||||
await knx.setup_integration(
|
||||
{
|
||||
SwitchSchema.PLATFORM: {
|
||||
CONF_NAME: "test",
|
||||
KNX_ADDRESS: "1/2/3",
|
||||
CONF_UNIQUE_ID: "my_switch",
|
||||
}
|
||||
}
|
||||
)
|
||||
entry = entity_registry.async_get("switch.test")
|
||||
assert entry
|
||||
assert entry.unique_id == "my_switch"
|
||||
|
||||
|
||||
async def test_yaml_user_unique_id_migration(
|
||||
hass: HomeAssistant, knx: KNXTestKit, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Adding a user-defined unique_id migrates the auto-generated entity to it."""
|
||||
auto_unique_id = "1/2/3"
|
||||
knx.mock_config_entry.add_to_hass(hass)
|
||||
entity_registry.async_get_or_create(
|
||||
object_id_base="test",
|
||||
domain=Platform.SWITCH,
|
||||
platform=DOMAIN,
|
||||
unique_id=auto_unique_id,
|
||||
config_entry=knx.mock_config_entry,
|
||||
)
|
||||
await knx.setup_integration(
|
||||
{
|
||||
SwitchSchema.PLATFORM: {
|
||||
CONF_NAME: "test",
|
||||
KNX_ADDRESS: "1/2/3",
|
||||
CONF_UNIQUE_ID: "my_switch",
|
||||
}
|
||||
},
|
||||
add_entry_to_hass=False,
|
||||
)
|
||||
entry = entity_registry.async_get("switch.test")
|
||||
assert entry
|
||||
assert entry.unique_id == "my_switch"
|
||||
assert not entity_registry.async_get_entity_id(
|
||||
Platform.SWITCH, DOMAIN, auto_unique_id
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("two_level_address_format")
|
||||
async def test_yaml_user_unique_id_migration_from_legacy(
|
||||
hass: HomeAssistant, knx: KNXTestKit, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""A user-defined unique_id migrates even from the legacy (2-level) id."""
|
||||
legacy_unique_id = "1/515"
|
||||
knx.mock_config_entry.add_to_hass(hass)
|
||||
entity_registry.async_get_or_create(
|
||||
object_id_base="test",
|
||||
domain=Platform.SWITCH,
|
||||
platform=DOMAIN,
|
||||
unique_id=legacy_unique_id,
|
||||
config_entry=knx.mock_config_entry,
|
||||
)
|
||||
await knx.setup_integration(
|
||||
{
|
||||
SwitchSchema.PLATFORM: {
|
||||
CONF_NAME: "test",
|
||||
KNX_ADDRESS: "1/2/3",
|
||||
CONF_UNIQUE_ID: "my_switch",
|
||||
}
|
||||
},
|
||||
add_entry_to_hass=False,
|
||||
)
|
||||
entry = entity_registry.async_get("switch.test")
|
||||
assert entry
|
||||
assert entry.unique_id == "my_switch"
|
||||
assert not entity_registry.async_get_entity_id(
|
||||
Platform.SWITCH, DOMAIN, legacy_unique_id
|
||||
)
|
||||
assert not entity_registry.async_get_entity_id(Platform.SWITCH, DOMAIN, "1/2/3")
|
||||
|
||||
|
||||
async def test_yaml_user_unique_id_shared_across_platforms(
|
||||
hass: HomeAssistant, knx: KNXTestKit, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""The same user unique_id is allowed on entities of different platforms."""
|
||||
await knx.setup_integration(
|
||||
{
|
||||
SwitchSchema.PLATFORM: {
|
||||
CONF_NAME: "sw",
|
||||
KNX_ADDRESS: "1/2/3",
|
||||
CONF_UNIQUE_ID: "shared",
|
||||
},
|
||||
LightSchema.PLATFORM: {
|
||||
CONF_NAME: "li",
|
||||
KNX_ADDRESS: "1/2/4",
|
||||
CONF_UNIQUE_ID: "shared",
|
||||
},
|
||||
}
|
||||
)
|
||||
switch_entry = entity_registry.async_get("switch.sw")
|
||||
light_entry = entity_registry.async_get("light.li")
|
||||
assert switch_entry and switch_entry.unique_id == "shared"
|
||||
assert light_entry and light_entry.unique_id == "shared"
|
||||
|
||||
|
||||
async def test_yaml_duplicate_user_unique_id_invalid(
|
||||
hass: HomeAssistant, knx: KNXTestKit, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Two entities of the same platform must not share a user unique_id."""
|
||||
await knx.setup_integration(
|
||||
{
|
||||
SwitchSchema.PLATFORM: [
|
||||
{CONF_NAME: "a", KNX_ADDRESS: "1/2/3", CONF_UNIQUE_ID: "dup"},
|
||||
{CONF_NAME: "b", KNX_ADDRESS: "1/2/4", CONF_UNIQUE_ID: "dup"},
|
||||
]
|
||||
}
|
||||
)
|
||||
assert "duplicate 'unique_id' not allowed: dup" in caplog.text
|
||||
assert hass.states.get("switch.a") is None
|
||||
|
||||
Reference in New Issue
Block a user