From 23d051d694bf7f88535cd22348711e641940c0bf Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 20 Jul 2026 17:40:03 +0200 Subject: [PATCH 1/8] Migrate template helper to new device cleanup helper (#176900) --- homeassistant/components/template/__init__.py | 13 +-- homeassistant/helpers/helper_integration.py | 17 ++- tests/components/template/test_init.py | 110 ++++++++++++++++++ tests/helpers/test_helper_integration.py | 58 ++++++++- 4 files changed, 181 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index c43881381da4bc..40a4cb80dd2cfd 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -17,9 +17,6 @@ from homeassistant.core import Event, HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryError, HomeAssistantError from homeassistant.helpers import discovery -from homeassistant.helpers.device import ( - async_remove_stale_devices_links_keep_current_device, -) from homeassistant.helpers.helper_integration import async_remove_helper_devices from homeassistant.helpers.reload import async_reload_integration_platforms from homeassistant.helpers.service import async_register_admin_service @@ -94,11 +91,13 @@ async def _reload_config(call: Event | ServiceCall) -> None: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - # This can be removed in HA Core 2026.7 - async_remove_stale_devices_links_keep_current_device( + # Clean up devices this helper created for previously selected source devices; + # this can be removed in HA Core 2027.8. + async_remove_helper_devices( hass, - entry.entry_id, - entry.options.get(CONF_DEVICE_ID), + helper_config_entry_id=entry.entry_id, + source_device_id=entry.options.get(CONF_DEVICE_ID), + sweep_helper_devices=True, ) for key in (CONF_MAX, CONF_MIN, CONF_STEP): diff --git a/homeassistant/helpers/helper_integration.py b/homeassistant/helpers/helper_integration.py index 10a247c3d37f6e..62f589fadff2c2 100644 --- a/homeassistant/helpers/helper_integration.py +++ b/homeassistant/helpers/helper_integration.py @@ -135,7 +135,7 @@ def async_remove_helper_devices( hass: HomeAssistant, *, helper_config_entry_id: str, - source_device_id: str, + source_device_id: str | None, sweep_helper_devices: bool = False, keep_device_ids: Collection[str] = (), ) -> None: @@ -151,7 +151,9 @@ def async_remove_helper_devices( :param helper_config_entry_id: The config entry id of the helper being migrated. :param source_device_id: The device the helper should link to. May be the pre-migration composite id (as a helper that stored the device id before it was split passes) or a - concrete source device. + concrete source device. May also be None when no source device is selected: in sweep + mode the helper's devices are then removed and its entities left without a device; + in targeted mode this is a no-op. :param sweep_helper_devices: By default only the helper's single duplicate of source_device_id (a split or fork) is removed. When True, every device the helper owns except source_device_id and keep_device_ids is removed instead - @@ -165,10 +167,15 @@ def async_remove_helper_devices( device_registry = dr.async_get(hass) entity_registry = er.async_get(hass) - source_device = device_registry.async_get(source_device_id) + source_device = ( + device_registry.async_get(source_device_id) + if source_device_id is not None + else None + ) if source_device is None: - # The source device is gone. In sweep mode the helper's devices are still removed, - # leaving its entities without a device; targeted mode has no duplicate to match. + # No source device (gone, or none selected). In sweep mode the helper's devices are + # still removed, leaving its entities without a device; targeted mode has no duplicate + # to match. if sweep_helper_devices: _sweep_helper_devices( device_registry, diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py index edd85ec0dad092..f1c2233ba4ced0 100644 --- a/tests/components/template/test_init.py +++ b/tests/components/template/test_init.py @@ -490,6 +490,116 @@ def check_template_entities( ) +async def test_setup_removes_stale_helper_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Setup removes stale devices the helper owns and relinks their entities. + + A device is owned by a single config entry now, so a template entry that created a + device for a previously selected source device has that leftover removed on setup and + its entities relinked to the current source device. + """ + source_entry = MockConfigEntry() + source_entry.add_to_hass(hass) + source_device = device_registry.async_get_or_create( + config_entry_id=source_entry.entry_id, + identifiers={("test", "source")}, + ) + + template_config_entry = MockConfigEntry( + domain=DOMAIN, + options={ + "name": "My template", + "state": "{{10}}", + "template_type": "sensor", + "device_id": source_device.id, + }, + title="Template", + ) + template_config_entry.add_to_hass(hass) + + # A leftover device owned by the template config entry, with a helper entity on it + stale_device = device_registry.async_get_or_create( + config_entry_id=template_config_entry.entry_id, + identifiers={("test", "stale")}, + ) + stale_entity = entity_registry.async_get_or_create( + "sensor", + DOMAIN, + "stale", + config_entry=template_config_entry, + device_id=stale_device.id, + ) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + # The stale device is removed, its entity relinked to the source device, and the + # template config entry is left owning no devices. + assert device_registry.async_get(stale_device.id) is None + assert ( + entity_registry.async_get(stale_entity.entity_id).device_id == source_device.id + ) + assert ( + dr.async_entries_for_config_entry( + device_registry, template_config_entry.entry_id + ) + == [] + ) + + +async def test_setup_removes_stale_helper_device_without_source_device( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Setup sweeps leftover helper devices even when no source device is selected. + + After the user removes the device option, a leftover device the entry created for a + previously selected source device is still removed on setup and its entity left without + a device. + """ + template_config_entry = MockConfigEntry( + domain=DOMAIN, + options={ + "name": "My template", + "state": "{{10}}", + "template_type": "sensor", + }, + title="Template", + ) + template_config_entry.add_to_hass(hass) + + # A leftover device owned by the template config entry, with a helper entity on it + stale_device = device_registry.async_get_or_create( + config_entry_id=template_config_entry.entry_id, + identifiers={("test", "stale")}, + ) + stale_entity = entity_registry.async_get_or_create( + "sensor", + DOMAIN, + "stale", + config_entry=template_config_entry, + device_id=stale_device.id, + ) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + # The stale device is removed, its entity left without a device, and the template config + # entry is left owning no devices. + assert device_registry.async_get(stale_device.id) is None + assert entity_registry.async_get(stale_entity.entity_id).device_id is None + assert ( + dr.async_entries_for_config_entry( + device_registry, template_config_entry.entry_id + ) + == [] + ) + + async def test_fail_non_numerical_number_settings( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: diff --git a/tests/helpers/test_helper_integration.py b/tests/helpers/test_helper_integration.py index c05950005b2760..0e899de2c52295 100644 --- a/tests/helpers/test_helper_integration.py +++ b/tests/helpers/test_helper_integration.py @@ -833,15 +833,24 @@ async def test_async_remove_helper_devices_sweep( ) -async def test_async_remove_helper_devices_sweep_source_device_gone( +@pytest.mark.parametrize( + "source_device_id", + [ + pytest.param("nonexistent_device_id", id="missing_device"), + pytest.param(None, id="no_device_selected"), + ], +) +async def test_async_remove_helper_devices_sweep_no_source( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + source_device_id: str | None, ) -> None: - """Sweep mode removes the helper's devices even when the source device is gone. + """Sweep mode removes the helper's devices when there is no source device. - With no source device to relink to, the helper's entities are left without a device and - its devices are still removed. + Whether the source device id points to a removed device or is None because no device is + selected, the helper's entities are left without a device and its devices are still + removed. """ helper_config_entry = MockConfigEntry(domain=HELPER_DOMAIN) helper_config_entry.add_to_hass(hass) @@ -861,7 +870,7 @@ async def test_async_remove_helper_devices_sweep_source_device_gone( async_remove_helper_devices( hass, helper_config_entry_id=helper_config_entry.entry_id, - source_device_id="nonexistent_device_id", + source_device_id=source_device_id, sweep_helper_devices=True, ) @@ -870,6 +879,45 @@ async def test_async_remove_helper_devices_sweep_source_device_gone( assert entity_registry.async_get(entity_on_fork.entity_id).device_id is None +async def test_async_remove_helper_devices_none_source_targeted_noop( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Targeted mode is a no-op when no source device is selected. + + Without sweep_helper_devices there is no duplicate to match against a missing source, so + the helper's device and its entity's device link are left untouched. + """ + helper_config_entry = MockConfigEntry(domain=HELPER_DOMAIN) + helper_config_entry.add_to_hass(hass) + + helper_device = device_registry.async_get_or_create( + config_entry_id=helper_config_entry.entry_id, + identifiers={(HELPER_DOMAIN, "device")}, + ) + entity_on_device = entity_registry.async_get_or_create( + "sensor", + HELPER_DOMAIN, + "1", + config_entry=helper_config_entry, + device_id=helper_device.id, + ) + + async_remove_helper_devices( + hass, + helper_config_entry_id=helper_config_entry.entry_id, + source_device_id=None, + ) + + # Nothing is removed or relinked + assert device_registry.async_get(helper_device.id) is not None + assert ( + entity_registry.async_get(entity_on_device.entity_id).device_id + == helper_device.id + ) + + async def test_async_remove_helper_config_entry_from_source_device_deprecated( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, From 70a78c388d3eea354930a5df1762656708f68839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Mon, 20 Jul 2026 17:41:48 +0100 Subject: [PATCH 2/8] Add motion device class to reolink AI detection binary sensors (#176802) --- .../components/reolink/binary_sensor.py | 7 +++++++ .../reolink/snapshots/test_binary_sensor.ambr | 18 ++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/reolink/binary_sensor.py b/homeassistant/components/reolink/binary_sensor.py index 5e125d4227cd9d..37c6756f485ab7 100644 --- a/homeassistant/components/reolink/binary_sensor.py +++ b/homeassistant/components/reolink/binary_sensor.py @@ -79,6 +79,7 @@ class ReolinkIndexBinarySensorEntityDescription( key=FACE_DETECTION_TYPE, cmd_id=33, translation_key="face", + device_class=BinarySensorDeviceClass.MOTION, lens_entity=True, value=lambda api, ch: api.ai_detected(ch, FACE_DETECTION_TYPE), supported=lambda api, ch: api.ai_supported(ch, FACE_DETECTION_TYPE), @@ -87,6 +88,7 @@ class ReolinkIndexBinarySensorEntityDescription( key=PERSON_DETECTION_TYPE, cmd_id=[33, 600, 696], translation_key="person", + device_class=BinarySensorDeviceClass.MOTION, lens_entity=True, value=lambda api, ch: api.ai_detected(ch, PERSON_DETECTION_TYPE), supported=lambda api, ch: api.ai_supported(ch, PERSON_DETECTION_TYPE), @@ -95,6 +97,7 @@ class ReolinkIndexBinarySensorEntityDescription( key=VEHICLE_DETECTION_TYPE, cmd_id=[33, 600, 696], translation_key="vehicle", + device_class=BinarySensorDeviceClass.MOTION, lens_entity=True, value=lambda api, ch: api.ai_detected(ch, VEHICLE_DETECTION_TYPE), supported=lambda api, ch: api.ai_supported(ch, VEHICLE_DETECTION_TYPE), @@ -103,6 +106,7 @@ class ReolinkIndexBinarySensorEntityDescription( key="non-motor_vehicle", cmd_id=[600, 696], translation_key="non-motor_vehicle", + device_class=BinarySensorDeviceClass.MOTION, lens_entity=True, value=lambda api, ch: api.ai_detected(ch, "non-motor vehicle"), supported=lambda api, ch: api.supported(ch, "ai_non-motor vehicle"), @@ -111,6 +115,7 @@ class ReolinkIndexBinarySensorEntityDescription( key=PET_DETECTION_TYPE, cmd_id=[33, 600, 696], translation_key="pet", + device_class=BinarySensorDeviceClass.MOTION, lens_entity=True, value=lambda api, ch: api.ai_detected(ch, PET_DETECTION_TYPE), supported=lambda api, ch: ( @@ -122,6 +127,7 @@ class ReolinkIndexBinarySensorEntityDescription( key=PET_DETECTION_TYPE, cmd_id=[33, 600, 696], translation_key="animal", + device_class=BinarySensorDeviceClass.MOTION, lens_entity=True, value=lambda api, ch: api.ai_detected(ch, PET_DETECTION_TYPE), supported=lambda api, ch: api.supported(ch, "ai_animal"), @@ -130,6 +136,7 @@ class ReolinkIndexBinarySensorEntityDescription( key=PACKAGE_DETECTION_TYPE, cmd_id=[33, 600, 696], translation_key="package", + device_class=BinarySensorDeviceClass.MOTION, lens_entity=True, value=lambda api, ch: api.ai_detected(ch, PACKAGE_DETECTION_TYPE), supported=lambda api, ch: api.ai_supported(ch, PACKAGE_DETECTION_TYPE), diff --git a/tests/components/reolink/snapshots/test_binary_sensor.ambr b/tests/components/reolink/snapshots/test_binary_sensor.ambr index 07aab58eb3a06d..d031f47192769f 100644 --- a/tests/components/reolink/snapshots/test_binary_sensor.ambr +++ b/tests/components/reolink/snapshots/test_binary_sensor.ambr @@ -24,7 +24,7 @@ 'object_id_base': 'Animal', 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Animal', 'platform': 'reolink', @@ -39,6 +39,7 @@ # name: test_all_entities[binary_sensor.test_reolink_cam_animal-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + : 'motion', : 'test_reolink_cam Animal', }), 'context': , @@ -124,7 +125,7 @@ 'object_id_base': 'Bicycle', 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Bicycle', 'platform': 'reolink', @@ -139,6 +140,7 @@ # name: test_all_entities[binary_sensor.test_reolink_cam_bicycle-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + : 'motion', : 'test_reolink_cam Bicycle', }), 'context': , @@ -224,7 +226,7 @@ 'object_id_base': 'Face', 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Face', 'platform': 'reolink', @@ -239,6 +241,7 @@ # name: test_all_entities[binary_sensor.test_reolink_cam_face-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + : 'motion', : 'test_reolink_cam Face', }), 'context': , @@ -525,7 +528,7 @@ 'object_id_base': 'Package', 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Package', 'platform': 'reolink', @@ -540,6 +543,7 @@ # name: test_all_entities[binary_sensor.test_reolink_cam_package-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + : 'motion', : 'test_reolink_cam Package', }), 'context': , @@ -575,7 +579,7 @@ 'object_id_base': 'Person', 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Person', 'platform': 'reolink', @@ -590,6 +594,7 @@ # name: test_all_entities[binary_sensor.test_reolink_cam_person-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + : 'motion', : 'test_reolink_cam Person', }), 'context': , @@ -675,7 +680,7 @@ 'object_id_base': 'Vehicle', 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Vehicle', 'platform': 'reolink', @@ -690,6 +695,7 @@ # name: test_all_entities[binary_sensor.test_reolink_cam_vehicle-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + : 'motion', : 'test_reolink_cam Vehicle', }), 'context': , From 16d7c5663739e3671b4564045a2ce1404b1befc9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 20 Jul 2026 19:36:36 +0200 Subject: [PATCH 3/8] Simplify async_remove_helper_devices (#176919) --- homeassistant/helpers/helper_integration.py | 56 ++----------- tests/helpers/test_helper_integration.py | 89 ++------------------- 2 files changed, 14 insertions(+), 131 deletions(-) diff --git a/homeassistant/helpers/helper_integration.py b/homeassistant/helpers/helper_integration.py index 62f589fadff2c2..222c5d749a1d59 100644 --- a/homeassistant/helpers/helper_integration.py +++ b/homeassistant/helpers/helper_integration.py @@ -146,14 +146,14 @@ def async_remove_helper_devices( now owns a split of it (linked by the pre-migration composite_device_id); a helper that declared the source device's identifiers or connections in its device_info afterwards now owns a fork (a separate device that copied that identity). This removes the helper's - duplicate device(s) and relinks its entities to the source device. + duplicate device(s) and relinks its entities to the source device, or detaches them when + the source has no concrete device to hold the link. :param helper_config_entry_id: The config entry id of the helper being migrated. - :param source_device_id: The device the helper should link to. May be the pre-migration - composite id (as a helper that stored the device id before it was split passes) or a - concrete source device. May also be None when no source device is selected: in sweep - mode the helper's devices are then removed and its entities left without a device; - in targeted mode this is a no-op. + :param source_device_id: The device the helper should link its entities to. A concrete + device relinks the entities to it. A pre-migration composite id and None have no + concrete device to hold the link, so the entities are detached; in targeted mode + a composite or None source with no matching duplicate is a no-op. :param sweep_helper_devices: By default only the helper's single duplicate of source_device_id (a split or fork) is removed. When True, every device the helper owns except source_device_id and keep_device_ids is removed instead - @@ -193,19 +193,7 @@ def async_remove_helper_devices( composite_device_id = ( source_device.composite_device_id if source_is_concrete else source_device_id ) - split_devices = ( - device_registry.async_get_devices_for_composite_device_id(composite_device_id) - if composite_device_id is not None - else [] - ) - - # The helper's entities are relinked to the source device: itself when concrete, else - # the composite's recorded primary owner's split. - target_device_id = ( - source_device_id - if source_is_concrete - else _composite_source_split_id(split_devices, helper_config_entry_id) - ) + target_device_id = source_device_id if source_is_concrete else None if sweep_helper_devices: _sweep_helper_devices( @@ -226,36 +214,6 @@ def async_remove_helper_devices( ) -def _composite_source_split_id( - split_devices: list[dr.DeviceEntry], helper_config_entry_id: str -) -> str | None: - """Return the source split of a composite device. - - This is the split owned by the composite's recorded primary config entry - the rule - _restore_composite_device uses. Devices migrated from before 2024.7 have no recorded - primary (primary_config_entry, and thus composite_primary_config_entry, is None), so - fall back to a surviving non-helper split, mirroring _restore_composite_device's - fallback to the first split. None when source_device_id is not a composite id or the - helper owns every split. - """ - non_helper_splits = [ - device - for device in split_devices - if device.config_entry_id != helper_config_entry_id - ] - if not non_helper_splits: - return None - primary_config_entry_id = split_devices[0].composite_primary_config_entry - return next( - ( - device.id - for device in non_helper_splits - if device.config_entry_id == primary_config_entry_id - ), - non_helper_splits[0].id, - ) - - def _remove_duplicate_helper_device( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, diff --git a/tests/helpers/test_helper_integration.py b/tests/helpers/test_helper_integration.py index 0e899de2c52295..8bf01c5915b866 100644 --- a/tests/helpers/test_helper_integration.py +++ b/tests/helpers/test_helper_integration.py @@ -526,16 +526,6 @@ async def test_async_handle_source_entity_new_entity_id( "source_via_composite_id", [pytest.param(True, id="composite_id"), pytest.param(False, id="source_split")], ) -@pytest.mark.parametrize( - "record_composite_primary", - [ - pytest.param(True, id="recorded_primary"), - # Devices migrated from before 2024.7 have no primary_config_entry, so their splits - # carry composite_primary_config_entry=None; the source split is then found by - # falling back to the surviving non-helper split. - pytest.param(False, id="no_recorded_primary"), - ], -) async def test_async_remove_helper_devices( hass: HomeAssistant, device_registry: dr.DeviceRegistry, @@ -543,7 +533,6 @@ async def test_async_remove_helper_devices( helper_identifiers: set[tuple[str, str]], helper_has_composite_identifiers: bool, source_via_composite_id: bool, - record_composite_primary: bool, ) -> None: """Test migrating a helper off a device it co-owned before the migration split. @@ -551,17 +540,14 @@ async def test_async_remove_helper_devices( the pre-migration id as their composite id. The helper's split is found via that id - both while it still carries the identifiers copied at the split and once the helper has re-registered and pruned them to its own - and whether the caller passes the composite - id or the concrete source split. Its entities move onto the source split; its split is - removed. + id or the concrete source split. Its split is removed; its entities move onto the source + split when a concrete device is passed, or are detached when only the composite id is. """ source_config_entry = MockConfigEntry(domain=SOURCE_DOMAIN) source_config_entry.add_to_hass(hass) helper_config_entry = MockConfigEntry(domain=HELPER_DOMAIN) helper_config_entry.add_to_hass(hass) composite_id = "pre_split_composite_id" - composite_primary = ( - source_config_entry.entry_id if record_composite_primary else None - ) source_split = device_registry.async_get_or_create( config_entry_id=source_config_entry.entry_id, @@ -571,16 +557,14 @@ async def test_async_remove_helper_devices( config_entry_id=helper_config_entry.entry_id, identifiers=helper_identifiers, ) - # Both are splits of the same pre-migration device, sharing its id and primary owner + # Both are splits of the same pre-migration device, sharing its id device_registry.devices[source_split.id] = attr.evolve( source_split, composite_device_id=composite_id, - composite_primary_config_entry=composite_primary, ) device_registry.devices[helper_split.id] = attr.evolve( helper_split, composite_device_id=composite_id, - composite_primary_config_entry=composite_primary, has_composite_identifiers=helper_has_composite_identifiers, ) # A helper entity on the helper's split, plus one not linked to any device @@ -601,10 +585,12 @@ async def test_async_remove_helper_devices( source_device_id=composite_id if source_via_composite_id else source_split.id, ) - # The helper's entity moved onto the source split; its own split was removed + # The helper's split was removed. Its entity moved onto the source split when a concrete + # source was passed; with only the composite id there is no concrete device, so it detaches. + expected_device_id = None if source_via_composite_id else source_split.id assert ( entity_registry.async_get(helper_entity_entry.entity_id).device_id - == source_split.id + == expected_device_id ) assert ( entity_registry.async_get(extra_helper_entity_entry.entity_id).device_id is None @@ -613,67 +599,6 @@ async def test_async_remove_helper_devices( assert device_registry.async_get(source_split.id) is not None -async def test_async_remove_helper_devices_multiple_co_owners( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, -) -> None: - """With more than two co-owners, the helper's entities go to the primary's split. - - A pre-migration device can be co-owned by more than two config entries. The source - split is identified by the composite's recorded primary owner, not by split order, so - the helper's entities are not relinked onto an unrelated integration's split. - """ - source_config_entry = MockConfigEntry(domain=SOURCE_DOMAIN) - source_config_entry.add_to_hass(hass) - other_config_entry = MockConfigEntry(domain="other") - other_config_entry.add_to_hass(hass) - helper_config_entry = MockConfigEntry(domain=HELPER_DOMAIN) - helper_config_entry.add_to_hass(hass) - composite_id = "pre_split_composite_id" - - # other_split is indexed before source_split, so a split-order heuristic would wrongly - # pick it as the source; the recorded primary (source) must win instead. - other_split = device_registry.async_get_or_create( - config_entry_id=other_config_entry.entry_id, identifiers={("other", "1")} - ) - source_split = device_registry.async_get_or_create( - config_entry_id=source_config_entry.entry_id, identifiers={(SOURCE_DOMAIN, "1")} - ) - helper_split = device_registry.async_get_or_create( - config_entry_id=helper_config_entry.entry_id, identifiers={(HELPER_DOMAIN, "1")} - ) - for split in (other_split, source_split, helper_split): - device_registry.devices[split.id] = attr.evolve( - split, - composite_device_id=composite_id, - composite_primary_config_entry=source_config_entry.entry_id, - ) - helper_entity_entry = entity_registry.async_get_or_create( - "sensor", - HELPER_DOMAIN, - "1", - config_entry=helper_config_entry, - device_id=helper_split.id, - ) - - async_remove_helper_devices( - hass, - helper_config_entry_id=helper_config_entry.entry_id, - source_device_id=composite_id, - ) - - # Relinked to the primary owner's (source) split, not the unrelated other split, which - # is left untouched - assert ( - entity_registry.async_get(helper_entity_entry.entity_id).device_id - == source_split.id - ) - assert device_registry.async_get(helper_split.id) is None - assert device_registry.async_get(other_split.id) is not None - assert device_registry.async_get(source_split.id) is not None - - async def test_async_remove_helper_devices_fork( hass: HomeAssistant, device_registry: dr.DeviceRegistry, From 8a7bacc71370c3595db8dbb54c4954122316ae4b Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 20 Jul 2026 19:52:37 +0200 Subject: [PATCH 4/8] Execute uv run in $VIRTUAL_ENV (#176667) --- .github/copilot-instructions.md | 3 ++- AGENTS.md | 3 ++- Dockerfile.dev | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5fa2da4a257b3d..5ec53f523ad43e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -149,6 +149,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom ## Development Commands +- Run "python3" in current virtual environment to ensure the correct Python version is used for testing. - When entering a new environment or worktree, run `script/setup` to set up the virtual environment with all development dependencies (pylint, pre-commit hooks, etc.). This is required before committing. If uv reports that no download was found for the required Python version, the environment is running an outdated version of uv; upgrade it with `curl -LsSf https://astral.sh/uv/install.sh | sh` and run `script/setup` again. - .vscode/tasks.json contains useful commands used for development. - After finishing a code session, run `uv run prek run --all-files` to check for linting and formatting issues. @@ -162,7 +163,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom ## Testing - Use `uv run pytest` to run tests -- After modifying `strings.json` for an integration, regenerate the English translation file before running tests: `.venv/bin/python3 -m script.translations develop --integration `. Tests load translations from the generated `translations/en.json`, not directly from `strings.json`. +- After modifying `strings.json` for an integration, regenerate the English translation file before running tests: `python3 -m script.translations develop --integration `. Tests load translations from the generated `translations/en.json`, not directly from `strings.json`. - When writing or modifying tests, ensure all test function parameters have type annotations. - Prefer concrete types (for example, `HomeAssistant`, `MockConfigEntry`, etc.) over `Any`. - Prefer `@pytest.mark.usefixtures` over arguments, if the argument is not going to be used. diff --git a/AGENTS.md b/AGENTS.md index c21bf9bf535c6c..cd4be0a644e1bf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom ## Development Commands +- Run "python3" in current virtual environment to ensure the correct Python version is used for testing. - When entering a new environment or worktree, run `script/setup` to set up the virtual environment with all development dependencies (pylint, pre-commit hooks, etc.). This is required before committing. If uv reports that no download was found for the required Python version, the environment is running an outdated version of uv; upgrade it with `curl -LsSf https://astral.sh/uv/install.sh | sh` and run `script/setup` again. - .vscode/tasks.json contains useful commands used for development. - After finishing a code session, run `uv run prek run --all-files` to check for linting and formatting issues. @@ -26,7 +27,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom ## Testing - Use `uv run pytest` to run tests -- After modifying `strings.json` for an integration, regenerate the English translation file before running tests: `.venv/bin/python3 -m script.translations develop --integration `. Tests load translations from the generated `translations/en.json`, not directly from `strings.json`. +- After modifying `strings.json` for an integration, regenerate the English translation file before running tests: `python3 -m script.translations develop --integration `. Tests load translations from the generated `translations/en.json`, not directly from `strings.json`. - When writing or modifying tests, ensure all test function parameters have type annotations. - Prefer concrete types (for example, `HomeAssistant`, `MockConfigEntry`, etc.) over `Any`. - Prefer `@pytest.mark.usefixtures` over arguments, if the argument is not going to be used. diff --git a/Dockerfile.dev b/Dockerfile.dev index 1248979211b465..e3344bec87de9e 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -37,6 +37,10 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv USER vscode ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv" +# Force "uv run" to use the configured virtual environment for all commands +# avoid using uv run --active for every command +# '.venv' is no longer created +ENV UV_PROJECT_ENVIRONMENT=$VIRTUAL_ENV RUN --mount=type=bind,source=.python-version,target=.python-version \ uv python install \ && uv venv $VIRTUAL_ENV From f5093afa580dee50b0164325e227e2a9fcd1d791 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 20 Jul 2026 11:49:06 -0700 Subject: [PATCH 5/8] Fix voluptuous schema conversion of custom callables (#176814) --- homeassistant/helpers/llm.py | 5 +++ .../homeassistant/snapshots/test_llm.ambr | 32 +++++++++++++++++++ tests/components/homeassistant/test_llm.py | 15 +++++++++ tests/helpers/test_llm.py | 4 +++ 4 files changed, 56 insertions(+) create mode 100644 tests/components/homeassistant/snapshots/test_llm.ambr diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index efc050a2c257d8..5bbb2563bcdb9d 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -406,6 +406,11 @@ def merged(x: Any) -> Any: def selector_serializer(schema: Any) -> Any: # noqa: C901 """Convert selectors into OpenAPI schema.""" + if schema is cv.string or schema is intent.non_empty_string: + return {"type": "string"} + if schema is cv.boolean: + return {"type": "boolean"} + if not isinstance(schema, selector.Selector): return UNSUPPORTED diff --git a/tests/components/homeassistant/snapshots/test_llm.ambr b/tests/components/homeassistant/snapshots/test_llm.ambr new file mode 100644 index 00000000000000..19ecb10ae4b107 --- /dev/null +++ b/tests/components/homeassistant/snapshots/test_llm.ambr @@ -0,0 +1,32 @@ +# serializer version: 1 +# name: test_get_live_context_schema + dict({ + 'properties': dict({ + 'area': dict({ + 'description': 'Filter entities by area name or alias (case-insensitive).', + 'type': 'string', + }), + 'domain': dict({ + 'anyOf': list([ + dict({ + 'type': 'string', + }), + dict({ + 'items': dict({ + 'type': 'string', + }), + 'type': 'array', + }), + ]), + 'description': "Filter entities by domain (e.g. 'light', 'sensor'). Accepts a single domain or a list.", + }), + 'name': dict({ + 'description': 'Filter entities by name or alias (case-insensitive).', + 'type': 'string', + }), + }), + 'required': list([ + ]), + 'type': 'object', + }) +# --- diff --git a/tests/components/homeassistant/test_llm.py b/tests/components/homeassistant/test_llm.py index 53cb3574803185..241954b749c30c 100644 --- a/tests/components/homeassistant/test_llm.py +++ b/tests/components/homeassistant/test_llm.py @@ -1,6 +1,8 @@ """Tests for the homeassistant LLM tools platform.""" import pytest +from syrupy.assertion import SnapshotAssertion +from voluptuous_openapi import convert from homeassistant.components import llm as llm_component from homeassistant.components.homeassistant import llm as ha_llm @@ -393,3 +395,16 @@ async def _get_live_context(tool_args: dict) -> dict: assert result["result"].count("domain: climate") == 1 assert "Kitchen" in result["result"] assert "Office" not in result["result"] + + +async def test_get_live_context_schema( + hass: HomeAssistant, snapshot: SnapshotAssertion +) -> None: + """Test that GetLiveContext tool parameters convert to a sane OpenAPI schema.""" + result = await llm_component.async_get_tools(hass, _llm_context(), "assist") + tool = next(t for t in result.tools if t.name == "GetLiveContext") + + api = await llm.async_get_api(hass, "assist", _llm_context()) + schema = convert(tool.parameters, custom_serializer=api.custom_serializer) + + assert schema == snapshot diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index 805dd3239c90a1..873d2342bb6022 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -985,6 +985,10 @@ async def test_selector_serializer( api = await llm.async_get_api(hass, "assist", llm_context) selector_serializer = api.custom_serializer + assert selector_serializer(cv.string) == {"type": "string"} + assert selector_serializer(cv.boolean) == {"type": "boolean"} + assert selector_serializer(intent.non_empty_string) == {"type": "string"} + assert selector_serializer(selector.ActionSelector()) == {"type": "string"} assert selector_serializer(selector.AddonSelector()) == {"type": "string"} assert selector_serializer(selector.AreaSelector()) == {"type": "string"} From 860ac1bf51a957efd43dffec0db38fcce09a8349 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 20 Jul 2026 23:13:01 +0200 Subject: [PATCH 6/8] Migrate integrations to async_get_device_by_identifier (part 1) (#176901) --- homeassistant/components/airgradient/coordinator.py | 4 ++-- homeassistant/components/airos/__init__.py | 4 ++-- homeassistant/components/airthings_ble/sensor.py | 10 ++++++---- homeassistant/components/alexa_devices/coordinator.py | 4 ++-- homeassistant/components/androidtv/diagnostics.py | 4 ++-- homeassistant/components/anthropic/__init__.py | 4 ++-- homeassistant/components/aqvify/coordinator.py | 5 +++-- homeassistant/components/bosch_shc/entity.py | 4 +++- homeassistant/components/broadlink/device.py | 4 ++-- homeassistant/components/comelit/coordinator.py | 4 ++-- homeassistant/components/comelit/utils.py | 4 +++- homeassistant/components/deconz/services.py | 4 ++-- .../components/devolo_home_network/coordinator.py | 6 ++++-- homeassistant/components/dsmr/sensor.py | 4 +++- homeassistant/components/duco/sensor.py | 4 ++-- .../components/dwd_weather_warnings/__init__.py | 4 +++- homeassistant/components/earn_e_p1/coordinator.py | 6 ++++-- homeassistant/components/enphase_envoy/coordinator.py | 9 ++------- homeassistant/components/epson/media_player.py | 4 +++- 19 files changed, 52 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/airgradient/coordinator.py b/homeassistant/components/airgradient/coordinator.py index ad6c48c7d162af..ef39b55b8a3feb 100644 --- a/homeassistant/components/airgradient/coordinator.py +++ b/homeassistant/components/airgradient/coordinator.py @@ -75,8 +75,8 @@ async def _async_update_data(self) -> AirGradientData: ) from error if measures.firmware_version != self._current_version: device_registry = dr.async_get(self.hass) - device_entry = device_registry.async_get_device( - identifiers={(DOMAIN, self.serial_number)} + device_entry = device_registry.async_get_device_by_identifier( + (DOMAIN, self.serial_number), self.config_entry.entry_id ) assert device_entry device_registry.async_update_device( diff --git a/homeassistant/components/airos/__init__.py b/homeassistant/components/airos/__init__.py index e154c3d1b66254..191209fc23b64f 100644 --- a/homeassistant/components/airos/__init__.py +++ b/homeassistant/components/airos/__init__.py @@ -184,8 +184,8 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> b mac_adress = dr.format_mac(entry.unique_id) device_registry = dr.async_get(hass) - if device_entry := device_registry.async_get_device( - connections={(dr.CONNECTION_NETWORK_MAC, mac_adress)} + if device_entry := device_registry.async_get_device_by_connection( + (dr.CONNECTION_NETWORK_MAC, mac_adress), entry.entry_id ): old_device_id = next( ( diff --git a/homeassistant/components/airthings_ble/sensor.py b/homeassistant/components/airthings_ble/sensor.py index afeaacc62f160d..071bd1c1fa1dc8 100644 --- a/homeassistant/components/airthings_ble/sensor.py +++ b/homeassistant/components/airthings_ble/sensor.py @@ -169,7 +169,9 @@ class AirthingsBLESensorEntityDescription(SensorEntityDescription): @callback -def async_migrate(hass: HomeAssistant, address: str, sensor_name: str) -> None: +def async_migrate( + hass: HomeAssistant, entry_id: str, address: str, sensor_name: str +) -> None: """Migrate entities to new unique ids (with BLE Address).""" ent_reg = er.async_get(hass) unique_id_trailer = f"_{sensor_name}" @@ -179,8 +181,8 @@ def async_migrate(hass: HomeAssistant, address: str, sensor_name: str) -> None: return dev_reg = dr.async_get(hass) if not ( - device := dev_reg.async_get_device( - connections={(CONNECTION_BLUETOOTH, address)} + device := dev_reg.async_get_device_by_connection( + (CONNECTION_BLUETOOTH, address), entry_id ) ): return @@ -221,7 +223,7 @@ async def async_setup_entry( sensor_value, ) continue - async_migrate(hass, coordinator.data.address, sensor_type) + async_migrate(hass, entry.entry_id, coordinator.data.address, sensor_type) entities.append( AirthingsSensor( coordinator, coordinator.data, SENSORS_MAPPING_TEMPLATE[sensor_type] diff --git a/homeassistant/components/alexa_devices/coordinator.py b/homeassistant/components/alexa_devices/coordinator.py index 8f3bb113059650..5031537a940d6d 100644 --- a/homeassistant/components/alexa_devices/coordinator.py +++ b/homeassistant/components/alexa_devices/coordinator.py @@ -236,8 +236,8 @@ async def _async_remove_device_stale( "Detected change in devices: serial %s removed", serial_num, ) - device = device_registry.async_get_device( - identifiers={(DOMAIN, serial_num)} + device = device_registry.async_get_device_by_identifier( + (DOMAIN, serial_num), self.config_entry.entry_id ) if device: device_registry.async_update_device( diff --git a/homeassistant/components/androidtv/diagnostics.py b/homeassistant/components/androidtv/diagnostics.py index e7f2cdb540c854..407964ebe1341c 100644 --- a/homeassistant/components/androidtv/diagnostics.py +++ b/homeassistant/components/androidtv/diagnostics.py @@ -35,8 +35,8 @@ async def async_get_config_entry_diagnostics( # Gather information how this AndroidTV device is represented in Home Assistant device_registry = dr.async_get(hass) entity_registry = er.async_get(hass) - hass_device = device_registry.async_get_device( - identifiers={(DOMAIN, str(entry.unique_id))} + hass_device = device_registry.async_get_device_by_identifier( + (DOMAIN, str(entry.unique_id)), entry.entry_id ) if not hass_device: return data diff --git a/homeassistant/components/anthropic/__init__.py b/homeassistant/components/anthropic/__init__.py index 05f30d960b90ff..e70e5719894d77 100644 --- a/homeassistant/components/anthropic/__init__.py +++ b/homeassistant/components/anthropic/__init__.py @@ -106,8 +106,8 @@ async def async_migrate_integration(hass: HomeAssistant) -> None: DOMAIN, entry.entry_id, ) - device = device_registry.async_get_device( - identifiers={(DOMAIN, entry.entry_id)} + device = device_registry.async_get_device_by_identifier( + (DOMAIN, entry.entry_id), entry.entry_id ) if conversation_entity_id is not None: diff --git a/homeassistant/components/aqvify/coordinator.py b/homeassistant/components/aqvify/coordinator.py index 65f064e29c3948..d1af79a5c64796 100644 --- a/homeassistant/components/aqvify/coordinator.py +++ b/homeassistant/components/aqvify/coordinator.py @@ -126,8 +126,9 @@ async def _async_update_data(self) -> AqvifyCoordinatorData: account_id = self.config_entry.unique_id device_registry = dr.async_get(self.hass) for device_id in stale_devices: - device = device_registry.async_get_device( - identifiers={(DOMAIN, f"{account_id}_{device_id}")} + device = device_registry.async_get_device_by_identifier( + (DOMAIN, f"{account_id}_{device_id}"), + self.config_entry.entry_id, ) if device: device_registry.async_update_device( diff --git a/homeassistant/components/bosch_shc/entity.py b/homeassistant/components/bosch_shc/entity.py index 91484bc9b6fd81..4b70eee92789cf 100644 --- a/homeassistant/components/bosch_shc/entity.py +++ b/homeassistant/components/bosch_shc/entity.py @@ -17,7 +17,9 @@ async def async_remove_devices( ) -> None: """Get item that is removed from session.""" dev_registry = dr.async_get(hass) - device = dev_registry.async_get_device(identifiers={(DOMAIN, entity.device_id)}) + device = dev_registry.async_get_device_by_identifier( + (DOMAIN, entity.device_id), entry_id + ) if device is not None: dev_registry.async_update_device(device.id, remove_config_entry_id=entry_id) diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py index 3bc403a706a39a..4124276b1327f6 100644 --- a/homeassistant/components/broadlink/device.py +++ b/homeassistant/components/broadlink/device.py @@ -81,8 +81,8 @@ async def async_update(hass: HomeAssistant, entry: ConfigEntry) -> None: """ device_registry = dr.async_get(hass) assert entry.unique_id - device_entry = device_registry.async_get_device( - identifiers={(DOMAIN, entry.unique_id)} + device_entry = device_registry.async_get_device_by_identifier( + (DOMAIN, entry.unique_id), entry.entry_id ) assert device_entry device_registry.async_update_device(device_entry.id, name=entry.title) diff --git a/homeassistant/components/comelit/coordinator.py b/homeassistant/components/comelit/coordinator.py index 374498631fcf1d..50948d8e36cd75 100644 --- a/homeassistant/components/comelit/coordinator.py +++ b/homeassistant/components/comelit/coordinator.py @@ -145,8 +145,8 @@ async def _async_remove_stale_devices( i, ) identifier = f"{self.config_entry.entry_id}-{dev_type}-{i}" - device = device_registry.async_get_device( - identifiers={(DOMAIN, identifier)} + device = device_registry.async_get_device_by_identifier( + (DOMAIN, identifier), self.config_entry.entry_id ) if device: device_registry.async_update_device( diff --git a/homeassistant/components/comelit/utils.py b/homeassistant/components/comelit/utils.py index 30f5d691f41097..37b342b9695ae9 100644 --- a/homeassistant/components/comelit/utils.py +++ b/homeassistant/components/comelit/utils.py @@ -78,7 +78,9 @@ def _async_remove_state_config_entry_from_devices( device_registry = dr.async_get(hass) for identifier in identifiers: - device = device_registry.async_get_device(identifiers={(DOMAIN, identifier)}) + device = device_registry.async_get_device_by_identifier( + (DOMAIN, identifier), config_entry.entry_id + ) if device: _LOGGER.info( "Removing config entry %s from device %s", diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index c06112f820a2a7..2b87d97416d77c 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -187,8 +187,8 @@ async def async_remove_orphaned_entries_service(hub: DeconzHub) -> None: ] # Don't remove the Gateway service entry - hub_service = device_registry.async_get_device( - identifiers={(DOMAIN, hub.api.config.bridge_id)} + hub_service = device_registry.async_get_device_by_identifier( + (DOMAIN, hub.api.config.bridge_id), hub.config_entry.entry_id ) if hub_service and hub_service.id in devices_to_be_removed: devices_to_be_removed.remove(hub_service.id) diff --git a/homeassistant/components/devolo_home_network/coordinator.py b/homeassistant/components/devolo_home_network/coordinator.py index b4196fbd90db68..28a64eb60cd865 100644 --- a/homeassistant/components/devolo_home_network/coordinator.py +++ b/homeassistant/components/devolo_home_network/coordinator.py @@ -44,6 +44,8 @@ class DevoloDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]): """Class to manage fetching data from devolo Home Network devices.""" + config_entry: DevoloHomeNetworkConfigEntry + def __init__( self, hass: HomeAssistant, @@ -86,8 +88,8 @@ def update_sw_version(self) -> None: """Update device registry with new firmware version.""" device_registry = dr.async_get(self.hass) if ( - device_entry := device_registry.async_get_device( - identifiers={(DOMAIN, self.device.serial_number)} + device_entry := device_registry.async_get_device_by_identifier( + (DOMAIN, self.device.serial_number), self.config_entry.entry_id ) ) and device_entry.sw_version != self.device.firmware_version: device_registry.async_update_device( diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index e4340404a1b44a..71e39af387134f 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -608,7 +608,9 @@ def rename_old_gas_to_mbus( """Rename old gas sensor to mbus variant.""" dev_reg = dr.async_get(hass) for dev_id in (mbus_device_id, entry.entry_id): - device_entry_v1 = dev_reg.async_get_device(identifiers={(DOMAIN, dev_id)}) + device_entry_v1 = dev_reg.async_get_device_by_identifier( + (DOMAIN, dev_id), entry.entry_id + ) if device_entry_v1 is not None: device_id = device_entry_v1.id diff --git a/homeassistant/components/duco/sensor.py b/homeassistant/components/duco/sensor.py index 63e8f16cd27d1a..b15a489cef2eda 100644 --- a/homeassistant/components/duco/sensor.py +++ b/homeassistant/components/duco/sensor.py @@ -258,8 +258,8 @@ def _async_add_new_entities() -> None: device_reg = dr.async_get(hass) mac = entry.unique_id for node_id in stale_node_ids: - device = device_reg.async_get_device( - identifiers={(DOMAIN, f"{mac}_{node_id}")} + device = device_reg.async_get_device_by_identifier( + (DOMAIN, f"{mac}_{node_id}"), entry.entry_id ) if device: device_reg.async_update_device( diff --git a/homeassistant/components/dwd_weather_warnings/__init__.py b/homeassistant/components/dwd_weather_warnings/__init__.py index 67818456dbe34f..f9df009c52f01a 100644 --- a/homeassistant/components/dwd_weather_warnings/__init__.py +++ b/homeassistant/components/dwd_weather_warnings/__init__.py @@ -12,7 +12,9 @@ async def async_setup_entry( ) -> bool: """Set up a config entry.""" device_registry = dr.async_get(hass) - if device_registry.async_get_device(identifiers={(DOMAIN, entry.entry_id)}): + if device_registry.async_get_device_by_identifier( + (DOMAIN, entry.entry_id), entry.entry_id + ): device_registry.async_clear_config_entry(entry.entry_id, entry.domain) coordinator = DwdWeatherWarningsCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() diff --git a/homeassistant/components/earn_e_p1/coordinator.py b/homeassistant/components/earn_e_p1/coordinator.py index 01866aa049f5b4..18e3abc52f81b3 100644 --- a/homeassistant/components/earn_e_p1/coordinator.py +++ b/homeassistant/components/earn_e_p1/coordinator.py @@ -20,6 +20,8 @@ class EarnEP1Coordinator(DataUpdateCoordinator[dict[str, Any]]): """Coordinator for the EARN-E P1 Meter.""" + config_entry: EarnEP1ConfigEntry + def __init__( self, hass: HomeAssistant, @@ -49,8 +51,8 @@ def _handle_update(self, device: EarnEP1Device, _raw: dict[str, Any]) -> None: self.sw_version = device.sw_version device_registry = dr.async_get(self.hass) if ( - device_entry := device_registry.async_get_device( - identifiers={(DOMAIN, self.identifier)} + device_entry := device_registry.async_get_device_by_identifier( + (DOMAIN, self.identifier), self.config_entry.entry_id ) ) is not None: device_registry.async_update_device( diff --git a/homeassistant/components/enphase_envoy/coordinator.py b/homeassistant/components/enphase_envoy/coordinator.py index 3549c2f7c3c87e..e6b4145bdcee44 100644 --- a/homeassistant/components/enphase_envoy/coordinator.py +++ b/homeassistant/components/enphase_envoy/coordinator.py @@ -216,13 +216,8 @@ async def _async_fetch_and_compare_mac(self) -> None: # Add to or update device registry connections as needed device_registry = dr.async_get(self.hass) - envoy_device = device_registry.async_get_device( - identifiers={ - ( - DOMAIN, - self.envoy_serial_number, - ) - } + envoy_device = device_registry.async_get_device_by_identifier( + (DOMAIN, self.envoy_serial_number), self.config_entry.entry_id ) if envoy_device is None: _LOGGER.error( diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 1b1b442c384da8..bd103c56752117 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -106,7 +106,9 @@ async def set_unique_id(self) -> bool: if old_entity_id is not None: ent_reg.async_update_entity(old_entity_id, new_unique_id=uid) dev_reg = dr.async_get(self.hass) - device = dev_reg.async_get_device({(DOMAIN, self._entry.entry_id)}) + device = dev_reg.async_get_device_by_identifier( + (DOMAIN, self._entry.entry_id), self._entry.entry_id + ) if device is not None: dev_reg.async_update_device(device.id, new_identifiers={(DOMAIN, uid)}) self.hass.async_create_task( From b260f4e24e16f39c90a6f6287c5f2310dff21fe4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 20 Jul 2026 23:13:58 +0200 Subject: [PATCH 7/8] Migrate integrations to async_get_device_by_identifier (part 4) (#176908) --- homeassistant/components/simplisafe/__init__.py | 5 +++-- homeassistant/components/smartthings/__init__.py | 8 +++++--- homeassistant/components/smhi/config_flow.py | 4 ++-- homeassistant/components/solarlog/coordinator.py | 13 ++++++------- homeassistant/components/squeezebox/media_player.py | 4 ++-- homeassistant/components/steam_online/__init__.py | 4 +++- homeassistant/components/tailscale/coordinator.py | 4 +++- homeassistant/components/tedee/coordinator.py | 4 ++-- homeassistant/components/tplink/coordinator.py | 4 ++-- homeassistant/components/tradfri/diagnostics.py | 4 ++-- homeassistant/components/trmnl/coordinator.py | 4 ++-- homeassistant/components/tuya/coordinator.py | 4 ++-- homeassistant/components/twinkly/__init__.py | 4 ++-- homeassistant/components/twinkly/coordinator.py | 4 ++-- homeassistant/components/uptime_kuma/coordinator.py | 10 ++++++---- homeassistant/components/uptimerobot/coordinator.py | 4 ++-- homeassistant/components/waqi/__init__.py | 4 ++-- homeassistant/components/watts/coordinator.py | 4 +++- homeassistant/components/wolflink/__init__.py | 4 +++- homeassistant/components/xbox/__init__.py | 8 ++++++-- homeassistant/components/yolink/__init__.py | 4 ++-- 21 files changed, 62 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index df648d618854f8..e11b47eeecc7f8 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -117,8 +117,9 @@ def _async_register_base_station( ) # Check for an old system ID format and remove it: - if old_base_station := device_registry.async_get_device( - identifiers={(DOMAIN, system.system_id)} # type: ignore[arg-type] + if old_base_station := device_registry.async_get_device_by_identifier( + (DOMAIN, system.system_id), # type: ignore[arg-type] + entry.entry_id, ): # Update the new base station with any properties the user might have configured # on the old base station: diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 1eb9559a2b8f32..09cea70fa99f22 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -244,8 +244,8 @@ def _handle_new_subscription_identifier(identifier: str | None) -> None: def handle_deleted_device(device_id: str) -> None: """Handle a deleted device.""" - dev_entry = device_registry.async_get_device( - identifiers={(DOMAIN, device_id)}, + dev_entry = device_registry.async_get_device_by_identifier( + (DOMAIN, device_id), entry.entry_id ) if dev_entry is not None: device_registry.async_update_device( @@ -587,7 +587,9 @@ def create_devices( if mac_connections: kwargs.setdefault(ATTR_CONNECTIONS, set()).update(mac_connections) if ( - device_registry.async_get_device({(DOMAIN, device.device.device_id)}) + device_registry.async_get_device_by_identifier( + (DOMAIN, device.device.device_id), entry.entry_id + ) is None ): kwargs.update( diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py index 3ae60f8deab5f2..ff47320ce3817a 100644 --- a/homeassistant/components/smhi/config_flow.py +++ b/homeassistant/components/smhi/config_flow.py @@ -102,8 +102,8 @@ async def async_step_reconfigure( ) device_reg = dr.async_get(self.hass) - if device := device_reg.async_get_device( - identifiers={(DOMAIN, f"{old_lat}, {old_lon}")} + if device := device_reg.async_get_device_by_identifier( + (DOMAIN, f"{old_lat}, {old_lon}"), reconfigure_entry.entry_id ): device_reg.async_update_device( device.id, new_identifiers={(DOMAIN, f"{lat}, {lon}")} diff --git a/homeassistant/components/solarlog/coordinator.py b/homeassistant/components/solarlog/coordinator.py index a4489babbcd0d6..e6aa88ad45cff6 100644 --- a/homeassistant/components/solarlog/coordinator.py +++ b/homeassistant/components/solarlog/coordinator.py @@ -194,13 +194,12 @@ def _async_add_remove_devices(self, inverter_data: dict[int, InverterData]) -> N if did == removed_device[0]: device_name = dn break - if device := device_registry.async_get_device( - identifiers={ - ( - DOMAIN, - f"{self.config_entry.entry_id}_{slugify(device_name)}", - ) - } + if device := device_registry.async_get_device_by_identifier( + ( + DOMAIN, + f"{self.config_entry.entry_id}_{slugify(device_name)}", + ), + self.config_entry.entry_id, ): device_registry.async_update_device( device_id=device.id, diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 9d4f9b006d8f7f..f0e91f6ff8de3e 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -125,8 +125,8 @@ async def _player_discovered( player = coordinator.player _LOGGER.debug("Setting up media_player device and entity for player %s", player) device_registry = dr.async_get(hass) - server_device = device_registry.async_get_device( - identifiers={(DOMAIN, coordinator.server_uuid)}, + server_device = device_registry.async_get_device_by_identifier( + (DOMAIN, coordinator.server_uuid), entry.entry_id ) name = player.name diff --git a/homeassistant/components/steam_online/__init__.py b/homeassistant/components/steam_online/__init__.py index 335636df105c03..182c0a78937228 100644 --- a/homeassistant/components/steam_online/__init__.py +++ b/homeassistant/components/steam_online/__init__.py @@ -64,7 +64,9 @@ def migrate_unique_id(entity_entry: er.RegistryEntry) -> dict[str, str] | None: hass.config_entries.async_add_subentry(entry, subentry) dev_reg = dr.async_get(hass) - if device := dev_reg.async_get_device({(DOMAIN, entry.entry_id)}): + if device := dev_reg.async_get_device_by_identifier( + (DOMAIN, entry.entry_id), entry.entry_id + ): if TYPE_CHECKING: assert entry.unique_id dev_reg.async_update_device( diff --git a/homeassistant/components/tailscale/coordinator.py b/homeassistant/components/tailscale/coordinator.py index 61900935938f91..971fd6b9b6322a 100644 --- a/homeassistant/components/tailscale/coordinator.py +++ b/homeassistant/components/tailscale/coordinator.py @@ -67,7 +67,9 @@ async def _remove_stale_devices(self, stale_device_ids: set[str]) -> None: device_registry = dr.async_get(self.hass) for device_id in stale_device_ids: - device = device_registry.async_get_device(identifiers={(DOMAIN, device_id)}) + device = device_registry.async_get_device_by_identifier( + (DOMAIN, device_id), self.config_entry.entry_id + ) if device: LOGGER.debug("Removing stale device: %s", device_id) device_registry.async_remove_device(device.id) diff --git a/homeassistant/components/tedee/coordinator.py b/homeassistant/components/tedee/coordinator.py index d486efc240c2e8..ba194594ab8fca 100644 --- a/homeassistant/components/tedee/coordinator.py +++ b/homeassistant/components/tedee/coordinator.py @@ -148,8 +148,8 @@ def _async_add_remove_locks(self) -> None: _LOGGER.debug("Removed locks: %s", ", ".join(map(str, removed_locks))) device_registry = dr.async_get(self.hass) for lock_id in removed_locks: - if device := device_registry.async_get_device( - identifiers={(DOMAIN, str(lock_id))} + if device := device_registry.async_get_device_by_identifier( + (DOMAIN, str(lock_id)), self.config_entry.entry_id ): device_registry.async_update_device( device_id=device.id, diff --git a/homeassistant/components/tplink/coordinator.py b/homeassistant/components/tplink/coordinator.py index 7edd4b24ee78de..5b326c4287a209 100644 --- a/homeassistant/components/tplink/coordinator.py +++ b/homeassistant/components/tplink/coordinator.py @@ -106,8 +106,8 @@ async def _process_child_devices(self) -> None: ): device_registry = dr.async_get(self.hass) for device_id in stale_device_ids: - device = device_registry.async_get_device( - identifiers={(DOMAIN, device_id)} + device = device_registry.async_get_device_by_identifier( + (DOMAIN, device_id), self.config_entry.entry_id ) if device: device_registry.async_update_device( diff --git a/homeassistant/components/tradfri/diagnostics.py b/homeassistant/components/tradfri/diagnostics.py index 733657d749cfbf..92ed1b61f496c4 100644 --- a/homeassistant/components/tradfri/diagnostics.py +++ b/homeassistant/components/tradfri/diagnostics.py @@ -18,8 +18,8 @@ async def async_get_config_entry_diagnostics( device_registry = dr.async_get(hass) device = cast( dr.DeviceEntry, - device_registry.async_get_device( - identifiers={(DOMAIN, entry.data[CONF_GATEWAY_ID])} + device_registry.async_get_device_by_identifier( + (DOMAIN, entry.data[CONF_GATEWAY_ID]), entry.entry_id ), ) diff --git a/homeassistant/components/trmnl/coordinator.py b/homeassistant/components/trmnl/coordinator.py index 9066fc440cb70e..e4aa2b93520bd1 100644 --- a/homeassistant/components/trmnl/coordinator.py +++ b/homeassistant/components/trmnl/coordinator.py @@ -59,8 +59,8 @@ async def _async_update_data(self) -> dict[int, Device]: if self.data is not None: device_registry = dr.async_get(self.hass) for device_id in set(self.data) - set(new_data): - if entry := device_registry.async_get_device( - identifiers={(DOMAIN, str(device_id))} + if entry := device_registry.async_get_device_by_identifier( + (DOMAIN, str(device_id)), self.config_entry.entry_id ): device_registry.async_update_device( device_id=entry.id, diff --git a/homeassistant/components/tuya/coordinator.py b/homeassistant/components/tuya/coordinator.py index ba3f7115438e02..da1aad1007598c 100644 --- a/homeassistant/components/tuya/coordinator.py +++ b/homeassistant/components/tuya/coordinator.py @@ -163,8 +163,8 @@ def remove_device(self, device_id: str) -> None: def async_remove_device(self, device_id: str) -> None: """Remove device from Home Assistant.""" device_registry = dr.async_get(self.hass) - device_entry = device_registry.async_get_device( - identifiers={(DOMAIN, device_id)} + device_entry = device_registry.async_get_device_by_identifier( + (DOMAIN, device_id), self._entry.entry_id ) if device_entry is not None: device_registry.async_remove_device(device_entry.id) diff --git a/homeassistant/components/twinkly/__init__.py b/homeassistant/components/twinkly/__init__.py index 313b64c899fd30..ca61ba4357ab8f 100644 --- a/homeassistant/components/twinkly/__init__.py +++ b/homeassistant/components/twinkly/__init__.py @@ -64,8 +64,8 @@ async def async_migrate_entry(hass: HomeAssistant, entry: TwinklyConfigEntry) -> entity_entry.entity_id, new_unique_id=device_info["mac"] ) device_registry = dr.async_get(hass) - device_entry = device_registry.async_get_device( - identifiers={(DOMAIN, identifier)} + device_entry = device_registry.async_get_device_by_identifier( + (DOMAIN, identifier), entry.entry_id ) if device_entry: device_registry.async_update_device( diff --git a/homeassistant/components/twinkly/coordinator.py b/homeassistant/components/twinkly/coordinator.py index 17a67674179d47..4f0c200c9700ac 100644 --- a/homeassistant/components/twinkly/coordinator.py +++ b/homeassistant/components/twinkly/coordinator.py @@ -105,8 +105,8 @@ async def _async_update_data(self) -> TwinklyData: def _async_update_device_info(self, name: str) -> None: """Update the device info.""" device_registry = dr.async_get(self.hass) - device = device_registry.async_get_device( - identifiers={(DOMAIN, self.data.device_info["mac"])}, + device = device_registry.async_get_device_by_identifier( + (DOMAIN, self.data.device_info["mac"]), self.config_entry.entry_id ) if device: device_registry.async_update_device( diff --git a/homeassistant/components/uptime_kuma/coordinator.py b/homeassistant/components/uptime_kuma/coordinator.py index a685ebce3531d0..915a4af274ac11 100644 --- a/homeassistant/components/uptime_kuma/coordinator.py +++ b/homeassistant/components/uptime_kuma/coordinator.py @@ -128,8 +128,9 @@ def async_migrate_entities_unique_ids( # migrate device identifiers and update version device_reg = dr.async_get(hass) for monitor in metrics.values(): - if device := device_reg.async_get_device( - {(DOMAIN, f"{coordinator.config_entry.entry_id}_{monitor.monitor_name!s}")} + if device := device_reg.async_get_device_by_identifier( + (DOMAIN, f"{coordinator.config_entry.entry_id}_{monitor.monitor_name!s}"), + coordinator.config_entry.entry_id, ): new_identifier = { (DOMAIN, f"{coordinator.config_entry.entry_id}_{monitor.monitor_id!s}") @@ -139,8 +140,9 @@ def async_migrate_entities_unique_ids( new_identifiers=new_identifier, sw_version=coordinator.api.version.version, ) - if device := device_reg.async_get_device( - {(DOMAIN, f"{coordinator.config_entry.entry_id}_update")} + if device := device_reg.async_get_device_by_identifier( + (DOMAIN, f"{coordinator.config_entry.entry_id}_update"), + coordinator.config_entry.entry_id, ): device_reg.async_update_device( device.id, diff --git a/homeassistant/components/uptimerobot/coordinator.py b/homeassistant/components/uptimerobot/coordinator.py index 60e99339f6488d..a85d23e1178f10 100644 --- a/homeassistant/components/uptimerobot/coordinator.py +++ b/homeassistant/components/uptimerobot/coordinator.py @@ -69,8 +69,8 @@ async def _async_update_data(self) -> dict[int, UptimeRobotMonitor]: device_registry = dr.async_get(self.hass) for monitor_id in stale_ids: - if device := device_registry.async_get_device( - identifiers={(DOMAIN, str(monitor_id))} + if device := device_registry.async_get_device_by_identifier( + (DOMAIN, str(monitor_id)), self.config_entry.entry_id ): device_registry.async_update_device( device_id=device.id, diff --git a/homeassistant/components/waqi/__init__.py b/homeassistant/components/waqi/__init__.py index 41b60a6bd82339..e88438759b51ec 100644 --- a/homeassistant/components/waqi/__init__.py +++ b/homeassistant/components/waqi/__init__.py @@ -100,8 +100,8 @@ async def async_migrate_integration(hass: HomeAssistant) -> None: entities = er.async_entries_for_config_entry(entity_registry, entry.entry_id) if TYPE_CHECKING: assert entry.unique_id is not None - device = device_registry.async_get_device( - identifiers={(DOMAIN, entry.unique_id)} + device = device_registry.async_get_device_by_identifier( + (DOMAIN, entry.unique_id), entry.entry_id ) for entity_entry in entities: diff --git a/homeassistant/components/watts/coordinator.py b/homeassistant/components/watts/coordinator.py index ae6bdfc4e2aed6..754e268d8c90a6 100644 --- a/homeassistant/components/watts/coordinator.py +++ b/homeassistant/components/watts/coordinator.py @@ -148,7 +148,9 @@ async def _remove_stale_devices(self, stale_device_ids: set[str]) -> None: for device_id in stale_device_ids: _LOGGER.info("Removing stale device: %s", device_id) - device = device_registry.async_get_device(identifiers={(DOMAIN, device_id)}) + device = device_registry.async_get_device_by_identifier( + (DOMAIN, device_id), self.config_entry.entry_id + ) if device: device_registry.async_update_device( device_id=device.id, diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py index 1a94fc70019400..93e8a0b2b94406 100644 --- a/homeassistant/components/wolflink/__init__.py +++ b/homeassistant/components/wolflink/__init__.py @@ -168,7 +168,9 @@ def _reattach_device_to_hub( device_registry = dr.async_get(hass) entity_registry = er.async_get(hass) - device = device_registry.async_get_device(identifiers={(DOMAIN, str(device_id))}) + device = device_registry.async_get_device_by_identifier( + (DOMAIN, str(device_id)), source_entry.entry_id + ) if device is None: return diff --git a/homeassistant/components/xbox/__init__.py b/homeassistant/components/xbox/__init__.py index 76e10ba5d38b3b..2a294666640ff8 100644 --- a/homeassistant/components/xbox/__init__.py +++ b/homeassistant/components/xbox/__init__.py @@ -160,14 +160,18 @@ async def async_migrate_entry(hass: HomeAssistant, entry: XboxConfigEntry) -> bo ) hass.config_entries.async_add_subentry(entry, subentry) - if device := dev_reg.async_get_device({(DOMAIN, friend.xuid)}): + if device := dev_reg.async_get_device_by_identifier( + (DOMAIN, friend.xuid), entry.entry_id + ): dev_reg.async_update_device( device.id, remove_config_entry_id=entry.entry_id, add_config_subentry_id=subentry.subentry_id, add_config_entry_id=entry.entry_id, ) - if device := dev_reg.async_get_device({(DOMAIN, "xbox_live")}): + if device := dev_reg.async_get_device_by_identifier( + (DOMAIN, "xbox_live"), entry.entry_id + ): dev_reg.async_update_device( device.id, new_identifiers={(DOMAIN, client.xuid)} ) diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index c2404ea1419c06..d491799b356107 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -80,8 +80,8 @@ def on_message(self, device: YoLinkDevice, msg_data: dict[str, Any]) -> None: and msg_data.get("event") is not None ): device_registry = dr.async_get(self._hass) - device_entry = device_registry.async_get_device( - identifiers={(DOMAIN, device_coordinator.device.device_id)} + device_entry = device_registry.async_get_device_by_identifier( + (DOMAIN, device_coordinator.device.device_id), self._entry.entry_id ) if device_entry is None: return From 0c552ec037240fa95eca9f4be674151281b32321 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 20 Jul 2026 23:16:16 +0200 Subject: [PATCH 8/8] Migrate integrations to async_get_device_by_identifier (part 2) (#176902) --- homeassistant/components/freshr/coordinator.py | 4 ++-- homeassistant/components/fyta/coordinator.py | 10 +++------- homeassistant/components/github/__init__.py | 4 +++- .../google_generative_ai_conversation/__init__.py | 4 ++-- homeassistant/components/habitica/__init__.py | 6 ++++-- homeassistant/components/home_connect/coordinator.py | 4 ++-- homeassistant/components/homee/__init__.py | 4 ++-- homeassistant/components/hue/config_flow.py | 4 ++-- homeassistant/components/hue/v1/helpers.py | 4 +++- homeassistant/components/hue/v2/device.py | 4 +++- homeassistant/components/hue/v2/hue_event.py | 8 ++++++-- .../components/husqvarna_automower/coordinator.py | 4 +++- homeassistant/components/ibeacon/coordinator.py | 4 ++-- homeassistant/components/imou/coordinator.py | 4 ++-- homeassistant/components/iometer/coordinator.py | 4 ++-- homeassistant/components/iotty/coordinator.py | 4 ++-- homeassistant/components/iron_os/coordinator.py | 5 +++-- 17 files changed, 46 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/freshr/coordinator.py b/homeassistant/components/freshr/coordinator.py index 0aabfafa8122c3..7be8fbf13944eb 100644 --- a/homeassistant/components/freshr/coordinator.py +++ b/homeassistant/components/freshr/coordinator.py @@ -85,8 +85,8 @@ async def _async_update_data(self) -> dict[str, DeviceSummary]: if stale_ids: device_registry = dr.async_get(self.hass) for device_id in stale_ids: - if device := device_registry.async_get_device( - identifiers={(DOMAIN, device_id)} + if device := device_registry.async_get_device_by_identifier( + (DOMAIN, device_id), self.config_entry.entry_id ): device_registry.async_update_device( device.id, diff --git a/homeassistant/components/fyta/coordinator.py b/homeassistant/components/fyta/coordinator.py index 5adb12b9e04d40..5ed5d6a538d0ed 100644 --- a/homeassistant/components/fyta/coordinator.py +++ b/homeassistant/components/fyta/coordinator.py @@ -97,13 +97,9 @@ def _async_add_remove_devices(self) -> None: device_registry = dr.async_get(self.hass) for plant_id in removed_plants: - if device := device_registry.async_get_device( - identifiers={ - ( - DOMAIN, - f"{self.config_entry.entry_id}-{plant_id}", - ) - } + if device := device_registry.async_get_device_by_identifier( + (DOMAIN, f"{self.config_entry.entry_id}-{plant_id}"), + self.config_entry.entry_id, ): device_registry.async_update_device( device_id=device.id, diff --git a/homeassistant/components/github/__init__.py b/homeassistant/components/github/__init__.py index 4b4bb22d6d9ca5..f2fe5ada404ba6 100644 --- a/homeassistant/components/github/__init__.py +++ b/homeassistant/components/github/__init__.py @@ -95,7 +95,9 @@ async def async_migrate_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> unique_id=repository, ) hass.config_entries.async_add_subentry(entry, subentry) - if device := dev_reg.async_get_device({(DOMAIN, repository)}): + if device := dev_reg.async_get_device_by_identifier( + (DOMAIN, repository), entry.entry_id + ): dev_reg.async_update_device( device.id, new_config_entry_id=entry.entry_id, diff --git a/homeassistant/components/google_generative_ai_conversation/__init__.py b/homeassistant/components/google_generative_ai_conversation/__init__.py index 2667a278c9c17b..3186d9c50badb9 100644 --- a/homeassistant/components/google_generative_ai_conversation/__init__.py +++ b/homeassistant/components/google_generative_ai_conversation/__init__.py @@ -151,8 +151,8 @@ async def async_migrate_integration(hass: HomeAssistant) -> None: DOMAIN, entry.entry_id, ) - device = device_registry.async_get_device( - identifiers={(DOMAIN, entry.entry_id)} + device = device_registry.async_get_device_by_identifier( + (DOMAIN, entry.entry_id), entry.entry_id ) if conversation_entity_id is not None: diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 74141c0083765e..f864db7beb5d06 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -96,8 +96,10 @@ def _party_update_listener() -> None: ) party_added_by_this_entry = None if party: - identifier = {(DOMAIN, f"{config_entry.unique_id}_{party!s}")} - if device := device_reg.async_get_device(identifiers=identifier): + identifier = (DOMAIN, f"{config_entry.unique_id}_{party!s}") + if device := device_reg.async_get_device_by_identifier( + identifier, config_entry.entry_id + ): device_reg.async_update_device( device.id, remove_config_entry_id=config_entry.entry_id ) diff --git a/homeassistant/components/home_connect/coordinator.py b/homeassistant/components/home_connect/coordinator.py index 36e13b94f2af33..8028fdd14edcd9 100644 --- a/homeassistant/components/home_connect/coordinator.py +++ b/homeassistant/components/home_connect/coordinator.py @@ -364,8 +364,8 @@ async def event_listener(self, event_message: EventMessage) -> None: self.call_all_event_listeners() case EventType.DEPAIRED: - device = self.device_registry.async_get_device( - identifiers={(DOMAIN, self.data.info.ha_id)} + device = self.device_registry.async_get_device_by_identifier( + (DOMAIN, self.data.info.ha_id), self._config_entry.entry_id ) if device: self.device_registry.async_update_device( diff --git a/homeassistant/components/homee/__init__.py b/homeassistant/components/homee/__init__.py index dac324ea09ac84..9c7ec87def9327 100644 --- a/homeassistant/components/homee/__init__.py +++ b/homeassistant/components/homee/__init__.py @@ -112,8 +112,8 @@ async def _remove_node_callback(node: HomeeNode, add: bool) -> None: """Call when a node is removed.""" if add: return - device = device_registry.async_get_device( - identifiers={(DOMAIN, f"{entry.runtime_data.settings.uid}-{node.id}")} + device = device_registry.async_get_device_by_identifier( + (DOMAIN, f"{entry.runtime_data.settings.uid}-{node.id}"), entry.entry_id ) if device: _LOGGER.info("Removing device %s", device.name) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index ded4800eaf7847..d013e87cd0abcb 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -334,8 +334,8 @@ async def _check_migrated_bridge(self, bridge: DiscoveredHueBridge) -> bool: ) # also update the bridge device dev_reg = dr.async_get(self.hass) - if bridge_device := dev_reg.async_get_device( - identifiers={(DOMAIN, old_bridge_id)} + if bridge_device := dev_reg.async_get_device_by_identifier( + (DOMAIN, old_bridge_id), conf_entry.entry_id ): dev_reg.async_update_device( bridge_device.id, diff --git a/homeassistant/components/hue/v1/helpers.py b/homeassistant/components/hue/v1/helpers.py index b0d774915df929..1cf5bc45c55d45 100644 --- a/homeassistant/components/hue/v1/helpers.py +++ b/homeassistant/components/hue/v1/helpers.py @@ -21,7 +21,9 @@ async def remove_devices(bridge, api_ids, current): if entity.entity_id in ent_registry.entities: ent_registry.async_remove(entity.entity_id) dev_registry = dr.async_get(bridge.hass) - device = dev_registry.async_get_device(identifiers={(DOMAIN, entity.device_id)}) + device = dev_registry.async_get_device_by_identifier( + (DOMAIN, entity.device_id), bridge.config_entry.entry_id + ) if device is not None: dev_registry.async_update_device( device.id, remove_config_entry_id=bridge.config_entry.entry_id diff --git a/homeassistant/components/hue/v2/device.py b/homeassistant/components/hue/v2/device.py index 4a18a1f59a2708..298fd88418604a 100644 --- a/homeassistant/components/hue/v2/device.py +++ b/homeassistant/components/hue/v2/device.py @@ -78,7 +78,9 @@ def add_device(hue_resource: Device | Room | Zone | ServiceGroup) -> dr.DeviceEn @callback def remove_device(hue_device_id: str) -> None: """Remove device from registry.""" - if device := dev_reg.async_get_device(identifiers={(DOMAIN, hue_device_id)}): + if device := dev_reg.async_get_device_by_identifier( + (DOMAIN, hue_device_id), entry.entry_id + ): # note: removal of any underlying entities is handled by core dev_reg.async_remove_device(device.id) diff --git a/homeassistant/components/hue/v2/hue_event.py b/homeassistant/components/hue/v2/hue_event.py index f89caea1e01e35..8acf0d412d2031 100644 --- a/homeassistant/components/hue/v2/hue_event.py +++ b/homeassistant/components/hue/v2/hue_event.py @@ -45,7 +45,9 @@ def handle_button_event(evt_type: EventType, hue_resource: Button) -> None: return hue_device = btn_controller.get_device(hue_resource.id) - device = dev_reg.async_get_device(identifiers={(DOMAIN, hue_device.id)}) + device = dev_reg.async_get_device_by_identifier( + (DOMAIN, hue_device.id), conf_entry.entry_id + ) # Fire event data = { @@ -72,7 +74,9 @@ def handle_rotary_event(evt_type: EventType, hue_resource: RelativeRotary) -> No LOGGER.debug("Received relative_rotary event: %s", hue_resource) hue_device = btn_controller.get_device(hue_resource.id) - device = dev_reg.async_get_device(identifiers={(DOMAIN, hue_device.id)}) + device = dev_reg.async_get_device_by_identifier( + (DOMAIN, hue_device.id), conf_entry.entry_id + ) # Fire event data = { diff --git a/homeassistant/components/husqvarna_automower/coordinator.py b/homeassistant/components/husqvarna_automower/coordinator.py index b1067ed105cba8..91b460bb7a64d1 100644 --- a/homeassistant/components/husqvarna_automower/coordinator.py +++ b/homeassistant/components/husqvarna_automower/coordinator.py @@ -230,7 +230,9 @@ def _async_add_remove_devices(self) -> None: _LOGGER.debug("Removing orphaned devices: %s", orphaned_devices) device_registry = dr.async_get(self.hass) for mower_id in orphaned_devices: - dev = device_registry.async_get_device(identifiers={(DOMAIN, mower_id)}) + dev = device_registry.async_get_device_by_identifier( + (DOMAIN, mower_id), self.config_entry.entry_id + ) if dev is not None: device_registry.async_update_device( device_id=dev.id, diff --git a/homeassistant/components/ibeacon/coordinator.py b/homeassistant/components/ibeacon/coordinator.py index 6bb04a2c4b4684..826d34a526d3fb 100644 --- a/homeassistant/components/ibeacon/coordinator.py +++ b/homeassistant/components/ibeacon/coordinator.py @@ -223,8 +223,8 @@ def _async_ignore_address(self, address: str) -> None: def _async_purge_untrackable_entities(self, unique_ids: set[str]) -> None: """Remove entities that are no longer trackable.""" for unique_id in unique_ids: - if device := self._dev_reg.async_get_device( - identifiers={(DOMAIN, unique_id)} + if device := self._dev_reg.async_get_device_by_identifier( + (DOMAIN, unique_id), self._entry.entry_id ): self._dev_reg.async_remove_device(device.id) self._last_ibeacon_advertisement_by_unique_id.pop(unique_id, None) diff --git a/homeassistant/components/imou/coordinator.py b/homeassistant/components/imou/coordinator.py index 009bca6caaad21..c6190e02b431aa 100644 --- a/homeassistant/components/imou/coordinator.py +++ b/homeassistant/components/imou/coordinator.py @@ -136,8 +136,8 @@ def _async_add_remove_devices(self, fresh_by_key: dict[str, ImouHaDevice]) -> No device_registry = dr.async_get(self.hass) for device_key in removed_keys: del self.devices_by_key[device_key] - if device := device_registry.async_get_device( - identifiers={(DOMAIN, device_key)} + if device := device_registry.async_get_device_by_identifier( + (DOMAIN, device_key), self.config_entry.entry_id ): device_registry.async_update_device( device_id=device.id, diff --git a/homeassistant/components/iometer/coordinator.py b/homeassistant/components/iometer/coordinator.py index 4d82272b43e971..e331749c553761 100644 --- a/homeassistant/components/iometer/coordinator.py +++ b/homeassistant/components/iometer/coordinator.py @@ -69,8 +69,8 @@ async def _async_update_data(self) -> IOmeterData: fw_version = f"{status.device.core.version}/{status.device.bridge.version}" if self.current_fw_version and fw_version != self.current_fw_version: device_registry = dr.async_get(self.hass) - device_entry = device_registry.async_get_device( - identifiers={(DOMAIN, status.device.id)} + device_entry = device_registry.async_get_device_by_identifier( + (DOMAIN, status.device.id), self.config_entry.entry_id ) assert device_entry device_registry.async_update_device( diff --git a/homeassistant/components/iotty/coordinator.py b/homeassistant/components/iotty/coordinator.py index f41d9dd3285c37..91d04e293d4147 100644 --- a/homeassistant/components/iotty/coordinator.py +++ b/homeassistant/components/iotty/coordinator.py @@ -92,8 +92,8 @@ async def _async_update_data(self) -> IottyData: ] for removed_device in removed_devices: - device_to_remove = self._device_registry.async_get_device( - {(DOMAIN, removed_device.device_id)} + device_to_remove = self._device_registry.async_get_device_by_identifier( + (DOMAIN, removed_device.device_id), self.config_entry.entry_id ) if device_to_remove is not None: self._device_registry.async_remove_device(device_to_remove.id) diff --git a/homeassistant/components/iron_os/coordinator.py b/homeassistant/components/iron_os/coordinator.py index 10063ebdb03640..556c50653f6ddf 100644 --- a/homeassistant/components/iron_os/coordinator.py +++ b/homeassistant/components/iron_os/coordinator.py @@ -140,8 +140,9 @@ async def _update_device_info(self) -> None: device_registry = dr.async_get(self.hass) if TYPE_CHECKING: assert self.config_entry.unique_id - device = device_registry.async_get_device( - connections={(CONNECTION_BLUETOOTH, self.config_entry.unique_id)} + device = device_registry.async_get_device_by_connection( + (CONNECTION_BLUETOOTH, self.config_entry.unique_id), + self.config_entry.entry_id, ) if device is None: return