Allow a device to be connected to no or a single subentry of a config entry

This commit is contained in:
Erik 2024-12-16 15:15:27 +01:00
parent 6b4c27e700
commit 53fd84a5a4
3 changed files with 138 additions and 443 deletions

View File

@ -272,7 +272,7 @@ class DeviceEntry:
area_id: str | None = attr.ib(default=None)
config_entries: set[str] = attr.ib(converter=set, factory=set)
config_subentries: dict[str, set[str | None]] = attr.ib(factory=dict)
config_subentries: dict[str, str | None] = attr.ib(factory=dict)
configuration_url: str | None = attr.ib(default=None)
connections: set[tuple[str, str]] = attr.ib(converter=set, factory=set)
created_at: datetime = attr.ib(factory=utcnow)
@ -312,10 +312,7 @@ class DeviceEntry:
"area_id": self.area_id,
"configuration_url": self.configuration_url,
"config_entries": list(self.config_entries),
"config_subentries": {
config_entry_id: list(subentries)
for config_entry_id, subentries in self.config_subentries.items()
},
"config_subentries": self.config_subentries,
"connections": list(self.connections),
"created_at": self.created_at.timestamp(),
"disabled_by": self.disabled_by,
@ -362,10 +359,7 @@ class DeviceEntry:
# The config_entries list can be removed from the storage
# representation in HA Core 2026.1
"config_entries": list(self.config_entries),
"config_subentries": {
config_entry_id: list(subentries)
for config_entry_id, subentries in self.config_subentries.items()
},
"config_subentries": self.config_subentries,
"configuration_url": self.configuration_url,
"connections": list(self.connections),
"created_at": self.created_at,
@ -395,7 +389,7 @@ class DeletedDeviceEntry:
"""Deleted Device Registry Entry."""
config_entries: set[str] = attr.ib()
config_subentries: dict[str, set[str | None]] = attr.ib()
config_subentries: dict[str, str | None] = attr.ib()
connections: set[tuple[str, str]] = attr.ib()
identifiers: set[tuple[str, str]] = attr.ib()
id: str = attr.ib()
@ -415,7 +409,7 @@ class DeletedDeviceEntry:
return DeviceEntry(
# type ignores: likely https://github.com/python/mypy/issues/8625
config_entries={config_entry_id}, # type: ignore[arg-type]
config_subentries={config_entry_id: {config_subentry_id}},
config_subentries={config_entry_id: config_subentry_id},
connections=self.connections & connections, # type: ignore[arg-type]
created_at=self.created_at,
identifiers=self.identifiers & identifiers, # type: ignore[arg-type]
@ -432,10 +426,7 @@ class DeletedDeviceEntry:
# The config_entries list can be removed from the storage
# representation in HA Core 2026.1
"config_entries": list(self.config_entries),
"config_subentries": {
config_entry_id: list(subentries)
for config_entry_id, subentries in self.config_subentries.items()
},
"config_subentries": self.config_subentries,
"connections": list(self.connections),
"created_at": self.created_at,
"identifiers": list(self.identifiers),
@ -532,12 +523,12 @@ class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]):
# Introduced in 2025.1
for device in old_data["devices"]:
device["config_subentries"] = {
config_entry_id: {None}
config_entry_id: None
for config_entry_id in device["config_entries"]
}
for device in old_data["deleted_devices"]:
device["config_subentries"] = {
config_entry_id: {None}
config_entry_id: None
for config_entry_id in device["config_entries"]
}
@ -913,7 +904,6 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
new_connections: set[tuple[str, str]] | UndefinedType = UNDEFINED,
new_identifiers: set[tuple[str, str]] | UndefinedType = UNDEFINED,
remove_config_entry_id: str | UndefinedType = UNDEFINED,
remove_config_subentry_id: str | None | UndefinedType = UNDEFINED,
serial_number: str | None | UndefinedType = UNDEFINED,
suggested_area: str | None | UndefinedType = UNDEFINED,
sw_version: str | None | UndefinedType = UNDEFINED,
@ -922,7 +912,6 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
"""Update device attributes.
:param add_config_subentry_id: Add the device to a specific subentry of add_config_entry_id
:param remove_config_subentry_id: Remove the device from a specific subentry of remove_config_subentry_id
"""
old = self.devices[device_id]
@ -957,14 +946,6 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
f"Config entry {add_config_entry_id} has no subentry {add_config_subentry_id}"
)
if (
remove_config_subentry_id is not UNDEFINED
and remove_config_entry_id is UNDEFINED
):
raise HomeAssistantError(
"Can't remove config subentry without specifying config entry"
)
if not new_connections and not new_identifiers:
raise HomeAssistantError(
"A device must have at least one of identifiers or connections"
@ -999,6 +980,15 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
# Interpret not specifying a subentry as None (the main entry)
add_config_subentry_id = None
if (
add_config_entry_id in config_subentries
and config_subentries[add_config_entry_id] != add_config_subentry_id
):
raise HomeAssistantError(
f"Device is already linked to config entry {add_config_entry_id} "
"with subentry {config_subentries[add_config_entry_id]}"
)
primary_entry_id = old.primary_config_entry
if (
device_info_type == "primary"
@ -1019,46 +1009,25 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
if add_config_entry_id not in old.config_entries:
config_entries = old.config_entries | {add_config_entry_id}
config_subentries = old.config_subentries | {
add_config_entry_id: {add_config_subentry_id}
}
elif (
add_config_subentry_id not in old.config_subentries[add_config_entry_id]
):
config_subentries = old.config_subentries | {
add_config_entry_id: old.config_subentries[add_config_entry_id]
| {add_config_subentry_id}
add_config_entry_id: add_config_subentry_id
}
if (
remove_config_entry_id is not UNDEFINED
and remove_config_entry_id in config_entries
):
if remove_config_subentry_id is UNDEFINED:
config_subentries = dict(old.config_subentries)
del config_subentries[remove_config_entry_id]
elif (
remove_config_subentry_id
in old.config_subentries[remove_config_entry_id]
):
config_subentries = old.config_subentries | {
remove_config_entry_id: old.config_subentries[
remove_config_entry_id
]
- {remove_config_subentry_id}
}
if not config_subentries[remove_config_entry_id]:
del config_subentries[remove_config_entry_id]
config_subentries = dict(config_subentries)
del config_subentries[remove_config_entry_id]
if remove_config_entry_id not in config_subentries:
if config_entries == {remove_config_entry_id}:
self.async_remove_device(device_id)
return None
if config_entries == {remove_config_entry_id}:
self.async_remove_device(device_id)
return None
if remove_config_entry_id == old.primary_config_entry:
new_values["primary_config_entry"] = None
old_values["primary_config_entry"] = old.primary_config_entry
if remove_config_entry_id == old.primary_config_entry:
new_values["primary_config_entry"] = None
old_values["primary_config_entry"] = old.primary_config_entry
config_entries = config_entries - {remove_config_entry_id}
config_entries = config_entries - {remove_config_entry_id}
if config_entries != old.config_entries:
new_values["config_entries"] = config_entries
@ -1254,12 +1223,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
devices[device["id"]] = DeviceEntry(
area_id=device["area_id"],
config_entries=set(device["config_subentries"]),
config_subentries={
config_entry_id: set(subentries)
for config_entry_id, subentries in device[
"config_subentries"
].items()
},
config_subentries=device["config_subentries"],
configuration_url=device["configuration_url"],
# type ignores (if tuple arg was cast): likely https://github.com/python/mypy/issues/8625
connections={
@ -1299,12 +1263,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
for device in data["deleted_devices"]:
deleted_devices[device["id"]] = DeletedDeviceEntry(
config_entries=set(device["config_entries"]),
config_subentries={
config_entry_id: set(subentries)
for config_entry_id, subentries in device[
"config_subentries"
].items()
},
config_subentries=device["config_subentries"],
connections={tuple(conn) for conn in device["connections"]},
created_at=datetime.fromisoformat(device["created_at"]),
identifiers={tuple(iden) for iden in device["identifiers"]},
@ -1359,22 +1318,23 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
now_time = time.time()
now_time = time.time()
for device in self.devices.get_devices_for_config_entry_id(config_entry_id):
if device.config_subentries[config_entry_id] != config_subentry_id:
continue
self.async_update_device(
device.id,
remove_config_entry_id=config_entry_id,
remove_config_subentry_id=config_subentry_id,
)
for deleted_device in list(self.deleted_devices.values()):
config_entries = deleted_device.config_entries
config_subentries = deleted_device.config_subentries
if (
config_entry_id not in config_subentries
or config_subentry_id not in config_subentries[config_entry_id]
or config_subentries[config_entry_id] != config_subentry_id
):
continue
if config_subentries == {config_entry_id: {config_subentry_id}}:
# We're removing the last config subentry from the last config
# entry, add a time stamp when the deleted device became orphaned
if config_subentries == {config_entry_id: config_subentry_id}:
# We're removing the last config entry, add a time stamp
# when the deleted device became orphaned
self.deleted_devices[deleted_device.id] = attr.evolve(
deleted_device,
orphaned_timestamp=now_time,
@ -1382,13 +1342,9 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
config_subentries={},
)
else:
config_subentries = config_subentries | {
config_entry_id: config_subentries[config_entry_id]
- {config_subentry_id}
}
if not config_subentries[config_entry_id]:
del config_subentries[config_entry_id]
config_entries = config_entries - {config_entry_id}
config_subentries = dict(config_subentries)
del config_subentries[config_entry_id]
config_entries = config_entries - {config_entry_id}
# No need to reindex here since we currently
# do not have a lookup by config entry
self.deleted_devices[deleted_device.id] = attr.evolve(

View File

@ -65,7 +65,7 @@ async def test_list_devices(
{
"area_id": None,
"config_entries": [entry.entry_id],
"config_subentries": {entry.entry_id: [None]},
"config_subentries": {entry.entry_id: None},
"configuration_url": None,
"connections": [["ethernet", "12:34:56:78:90:AB:CD:EF"]],
"created_at": utcnow().timestamp(),
@ -88,7 +88,7 @@ async def test_list_devices(
{
"area_id": None,
"config_entries": [entry.entry_id],
"config_subentries": {entry.entry_id: [None]},
"config_subentries": {entry.entry_id: None},
"configuration_url": None,
"connections": [],
"created_at": utcnow().timestamp(),
@ -123,7 +123,7 @@ async def test_list_devices(
{
"area_id": None,
"config_entries": [entry.entry_id],
"config_subentries": {entry.entry_id: [None]},
"config_subentries": {entry.entry_id: None},
"configuration_url": None,
"connections": [["ethernet", "12:34:56:78:90:AB:CD:EF"]],
"created_at": utcnow().timestamp(),

View File

@ -185,12 +185,6 @@ async def test_multiple_config_subentries(
title="Mock title",
unique_id="test",
),
config_entries.ConfigSubentryData(
data={},
subentry_id="mock-subentry-id-1-2",
title="Mock title",
unique_id="test",
),
)
)
config_entry_1.add_to_hass(hass)
@ -214,7 +208,7 @@ async def test_multiple_config_subentries(
model="model",
)
assert entry.config_entries == {config_entry_1.entry_id}
assert entry.config_subentries == {config_entry_1.entry_id: {None}}
assert entry.config_subentries == {config_entry_1.entry_id: None}
entry_id = entry.id
entry = device_registry.async_get_or_create(
@ -227,35 +221,17 @@ async def test_multiple_config_subentries(
)
assert entry.id == entry_id
assert entry.config_entries == {config_entry_1.entry_id}
assert entry.config_subentries == {config_entry_1.entry_id: {None}}
assert entry.config_subentries == {config_entry_1.entry_id: None}
entry = device_registry.async_get_or_create(
config_entry_id=config_entry_1.entry_id,
config_subentry_id="mock-subentry-id-1-1",
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
identifiers={("bridgeid", "0123")},
manufacturer="manufacturer",
model="model",
)
assert entry.id == entry_id
assert entry.config_entries == {config_entry_1.entry_id}
assert entry.config_subentries == {
config_entry_1.entry_id: {None, "mock-subentry-id-1-1"}
}
entry = device_registry.async_get_or_create(
config_entry_id=config_entry_1.entry_id,
config_subentry_id="mock-subentry-id-1-2",
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
identifiers={("bridgeid", "0123")},
manufacturer="manufacturer",
model="model",
)
assert entry.id == entry_id
assert entry.config_entries == {config_entry_1.entry_id}
assert entry.config_subentries == {
config_entry_1.entry_id: {None, "mock-subentry-id-1-1", "mock-subentry-id-1-2"}
}
with pytest.raises(HomeAssistantError):
device_registry.async_get_or_create(
config_entry_id=config_entry_1.entry_id,
config_subentry_id="mock-subentry-id-1-1",
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
identifiers={("bridgeid", "0123")},
manufacturer="manufacturer",
model="model",
)
entry = device_registry.async_get_or_create(
config_entry_id=config_entry_2.entry_id,
@ -268,8 +244,8 @@ async def test_multiple_config_subentries(
assert entry.id == entry_id
assert entry.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id}
assert entry.config_subentries == {
config_entry_1.entry_id: {None, "mock-subentry-id-1-1", "mock-subentry-id-1-2"},
config_entry_2.entry_id: {"mock-subentry-id-2-1"},
config_entry_1.entry_id: None,
config_entry_2.entry_id: "mock-subentry-id-2-1",
}
@ -291,7 +267,7 @@ async def test_loading_from_storage(
{
"area_id": "12345A",
"config_entries": [mock_config_entry.entry_id],
"config_subentries": {mock_config_entry.entry_id: [None]},
"config_subentries": {mock_config_entry.entry_id: None},
"configuration_url": "https://example.com/config",
"connections": [["Zigbee", "01.23.45.67.89"]],
"created_at": created_at,
@ -316,7 +292,7 @@ async def test_loading_from_storage(
"deleted_devices": [
{
"config_entries": [mock_config_entry.entry_id],
"config_subentries": {mock_config_entry.entry_id: [None]},
"config_subentries": {mock_config_entry.entry_id: None},
"connections": [["Zigbee", "23.45.67.89.01"]],
"created_at": created_at,
"id": "bcdefghijklmn",
@ -335,7 +311,7 @@ async def test_loading_from_storage(
assert registry.deleted_devices["bcdefghijklmn"] == dr.DeletedDeviceEntry(
config_entries={mock_config_entry.entry_id},
config_subentries={mock_config_entry.entry_id: {None}},
config_subentries={mock_config_entry.entry_id: None},
connections={("Zigbee", "23.45.67.89.01")},
created_at=datetime.fromisoformat(created_at),
id="bcdefghijklmn",
@ -354,7 +330,7 @@ async def test_loading_from_storage(
assert entry == dr.DeviceEntry(
area_id="12345A",
config_entries={mock_config_entry.entry_id},
config_subentries={mock_config_entry.entry_id: {None}},
config_subentries={mock_config_entry.entry_id: None},
configuration_url="https://example.com/config",
connections={("Zigbee", "01.23.45.67.89")},
created_at=datetime.fromisoformat(created_at),
@ -389,7 +365,7 @@ async def test_loading_from_storage(
)
assert entry == dr.DeviceEntry(
config_entries={mock_config_entry.entry_id},
config_subentries={mock_config_entry.entry_id: {None}},
config_subentries={mock_config_entry.entry_id: None},
connections={("Zigbee", "23.45.67.89.01")},
created_at=datetime.fromisoformat(created_at),
id="bcdefghijklmn",
@ -489,7 +465,7 @@ async def test_migration_from_1_1(
{
"area_id": None,
"config_entries": [mock_config_entry.entry_id],
"config_subentries": {mock_config_entry.entry_id: [None]},
"config_subentries": {mock_config_entry.entry_id: None},
"configuration_url": None,
"connections": [["Zigbee", "01.23.45.67.89"]],
"created_at": "1970-01-01T00:00:00+00:00",
@ -513,7 +489,7 @@ async def test_migration_from_1_1(
{
"area_id": None,
"config_entries": ["234567"],
"config_subentries": {"234567": [None]},
"config_subentries": {"234567": None},
"configuration_url": None,
"connections": [],
"created_at": "1970-01-01T00:00:00+00:00",
@ -538,7 +514,7 @@ async def test_migration_from_1_1(
"deleted_devices": [
{
"config_entries": ["123456"],
"config_subentries": {"123456": [None]},
"config_subentries": {"123456": None},
"connections": [],
"created_at": "1970-01-01T00:00:00+00:00",
"id": "deletedid",
@ -636,7 +612,7 @@ async def test_migration_from_1_2(
{
"area_id": None,
"config_entries": [mock_config_entry.entry_id],
"config_subentries": {mock_config_entry.entry_id: [None]},
"config_subentries": {mock_config_entry.entry_id: None},
"configuration_url": None,
"connections": [["Zigbee", "01.23.45.67.89"]],
"created_at": "1970-01-01T00:00:00+00:00",
@ -660,7 +636,7 @@ async def test_migration_from_1_2(
{
"area_id": None,
"config_entries": ["234567"],
"config_subentries": {"234567": [None]},
"config_subentries": {"234567": None},
"configuration_url": None,
"connections": [],
"created_at": "1970-01-01T00:00:00+00:00",
@ -772,7 +748,7 @@ async def test_migration_fom_1_3(
{
"area_id": None,
"config_entries": [mock_config_entry.entry_id],
"config_subentries": {mock_config_entry.entry_id: [None]},
"config_subentries": {mock_config_entry.entry_id: None},
"configuration_url": None,
"connections": [["Zigbee", "01.23.45.67.89"]],
"created_at": "1970-01-01T00:00:00+00:00",
@ -796,7 +772,7 @@ async def test_migration_fom_1_3(
{
"area_id": None,
"config_entries": ["234567"],
"config_subentries": {"234567": [None]},
"config_subentries": {"234567": None},
"configuration_url": None,
"connections": [],
"created_at": "1970-01-01T00:00:00+00:00",
@ -910,7 +886,7 @@ async def test_migration_from_1_4(
{
"area_id": None,
"config_entries": [mock_config_entry.entry_id],
"config_subentries": {mock_config_entry.entry_id: [None]},
"config_subentries": {mock_config_entry.entry_id: None},
"configuration_url": None,
"connections": [["Zigbee", "01.23.45.67.89"]],
"created_at": "1970-01-01T00:00:00+00:00",
@ -934,7 +910,7 @@ async def test_migration_from_1_4(
{
"area_id": None,
"config_entries": ["234567"],
"config_subentries": {"234567": [None]},
"config_subentries": {"234567": None},
"configuration_url": None,
"connections": [],
"created_at": "1970-01-01T00:00:00+00:00",
@ -1050,7 +1026,7 @@ async def test_migration_from_1_5(
{
"area_id": None,
"config_entries": [mock_config_entry.entry_id],
"config_subentries": {mock_config_entry.entry_id: [None]},
"config_subentries": {mock_config_entry.entry_id: None},
"configuration_url": None,
"connections": [["Zigbee", "01.23.45.67.89"]],
"created_at": "1970-01-01T00:00:00+00:00",
@ -1074,7 +1050,7 @@ async def test_migration_from_1_5(
{
"area_id": None,
"config_entries": ["234567"],
"config_subentries": {"234567": [None]},
"config_subentries": {"234567": None},
"configuration_url": None,
"connections": [],
"created_at": "1970-01-01T00:00:00+00:00",
@ -1192,7 +1168,7 @@ async def test_migration_from_1_6(
{
"area_id": None,
"config_entries": [mock_config_entry.entry_id],
"config_subentries": {mock_config_entry.entry_id: [None]},
"config_subentries": {mock_config_entry.entry_id: None},
"configuration_url": None,
"connections": [["Zigbee", "01.23.45.67.89"]],
"created_at": "1970-01-01T00:00:00+00:00",
@ -1216,7 +1192,7 @@ async def test_migration_from_1_6(
{
"area_id": None,
"config_entries": ["234567"],
"config_subentries": {"234567": [None]},
"config_subentries": {"234567": None},
"configuration_url": None,
"connections": [],
"created_at": "1970-01-01T00:00:00+00:00",
@ -1336,7 +1312,7 @@ async def test_migration_from_1_7(
{
"area_id": None,
"config_entries": [mock_config_entry.entry_id],
"config_subentries": {mock_config_entry.entry_id: [None]},
"config_subentries": {mock_config_entry.entry_id: None},
"configuration_url": None,
"connections": [["Zigbee", "01.23.45.67.89"]],
"created_at": "1970-01-01T00:00:00+00:00",
@ -1360,7 +1336,7 @@ async def test_migration_from_1_7(
{
"area_id": None,
"config_entries": ["234567"],
"config_subentries": {"234567": [None]},
"config_subentries": {"234567": None},
"configuration_url": None,
"connections": [],
"created_at": "1970-01-01T00:00:00+00:00",
@ -1445,7 +1421,7 @@ async def test_removing_config_entries(
"device_id": entry.id,
"changes": {
"config_entries": {config_entry_1.entry_id},
"config_subentries": {config_entry_1.entry_id: {None}},
"config_subentries": {config_entry_1.entry_id: None},
},
}
assert update_events[2].data == {
@ -1458,8 +1434,8 @@ async def test_removing_config_entries(
"changes": {
"config_entries": {config_entry_1.entry_id, config_entry_2.entry_id},
"config_subentries": {
config_entry_1.entry_id: {None},
config_entry_2.entry_id: {None},
config_entry_1.entry_id: None,
config_entry_2.entry_id: None,
},
"primary_config_entry": config_entry_1.entry_id,
},
@ -1525,7 +1501,7 @@ async def test_deleted_device_removing_config_entries(
"device_id": entry2.id,
"changes": {
"config_entries": {config_entry_1.entry_id},
"config_subentries": {config_entry_1.entry_id: {None}},
"config_subentries": {config_entry_1.entry_id: None},
},
}
assert update_events[2].data == {
@ -1593,12 +1569,6 @@ async def test_removing_config_subentries(
title="Mock title",
unique_id="test",
),
config_entries.ConfigSubentryData(
data={},
subentry_id="mock-subentry-id-1-2",
title="Mock title",
unique_id="test",
),
)
)
config_entry_1.add_to_hass(hass)
@ -1621,22 +1591,6 @@ async def test_removing_config_subentries(
manufacturer="manufacturer",
model="model",
)
entry2 = device_registry.async_get_or_create(
config_entry_id=config_entry_1.entry_id,
config_subentry_id="mock-subentry-id-1-1",
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
identifiers={("bridgeid", "0123")},
manufacturer="manufacturer",
model="model",
)
entry3 = device_registry.async_get_or_create(
config_entry_id=config_entry_1.entry_id,
config_subentry_id="mock-subentry-id-1-2",
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
identifiers={("bridgeid", "0123")},
manufacturer="manufacturer",
model="model",
)
entry4 = device_registry.async_get_or_create(
config_entry_id=config_entry_2.entry_id,
config_subentry_id="mock-subentry-id-2-1",
@ -1647,21 +1601,11 @@ async def test_removing_config_subentries(
)
assert len(device_registry.devices) == 1
assert entry.id == entry2.id
assert entry.id == entry3.id
assert entry.id == entry4.id
assert entry4.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id}
assert entry4.config_subentries == {
config_entry_1.entry_id: {None, "mock-subentry-id-1-1", "mock-subentry-id-1-2"},
config_entry_2.entry_id: {"mock-subentry-id-2-1"},
}
device_registry.async_clear_config_subentry(config_entry_1.entry_id, None)
entry = device_registry.async_get_device(identifiers={("bridgeid", "0123")})
assert entry.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id}
assert entry.config_subentries == {
config_entry_1.entry_id: {"mock-subentry-id-1-1", "mock-subentry-id-1-2"},
config_entry_2.entry_id: {"mock-subentry-id-2-1"},
config_entry_1.entry_id: None,
config_entry_2.entry_id: "mock-subentry-id-2-1",
}
device_registry.async_clear_config_subentry(
@ -1670,17 +1614,15 @@ async def test_removing_config_subentries(
entry = device_registry.async_get_device(identifiers={("bridgeid", "0123")})
assert entry.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id}
assert entry.config_subentries == {
config_entry_1.entry_id: {"mock-subentry-id-1-2"},
config_entry_2.entry_id: {"mock-subentry-id-2-1"},
config_entry_1.entry_id: None,
config_entry_2.entry_id: "mock-subentry-id-2-1",
}
device_registry.async_clear_config_subentry(
config_entry_1.entry_id, "mock-subentry-id-1-2"
)
device_registry.async_clear_config_subentry(config_entry_1.entry_id, None)
entry = device_registry.async_get_device(identifiers={("bridgeid", "0123")})
assert entry.config_entries == {config_entry_2.entry_id}
assert entry.config_subentries == {
config_entry_2.entry_id: {"mock-subentry-id-2-1"}
config_entry_2.entry_id: "mock-subentry-id-2-1",
}
device_registry.async_clear_config_subentry(
@ -1691,7 +1633,7 @@ async def test_removing_config_subentries(
await hass.async_block_till_done()
assert len(update_events) == 8
assert len(update_events) == 4
assert update_events[0].data == {
"action": "create",
"device_id": entry.id,
@ -1700,81 +1642,24 @@ async def test_removing_config_subentries(
"action": "update",
"device_id": entry.id,
"changes": {
"config_subentries": {config_entry_1.entry_id: {None}},
"config_entries": {config_entry_1.entry_id},
"config_subentries": {config_entry_1.entry_id: None},
"identifiers": {("bridgeid", "0123")},
},
}
assert update_events[2].data == {
"action": "update",
"device_id": entry.id,
"changes": {
"config_subentries": {
config_entry_1.entry_id: {None, "mock-subentry-id-1-1"}
},
},
}
assert update_events[3].data == {
"action": "update",
"device_id": entry.id,
"changes": {
"config_entries": {config_entry_1.entry_id},
"config_subentries": {
config_entry_1.entry_id: {
None,
"mock-subentry-id-1-1",
"mock-subentry-id-1-2",
}
},
"identifiers": {("bridgeid", "0123")},
},
}
assert update_events[4].data == {
"action": "update",
"device_id": entry.id,
"changes": {
"config_subentries": {
config_entry_1.entry_id: {
None,
"mock-subentry-id-1-1",
"mock-subentry-id-1-2",
},
config_entry_2.entry_id: {
"mock-subentry-id-2-1",
},
},
},
}
assert update_events[5].data == {
"action": "update",
"device_id": entry.id,
"changes": {
"config_subentries": {
config_entry_1.entry_id: {
"mock-subentry-id-1-1",
"mock-subentry-id-1-2",
},
config_entry_2.entry_id: {
"mock-subentry-id-2-1",
},
},
},
}
assert update_events[6].data == {
"action": "update",
"device_id": entry.id,
"changes": {
"config_entries": {config_entry_1.entry_id, config_entry_2.entry_id},
"config_subentries": {
config_entry_1.entry_id: {
"mock-subentry-id-1-2",
},
config_entry_2.entry_id: {
"mock-subentry-id-2-1",
},
config_entry_1.entry_id: None,
config_entry_2.entry_id: "mock-subentry-id-2-1",
},
"primary_config_entry": config_entry_1.entry_id,
},
}
assert update_events[7].data == {
assert update_events[3].data == {
"action": "remove",
"device_id": entry.id,
}
@ -1785,22 +1670,7 @@ async def test_deleted_device_removing_config_subentries(
) -> None:
"""Make sure we do not get duplicate entries."""
update_events = async_capture_events(hass, dr.EVENT_DEVICE_REGISTRY_UPDATED)
config_entry_1 = MockConfigEntry(
subentries_data=(
config_entries.ConfigSubentryData(
data={},
subentry_id="mock-subentry-id-1-1",
title="Mock title",
unique_id="test",
),
config_entries.ConfigSubentryData(
data={},
subentry_id="mock-subentry-id-1-2",
title="Mock title",
unique_id="test",
),
)
)
config_entry_1 = MockConfigEntry()
config_entry_1.add_to_hass(hass)
config_entry_2 = MockConfigEntry(
subentries_data=(
@ -1821,22 +1691,6 @@ async def test_deleted_device_removing_config_subentries(
manufacturer="manufacturer",
model="model",
)
entry2 = device_registry.async_get_or_create(
config_entry_id=config_entry_1.entry_id,
config_subentry_id="mock-subentry-id-1-1",
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
identifiers={("bridgeid", "0123")},
manufacturer="manufacturer",
model="model",
)
entry3 = device_registry.async_get_or_create(
config_entry_id=config_entry_1.entry_id,
config_subentry_id="mock-subentry-id-1-2",
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
identifiers={("bridgeid", "0123")},
manufacturer="manufacturer",
model="model",
)
entry4 = device_registry.async_get_or_create(
config_entry_id=config_entry_2.entry_id,
config_subentry_id="mock-subentry-id-2-1",
@ -1848,13 +1702,11 @@ async def test_deleted_device_removing_config_subentries(
assert len(device_registry.devices) == 1
assert len(device_registry.deleted_devices) == 0
assert entry.id == entry2.id
assert entry.id == entry3.id
assert entry.id == entry4.id
assert entry4.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id}
assert entry4.config_subentries == {
config_entry_1.entry_id: {None, "mock-subentry-id-1-1", "mock-subentry-id-1-2"},
config_entry_2.entry_id: {"mock-subentry-id-2-1"},
config_entry_1.entry_id: None,
config_entry_2.entry_id: "mock-subentry-id-2-1",
}
device_registry.async_remove_device(entry.id)
@ -1864,85 +1716,47 @@ async def test_deleted_device_removing_config_subentries(
await hass.async_block_till_done()
assert len(update_events) == 5
assert len(update_events) == 3
assert update_events[0].data == {
"action": "create",
"device_id": entry.id,
}
assert update_events[1].data == {
"action": "update",
"device_id": entry.id,
"changes": {
"config_subentries": {config_entry_1.entry_id: {None}},
},
}
assert update_events[2].data == {
"action": "update",
"device_id": entry.id,
"changes": {
"config_subentries": {
config_entry_1.entry_id: {None, "mock-subentry-id-1-1"}
},
},
}
assert update_events[3].data == {
"action": "update",
"device_id": entry.id,
"changes": {
"config_entries": {config_entry_1.entry_id},
"config_subentries": {
config_entry_1.entry_id: {
None,
"mock-subentry-id-1-1",
"mock-subentry-id-1-2",
}
},
"config_subentries": {config_entry_1.entry_id: None},
"identifiers": {("bridgeid", "0123")},
},
}
assert update_events[4].data == {
assert update_events[2].data == {
"action": "remove",
"device_id": entry.id,
}
device_registry.async_clear_config_subentry(config_entry_1.entry_id, None)
entry = device_registry.deleted_devices.get_entry({("bridgeid", "0123")}, None)
assert entry.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id}
assert entry.config_subentries == {
config_entry_1.entry_id: {"mock-subentry-id-1-1", "mock-subentry-id-1-2"},
config_entry_2.entry_id: {"mock-subentry-id-2-1"},
config_entry_1.entry_id: None,
config_entry_2.entry_id: "mock-subentry-id-2-1",
}
assert entry.orphaned_timestamp is None
device_registry.async_clear_config_subentry(
config_entry_1.entry_id, "mock-subentry-id-1-1"
)
device_registry.async_clear_config_subentry(config_entry_1.entry_id, None)
entry = device_registry.deleted_devices.get_entry({("bridgeid", "0123")}, None)
assert entry.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id}
assert entry.config_entries == {config_entry_2.entry_id}
assert entry.config_subentries == {
config_entry_1.entry_id: {"mock-subentry-id-1-2"},
config_entry_2.entry_id: {"mock-subentry-id-2-1"},
config_entry_2.entry_id: "mock-subentry-id-2-1",
}
assert entry.orphaned_timestamp is None
# Remove the same subentry again
device_registry.async_clear_config_subentry(
config_entry_1.entry_id, "mock-subentry-id-1-1"
)
device_registry.async_clear_config_subentry(config_entry_1.entry_id, None)
assert (
device_registry.deleted_devices.get_entry({("bridgeid", "0123")}, None) is entry
)
device_registry.async_clear_config_subentry(
config_entry_1.entry_id, "mock-subentry-id-1-2"
)
entry = device_registry.deleted_devices.get_entry({("bridgeid", "0123")}, None)
assert entry.config_entries == {config_entry_2.entry_id}
assert entry.config_subentries == {
config_entry_2.entry_id: {"mock-subentry-id-2-1"}
}
assert entry.orphaned_timestamp is None
device_registry.async_clear_config_subentry(
config_entry_2.entry_id, "mock-subentry-id-2-1"
)
@ -1953,7 +1767,7 @@ async def test_deleted_device_removing_config_subentries(
# No event when a deleted device is purged
await hass.async_block_till_done()
assert len(update_events) == 5
assert len(update_events) == 3
# Re-add, expect to keep the device id
restored_entry = device_registry.async_get_or_create(
@ -2375,7 +2189,7 @@ async def test_update(
assert updated_entry == dr.DeviceEntry(
area_id="12345A",
config_entries={mock_config_entry.entry_id},
config_subentries={mock_config_entry.entry_id: {None}},
config_subentries={mock_config_entry.entry_id: None},
configuration_url="https://example.com/config",
connections={("mac", "65:43:21:fe:dc:ba")},
created_at=created_at,
@ -2632,7 +2446,7 @@ async def test_update_remove_config_entries(
"device_id": entry2.id,
"changes": {
"config_entries": {config_entry_1.entry_id},
"config_subentries": {config_entry_1.entry_id: {None}},
"config_subentries": {config_entry_1.entry_id: None},
},
}
assert update_events[2].data == {
@ -2645,8 +2459,8 @@ async def test_update_remove_config_entries(
"changes": {
"config_entries": {config_entry_1.entry_id, config_entry_2.entry_id},
"config_subentries": {
config_entry_1.entry_id: {None},
config_entry_2.entry_id: {None},
config_entry_1.entry_id: None,
config_entry_2.entry_id: None,
},
},
}
@ -2660,9 +2474,9 @@ async def test_update_remove_config_entries(
config_entry_3.entry_id,
},
"config_subentries": {
config_entry_1.entry_id: {None},
config_entry_2.entry_id: {None},
config_entry_3.entry_id: {None},
config_entry_1.entry_id: None,
config_entry_2.entry_id: None,
config_entry_3.entry_id: None,
},
"primary_config_entry": config_entry_1.entry_id,
},
@ -2673,8 +2487,8 @@ async def test_update_remove_config_entries(
"changes": {
"config_entries": {config_entry_2.entry_id, config_entry_3.entry_id},
"config_subentries": {
config_entry_2.entry_id: {None},
config_entry_3.entry_id: {None},
config_entry_2.entry_id: None,
config_entry_3.entry_id: None,
},
},
}
@ -2697,12 +2511,6 @@ async def test_update_remove_config_subentries(
title="Mock title",
unique_id="test",
),
config_entries.ConfigSubentryData(
data={},
subentry_id="mock-subentry-id-1-2",
title="Mock title",
unique_id="test",
),
)
)
config_entry_1.add_to_hass(hass)
@ -2730,26 +2538,14 @@ async def test_update_remove_config_subentries(
)
entry_id = entry.id
assert entry.config_entries == {config_entry_1.entry_id}
assert entry.config_subentries == {
config_entry_1.entry_id: {"mock-subentry-id-1-1"}
}
entry = device_registry.async_update_device(
entry_id,
add_config_entry_id=config_entry_1.entry_id,
add_config_subentry_id="mock-subentry-id-1-2",
)
assert entry.config_entries == {config_entry_1.entry_id}
assert entry.config_subentries == {
config_entry_1.entry_id: {"mock-subentry-id-1-1", "mock-subentry-id-1-2"}
}
assert entry.config_subentries == {config_entry_1.entry_id: "mock-subentry-id-1-1"}
# Try adding the same subentry again
assert (
device_registry.async_update_device(
entry_id,
add_config_entry_id=config_entry_1.entry_id,
add_config_subentry_id="mock-subentry-id-1-2",
add_config_subentry_id="mock-subentry-id-1-1",
)
is entry
)
@ -2761,8 +2557,8 @@ async def test_update_remove_config_subentries(
)
assert entry.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id}
assert entry.config_subentries == {
config_entry_1.entry_id: {"mock-subentry-id-1-1", "mock-subentry-id-1-2"},
config_entry_2.entry_id: {"mock-subentry-id-2-1"},
config_entry_1.entry_id: "mock-subentry-id-1-1",
config_entry_2.entry_id: "mock-subentry-id-2-1",
}
entry = device_registry.async_update_device(
@ -2776,9 +2572,9 @@ async def test_update_remove_config_subentries(
config_entry_3.entry_id,
}
assert entry.config_subentries == {
config_entry_1.entry_id: {"mock-subentry-id-1-1", "mock-subentry-id-1-2"},
config_entry_2.entry_id: {"mock-subentry-id-2-1"},
config_entry_3.entry_id: {None},
config_entry_1.entry_id: "mock-subentry-id-1-1",
config_entry_2.entry_id: "mock-subentry-id-2-1",
config_entry_3.entry_id: None,
}
# Try to add a subentry without specifying entry
@ -2799,31 +2595,19 @@ async def test_update_remove_config_subentries(
add_config_subentry_id="blabla",
)
# Try to remove a subentry without specifying entry
with pytest.raises(
HomeAssistantError,
match="Can't remove config subentry without specifying config entry",
):
device_registry.async_update_device(
entry_id, remove_config_subentry_id="blabla"
)
assert len(device_registry.devices) == 1
entry = device_registry.async_update_device(
entry_id,
remove_config_entry_id=config_entry_1.entry_id,
remove_config_subentry_id="mock-subentry-id-1-1",
)
assert entry.config_entries == {
config_entry_1.entry_id,
config_entry_2.entry_id,
config_entry_3.entry_id,
}
assert entry.config_subentries == {
config_entry_1.entry_id: {"mock-subentry-id-1-2"},
config_entry_2.entry_id: {"mock-subentry-id-2-1"},
config_entry_3.entry_id: {None},
config_entry_2.entry_id: "mock-subentry-id-2-1",
config_entry_3.entry_id: None,
}
# Try removing the same subentry again
@ -2831,42 +2615,28 @@ async def test_update_remove_config_subentries(
device_registry.async_update_device(
entry_id,
remove_config_entry_id=config_entry_1.entry_id,
remove_config_subentry_id="mock-subentry-id-1-1",
)
is entry
)
entry = device_registry.async_update_device(
entry_id,
remove_config_entry_id=config_entry_1.entry_id,
remove_config_subentry_id="mock-subentry-id-1-2",
)
assert entry.config_entries == {config_entry_2.entry_id, config_entry_3.entry_id}
assert entry.config_subentries == {
config_entry_2.entry_id: {"mock-subentry-id-2-1"},
config_entry_3.entry_id: {None},
}
entry = device_registry.async_update_device(
entry_id,
remove_config_entry_id=config_entry_2.entry_id,
remove_config_subentry_id="mock-subentry-id-2-1",
)
assert entry.config_entries == {config_entry_3.entry_id}
assert entry.config_subentries == {
config_entry_3.entry_id: {None},
config_entry_3.entry_id: None,
}
entry = device_registry.async_update_device(
entry_id,
remove_config_entry_id=config_entry_3.entry_id,
remove_config_subentry_id=None,
)
assert entry is None
await hass.async_block_till_done()
assert len(update_events) == 8
assert len(update_events) == 6
assert update_events[0].data == {
"action": "create",
"device_id": entry_id,
@ -2875,51 +2645,22 @@ async def test_update_remove_config_subentries(
"action": "update",
"device_id": entry_id,
"changes": {
"config_subentries": {config_entry_1.entry_id: {"mock-subentry-id-1-1"}},
"config_entries": {config_entry_1.entry_id},
"config_subentries": {config_entry_1.entry_id: "mock-subentry-id-1-1"},
},
}
assert update_events[2].data == {
"action": "update",
"device_id": entry_id,
"changes": {
"config_entries": {config_entry_1.entry_id},
"config_entries": {config_entry_1.entry_id, config_entry_2.entry_id},
"config_subentries": {
config_entry_1.entry_id: {
"mock-subentry-id-1-1",
"mock-subentry-id-1-2",
}
config_entry_1.entry_id: "mock-subentry-id-1-1",
config_entry_2.entry_id: "mock-subentry-id-2-1",
},
},
}
assert update_events[3].data == {
"action": "update",
"device_id": entry_id,
"changes": {
"config_entries": {config_entry_1.entry_id, config_entry_2.entry_id},
"config_subentries": {
config_entry_1.entry_id: {
"mock-subentry-id-1-1",
"mock-subentry-id-1-2",
},
config_entry_2.entry_id: {"mock-subentry-id-2-1"},
},
},
}
assert update_events[4].data == {
"action": "update",
"device_id": entry_id,
"changes": {
"config_subentries": {
config_entry_1.entry_id: {
"mock-subentry-id-1-1",
"mock-subentry-id-1-2",
},
config_entry_2.entry_id: {"mock-subentry-id-2-1"},
config_entry_3.entry_id: {None},
},
},
}
assert update_events[5].data == {
"action": "update",
"device_id": entry_id,
"changes": {
@ -2929,27 +2670,25 @@ async def test_update_remove_config_subentries(
config_entry_3.entry_id,
},
"config_subentries": {
config_entry_1.entry_id: {
"mock-subentry-id-1-2",
},
config_entry_2.entry_id: {"mock-subentry-id-2-1"},
config_entry_3.entry_id: {None},
config_entry_1.entry_id: "mock-subentry-id-1-1",
config_entry_2.entry_id: "mock-subentry-id-2-1",
config_entry_3.entry_id: None,
},
"primary_config_entry": config_entry_1.entry_id,
},
}
assert update_events[6].data == {
assert update_events[4].data == {
"action": "update",
"device_id": entry_id,
"changes": {
"config_entries": {config_entry_2.entry_id, config_entry_3.entry_id},
"config_subentries": {
config_entry_2.entry_id: {"mock-subentry-id-2-1"},
config_entry_3.entry_id: {None},
config_entry_2.entry_id: "mock-subentry-id-2-1",
config_entry_3.entry_id: None,
},
},
}
assert update_events[7].data == {
assert update_events[5].data == {
"action": "remove",
"device_id": entry_id,
}
@ -3361,7 +3100,7 @@ async def test_restore_shared_device(
"device_id": entry.id,
"changes": {
"config_entries": {config_entry_1.entry_id},
"config_subentries": {config_entry_1.entry_id: {None}},
"config_subentries": {config_entry_1.entry_id: None},
"identifiers": {("entry_123", "0123")},
},
}
@ -3386,7 +3125,7 @@ async def test_restore_shared_device(
"device_id": entry.id,
"changes": {
"config_entries": {config_entry_2.entry_id},
"config_subentries": {config_entry_2.entry_id: {None}},
"config_subentries": {config_entry_2.entry_id: None},
"identifiers": {("entry_234", "2345")},
},
}