diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 98985c639357cc48407d363323ab4d09ef2425b5..c1d07377019e180fbb6936bc6d988c96a689a669 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -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 @@ -1244,12 +1213,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={ @@ -1289,12 +1253,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"]}, @@ -1349,22 +1308,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, @@ -1372,13 +1332,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( diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index a2b449a3356fada6360adcf3a4cced4954222e85..1859a96c0c06834a79645002d9248e5dce559c8e 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -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(), diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index d2347621ebf9bc59a33faab4b8c4bdfa701ae3b8..4fad5d354df6661f4b29d71a5d30498d3efdcb47 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -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}} - - 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"} - } + 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-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,90 +1633,33 @@ 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, } 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 == { - "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 == { + assert update_events[2].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,95 +2615,52 @@ 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, } assert update_events[1].data == { - "action": "update", - "device_id": entry_id, - "changes": { - "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_subentries": { - config_entry_1.entry_id: { - "mock-subentry-id-1-1", - "mock-subentry-id-1-2", - } - }, + "config_subentries": {config_entry_1.entry_id: "mock-subentry-id-1-1"}, }, } - assert update_events[3].data == { + assert update_events[2].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"}, + config_entry_1.entry_id: "mock-subentry-id-1-1", + 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 == { + assert update_events[3].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, } @@ -3369,7 +3108,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")}, }, } @@ -3394,7 +3133,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")}, }, }