Add primary_config_entry attribute to device registry entries (#119959)

Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Robert Resch <robert@resch.dev>
This commit is contained in:
Erik Montnemery 2024-06-26 12:26:24 +02:00 committed by GitHub
parent f55ddfecf4
commit 9bbeb5d608
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
82 changed files with 1001 additions and 105 deletions

View file

@ -90,7 +90,7 @@ async def test_get_or_create_returns_same_entry(
await hass.async_block_till_done()
# Only 2 update events. The third entry did not generate any changes.
assert len(update_events) == 2, update_events
assert len(update_events) == 2
assert update_events[0].data == {
"action": "create",
"device_id": entry.id,
@ -170,9 +170,10 @@ async def test_multiple_config_entries(
assert len(device_registry.devices) == 1
assert entry.id == entry2.id
assert entry.id == entry3.id
assert entry2.config_entries == [config_entry_2.entry_id, config_entry_1.entry_id]
# the 3rd get_or_create was a primary update, so that's now first config entry
assert entry3.config_entries == [config_entry_1.entry_id, config_entry_2.entry_id]
assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id}
assert entry2.primary_config_entry == config_entry_1.entry_id
assert entry3.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id}
assert entry3.primary_config_entry == config_entry_1.entry_id
@pytest.mark.parametrize("load_registries", [False])
@ -202,6 +203,7 @@ async def test_loading_from_storage(
"model": "model",
"name_by_user": "Test Friendly Name",
"name": "name",
"primary_config_entry": mock_config_entry.entry_id,
"serial_number": "serial_no",
"sw_version": "version",
"via_device_id": None,
@ -233,7 +235,7 @@ async def test_loading_from_storage(
)
assert entry == dr.DeviceEntry(
area_id="12345A",
config_entries=[mock_config_entry.entry_id],
config_entries={mock_config_entry.entry_id},
configuration_url="https://example.com/config",
connections={("Zigbee", "01.23.45.67.89")},
disabled_by=dr.DeviceEntryDisabler.USER,
@ -246,11 +248,12 @@ async def test_loading_from_storage(
model="model",
name_by_user="Test Friendly Name",
name="name",
primary_config_entry=mock_config_entry.entry_id,
serial_number="serial_no",
suggested_area=None, # Not stored
sw_version="version",
)
assert isinstance(entry.config_entries, list)
assert isinstance(entry.config_entries, set)
assert isinstance(entry.connections, set)
assert isinstance(entry.identifiers, set)
@ -263,26 +266,27 @@ async def test_loading_from_storage(
model="model",
)
assert entry == dr.DeviceEntry(
config_entries=[mock_config_entry.entry_id],
config_entries={mock_config_entry.entry_id},
connections={("Zigbee", "23.45.67.89.01")},
id="bcdefghijklmn",
identifiers={("serial", "3456ABCDEF12")},
manufacturer="manufacturer",
model="model",
primary_config_entry=mock_config_entry.entry_id,
)
assert entry.id == "bcdefghijklmn"
assert isinstance(entry.config_entries, list)
assert isinstance(entry.config_entries, set)
assert isinstance(entry.connections, set)
assert isinstance(entry.identifiers, set)
@pytest.mark.parametrize("load_registries", [False])
async def test_migration_1_1_to_1_5(
async def test_migration_1_1_to_1_6(
hass: HomeAssistant,
hass_storage: dict[str, Any],
mock_config_entry: MockConfigEntry,
) -> None:
"""Test migration from version 1.1 to 1.5."""
"""Test migration from version 1.1 to 1.6."""
hass_storage[dr.STORAGE_KEY] = {
"version": 1,
"minor_version": 1,
@ -371,6 +375,7 @@ async def test_migration_1_1_to_1_5(
"model": "model",
"name": "name",
"name_by_user": None,
"primary_config_entry": mock_config_entry.entry_id,
"serial_number": None,
"sw_version": "new_version",
"via_device_id": None,
@ -390,6 +395,7 @@ async def test_migration_1_1_to_1_5(
"model": None,
"name_by_user": None,
"name": None,
"primary_config_entry": None,
"serial_number": None,
"sw_version": None,
"via_device_id": None,
@ -409,12 +415,12 @@ async def test_migration_1_1_to_1_5(
@pytest.mark.parametrize("load_registries", [False])
async def test_migration_1_2_to_1_5(
async def test_migration_1_2_to_1_6(
hass: HomeAssistant,
hass_storage: dict[str, Any],
mock_config_entry: MockConfigEntry,
) -> None:
"""Test migration from version 1.2 to 1.5."""
"""Test migration from version 1.2 to 1.6."""
hass_storage[dr.STORAGE_KEY] = {
"version": 1,
"minor_version": 2,
@ -502,6 +508,7 @@ async def test_migration_1_2_to_1_5(
"model": "model",
"name": "name",
"name_by_user": None,
"primary_config_entry": mock_config_entry.entry_id,
"serial_number": None,
"sw_version": "new_version",
"via_device_id": None,
@ -521,6 +528,7 @@ async def test_migration_1_2_to_1_5(
"model": None,
"name_by_user": None,
"name": None,
"primary_config_entry": None,
"serial_number": None,
"sw_version": None,
"via_device_id": None,
@ -532,12 +540,12 @@ async def test_migration_1_2_to_1_5(
@pytest.mark.parametrize("load_registries", [False])
async def test_migration_1_3_to_1_5(
async def test_migration_1_3_to_1_6(
hass: HomeAssistant,
hass_storage: dict[str, Any],
mock_config_entry: MockConfigEntry,
) -> None:
"""Test migration from version 1.3 to 1.5."""
"""Test migration from version 1.3 to 1.6."""
hass_storage[dr.STORAGE_KEY] = {
"version": 1,
"minor_version": 3,
@ -627,6 +635,7 @@ async def test_migration_1_3_to_1_5(
"model": "model",
"name": "name",
"name_by_user": None,
"primary_config_entry": mock_config_entry.entry_id,
"serial_number": None,
"sw_version": "new_version",
"via_device_id": None,
@ -644,8 +653,9 @@ async def test_migration_1_3_to_1_5(
"labels": [],
"manufacturer": None,
"model": None,
"name_by_user": None,
"name": None,
"name_by_user": None,
"primary_config_entry": None,
"serial_number": None,
"sw_version": None,
"via_device_id": None,
@ -657,12 +667,12 @@ async def test_migration_1_3_to_1_5(
@pytest.mark.parametrize("load_registries", [False])
async def test_migration_1_4_to_1_5(
async def test_migration_1_4_to_1_6(
hass: HomeAssistant,
hass_storage: dict[str, Any],
mock_config_entry: MockConfigEntry,
) -> None:
"""Test migration from version 1.4 to 1.5."""
"""Test migration from version 1.4 to 1.6."""
hass_storage[dr.STORAGE_KEY] = {
"version": 1,
"minor_version": 4,
@ -754,6 +764,7 @@ async def test_migration_1_4_to_1_5(
"model": "model",
"name": "name",
"name_by_user": None,
"primary_config_entry": mock_config_entry.entry_id,
"serial_number": None,
"sw_version": "new_version",
"via_device_id": None,
@ -773,6 +784,138 @@ async def test_migration_1_4_to_1_5(
"model": None,
"name_by_user": None,
"name": None,
"primary_config_entry": None,
"serial_number": None,
"sw_version": None,
"via_device_id": None,
},
],
"deleted_devices": [],
},
}
@pytest.mark.parametrize("load_registries", [False])
async def test_migration_1_5_to_1_6(
hass: HomeAssistant,
hass_storage: dict[str, Any],
mock_config_entry: MockConfigEntry,
) -> None:
"""Test migration from version 1.5 to 1.6."""
hass_storage[dr.STORAGE_KEY] = {
"version": 1,
"minor_version": 5,
"key": dr.STORAGE_KEY,
"data": {
"devices": [
{
"area_id": None,
"config_entries": [mock_config_entry.entry_id],
"configuration_url": None,
"connections": [["Zigbee", "01.23.45.67.89"]],
"disabled_by": None,
"entry_type": "service",
"hw_version": "hw_version",
"id": "abcdefghijklm",
"identifiers": [["serial", "123456ABCDEF"]],
"labels": ["blah"],
"manufacturer": "manufacturer",
"model": "model",
"name": "name",
"name_by_user": None,
"serial_number": None,
"sw_version": "new_version",
"via_device_id": None,
},
{
"area_id": None,
"config_entries": [None],
"configuration_url": None,
"connections": [],
"disabled_by": None,
"entry_type": None,
"hw_version": None,
"id": "invalid-entry-type",
"identifiers": [["serial", "mock-id-invalid-entry"]],
"labels": ["blah"],
"manufacturer": None,
"model": None,
"name_by_user": None,
"name": None,
"serial_number": None,
"sw_version": None,
"via_device_id": None,
},
],
"deleted_devices": [],
},
}
await dr.async_load(hass)
registry = dr.async_get(hass)
# Test data was loaded
entry = registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
connections={("Zigbee", "01.23.45.67.89")},
identifiers={("serial", "123456ABCDEF")},
)
assert entry.id == "abcdefghijklm"
# Update to trigger a store
entry = registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
connections={("Zigbee", "01.23.45.67.89")},
identifiers={("serial", "123456ABCDEF")},
sw_version="new_version",
)
assert entry.id == "abcdefghijklm"
# Check we store migrated data
await flush_store(registry._store)
assert hass_storage[dr.STORAGE_KEY] == {
"version": dr.STORAGE_VERSION_MAJOR,
"minor_version": dr.STORAGE_VERSION_MINOR,
"key": dr.STORAGE_KEY,
"data": {
"devices": [
{
"area_id": None,
"config_entries": [mock_config_entry.entry_id],
"configuration_url": None,
"connections": [["Zigbee", "01.23.45.67.89"]],
"disabled_by": None,
"entry_type": "service",
"hw_version": "hw_version",
"id": "abcdefghijklm",
"identifiers": [["serial", "123456ABCDEF"]],
"labels": ["blah"],
"manufacturer": "manufacturer",
"model": "model",
"name": "name",
"name_by_user": None,
"primary_config_entry": mock_config_entry.entry_id,
"serial_number": None,
"sw_version": "new_version",
"via_device_id": None,
},
{
"area_id": None,
"config_entries": [None],
"configuration_url": None,
"connections": [],
"disabled_by": None,
"entry_type": None,
"hw_version": None,
"id": "invalid-entry-type",
"identifiers": [["serial", "mock-id-invalid-entry"]],
"labels": ["blah"],
"manufacturer": None,
"model": None,
"name_by_user": None,
"name": None,
"primary_config_entry": None,
"serial_number": None,
"sw_version": None,
"via_device_id": None,
@ -818,7 +961,7 @@ async def test_removing_config_entries(
assert len(device_registry.devices) == 2
assert entry.id == entry2.id
assert entry.id != entry3.id
assert entry2.config_entries == [config_entry_2.entry_id, config_entry_1.entry_id]
assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id}
device_registry.async_clear_config_entry(config_entry_1.entry_id)
entry = device_registry.async_get_device(identifiers={("bridgeid", "0123")})
@ -826,7 +969,7 @@ async def test_removing_config_entries(
identifiers={("bridgeid", "4567")}
)
assert entry.config_entries == [config_entry_2.entry_id]
assert entry.config_entries == {config_entry_2.entry_id}
assert entry3_removed is None
await hass.async_block_till_done()
@ -839,7 +982,9 @@ async def test_removing_config_entries(
assert update_events[1].data == {
"action": "update",
"device_id": entry.id,
"changes": {"config_entries": [config_entry_1.entry_id]},
"changes": {
"config_entries": {config_entry_1.entry_id},
},
}
assert update_events[2].data == {
"action": "create",
@ -849,7 +994,8 @@ async def test_removing_config_entries(
"action": "update",
"device_id": entry.id,
"changes": {
"config_entries": [config_entry_2.entry_id, config_entry_1.entry_id]
"config_entries": {config_entry_1.entry_id, config_entry_2.entry_id},
"primary_config_entry": config_entry_1.entry_id,
},
}
assert update_events[4].data == {
@ -894,7 +1040,7 @@ async def test_deleted_device_removing_config_entries(
assert len(device_registry.deleted_devices) == 0
assert entry.id == entry2.id
assert entry.id != entry3.id
assert entry2.config_entries == [config_entry_2.entry_id, config_entry_1.entry_id]
assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id}
device_registry.async_remove_device(entry.id)
device_registry.async_remove_device(entry3.id)
@ -911,7 +1057,9 @@ async def test_deleted_device_removing_config_entries(
assert update_events[1].data == {
"action": "update",
"device_id": entry2.id,
"changes": {"config_entries": [config_entry_1.entry_id]},
"changes": {
"config_entries": {config_entry_1.entry_id},
},
}
assert update_events[2].data == {
"action": "create",
@ -1290,7 +1438,7 @@ async def test_update(
assert updated_entry != entry
assert updated_entry == dr.DeviceEntry(
area_id="12345A",
config_entries=[mock_config_entry.entry_id],
config_entries={mock_config_entry.entry_id},
configuration_url="https://example.com/config",
connections={("mac", "65:43:21:fe:dc:ba")},
disabled_by=dr.DeviceEntryDisabler.USER,
@ -1473,6 +1621,8 @@ async def test_update_remove_config_entries(
config_entry_1.add_to_hass(hass)
config_entry_2 = MockConfigEntry()
config_entry_2.add_to_hass(hass)
config_entry_3 = MockConfigEntry()
config_entry_3.add_to_hass(hass)
entry = device_registry.async_get_or_create(
config_entry_id=config_entry_1.entry_id,
@ -1495,20 +1645,34 @@ async def test_update_remove_config_entries(
manufacturer="manufacturer",
model="model",
)
entry4 = device_registry.async_update_device(
entry2.id, add_config_entry_id=config_entry_3.entry_id
)
# Try to add an unknown config entry
with pytest.raises(HomeAssistantError):
device_registry.async_update_device(entry2.id, add_config_entry_id="blabla")
assert len(device_registry.devices) == 2
assert entry.id == entry2.id
assert entry.id == entry2.id == entry4.id
assert entry.id != entry3.id
assert entry2.config_entries == [config_entry_2.entry_id, config_entry_1.entry_id]
assert entry2.config_entries == {config_entry_1.entry_id, config_entry_2.entry_id}
assert entry4.config_entries == {
config_entry_1.entry_id,
config_entry_2.entry_id,
config_entry_3.entry_id,
}
updated_entry = device_registry.async_update_device(
device_registry.async_update_device(
entry2.id, remove_config_entry_id=config_entry_1.entry_id
)
updated_entry = device_registry.async_update_device(
entry2.id, remove_config_entry_id=config_entry_3.entry_id
)
removed_entry = device_registry.async_update_device(
entry3.id, remove_config_entry_id=config_entry_1.entry_id
)
assert updated_entry.config_entries == [config_entry_2.entry_id]
assert updated_entry.config_entries == {config_entry_2.entry_id}
assert removed_entry is None
removed_entry = device_registry.async_get_device(identifiers={("bridgeid", "4567")})
@ -1517,7 +1681,7 @@ async def test_update_remove_config_entries(
await hass.async_block_till_done()
assert len(update_events) == 5
assert len(update_events) == 7
assert update_events[0].data == {
"action": "create",
"device_id": entry.id,
@ -1525,7 +1689,9 @@ async def test_update_remove_config_entries(
assert update_events[1].data == {
"action": "update",
"device_id": entry2.id,
"changes": {"config_entries": [config_entry_1.entry_id]},
"changes": {
"config_entries": {config_entry_1.entry_id},
},
}
assert update_events[2].data == {
"action": "create",
@ -1535,10 +1701,29 @@ async def test_update_remove_config_entries(
"action": "update",
"device_id": entry.id,
"changes": {
"config_entries": [config_entry_2.entry_id, config_entry_1.entry_id]
"config_entries": {config_entry_1.entry_id, config_entry_2.entry_id}
},
}
assert update_events[4].data == {
"action": "update",
"device_id": entry2.id,
"changes": {
"config_entries": {
config_entry_1.entry_id,
config_entry_2.entry_id,
config_entry_3.entry_id,
},
"primary_config_entry": config_entry_1.entry_id,
},
}
assert update_events[5].data == {
"action": "update",
"device_id": entry2.id,
"changes": {
"config_entries": {config_entry_2.entry_id, config_entry_3.entry_id}
},
}
assert update_events[6].data == {
"action": "remove",
"device_id": entry3.id,
}
@ -1768,7 +1953,7 @@ async def test_restore_device(
assert len(device_registry.devices) == 2
assert len(device_registry.deleted_devices) == 0
assert isinstance(entry3.config_entries, list)
assert isinstance(entry3.config_entries, set)
assert isinstance(entry3.connections, set)
assert isinstance(entry3.identifiers, set)
@ -1900,7 +2085,7 @@ async def test_restore_shared_device(
assert len(device_registry.devices) == 1
assert len(device_registry.deleted_devices) == 0
assert isinstance(entry2.config_entries, list)
assert isinstance(entry2.config_entries, set)
assert isinstance(entry2.connections, set)
assert isinstance(entry2.identifiers, set)
@ -1918,7 +2103,7 @@ async def test_restore_shared_device(
assert len(device_registry.devices) == 1
assert len(device_registry.deleted_devices) == 0
assert isinstance(entry3.config_entries, list)
assert isinstance(entry3.config_entries, set)
assert isinstance(entry3.connections, set)
assert isinstance(entry3.identifiers, set)
@ -1934,7 +2119,7 @@ async def test_restore_shared_device(
assert len(device_registry.devices) == 1
assert len(device_registry.deleted_devices) == 0
assert isinstance(entry4.config_entries, list)
assert isinstance(entry4.config_entries, set)
assert isinstance(entry4.connections, set)
assert isinstance(entry4.identifiers, set)
@ -1949,7 +2134,7 @@ async def test_restore_shared_device(
"action": "update",
"device_id": entry.id,
"changes": {
"config_entries": [config_entry_1.entry_id],
"config_entries": {config_entry_1.entry_id},
"identifiers": {("entry_123", "0123")},
},
}
@ -1973,7 +2158,7 @@ async def test_restore_shared_device(
"action": "update",
"device_id": entry.id,
"changes": {
"config_entries": [config_entry_2.entry_id],
"config_entries": {config_entry_2.entry_id},
"identifiers": {("entry_234", "2345")},
},
}
@ -2291,6 +2476,7 @@ async def test_loading_invalid_configuration_url_from_storage(
"model": None,
"name_by_user": None,
"name": None,
"primary_config_entry": "1234",
"serial_number": None,
"sw_version": None,
"via_device_id": None,
@ -2794,3 +2980,75 @@ async def test_device_registry_identifiers_collision(
device3_refetched = device_registry.async_get(device3.id)
device1_refetched = device_registry.async_get(device1.id)
assert not device1_refetched.identifiers.isdisjoint(device3_refetched.identifiers)
async def test_primary_config_entry(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test the primary integration field."""
mock_config_entry_1 = MockConfigEntry(domain="mqtt", title=None)
mock_config_entry_1.add_to_hass(hass)
mock_config_entry_2 = MockConfigEntry(title=None)
mock_config_entry_2.add_to_hass(hass)
mock_config_entry_3 = MockConfigEntry(title=None)
mock_config_entry_3.add_to_hass(hass)
mock_config_entry_4 = MockConfigEntry(domain="matter", title=None)
mock_config_entry_4.add_to_hass(hass)
# Create device without model name etc, config entry will not be marked primary
device = device_registry.async_get_or_create(
config_entry_id=mock_config_entry_1.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
identifiers=set(),
)
assert device.primary_config_entry is None
# Set model, mqtt config entry will be promoted to primary
device = device_registry.async_get_or_create(
config_entry_id=mock_config_entry_1.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
model="model",
)
assert device.primary_config_entry == mock_config_entry_1.entry_id
# New config entry with model will be promoted to primary
device = device_registry.async_get_or_create(
config_entry_id=mock_config_entry_2.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
model="model 2",
)
assert device.primary_config_entry == mock_config_entry_2.entry_id
# New config entry with model will not be promoted to primary
device = device_registry.async_get_or_create(
config_entry_id=mock_config_entry_3.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
model="model 3",
)
assert device.primary_config_entry == mock_config_entry_2.entry_id
# New matter config entry with model will not be promoted to primary
device = device_registry.async_get_or_create(
config_entry_id=mock_config_entry_4.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
model="model 3",
)
assert device.primary_config_entry == mock_config_entry_2.entry_id
# Remove the primary config entry
device = device_registry.async_update_device(
device.id,
remove_config_entry_id=mock_config_entry_2.entry_id,
)
assert device.primary_config_entry is None
# Create new
device = device_registry.async_get_or_create(
config_entry_id=mock_config_entry_1.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
identifiers=set(),
manufacturer="manufacturer",
model="model",
)
assert device.primary_config_entry == mock_config_entry_1.entry_id