From 8440451202aaeba5cd7889f8f56533685ac8d639 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 19 Jul 2026 11:43:01 +0200 Subject: [PATCH 1/5] Detach entities from composite device when no matching device is found (#176819) Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> --- homeassistant/helpers/entity_registry.py | 4 +++- tests/helpers/test_entity_registry.py | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 092976d6e3821..56fd23d974b1b 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -2080,7 +2080,9 @@ def _split_device_id( for successor in successors: if successor.config_entry_id == config_entry_id: return successor.id - return successors[0].id + # No split device matches the entity's config entry; detach the entity + # rather than move it to an arbitrary split device it does not belong to. + return None if data is not None: for entity in data["entities"]: diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index b69752d296036..a2a0bc6ce6284 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -5547,7 +5547,7 @@ async def test_migration_repoints_entities( async def test_migration_repoints_entities_fallbacks( hass: HomeAssistant, hass_storage: dict[str, Any] ) -> None: - """An entity not exactly matching a split falls back by config entry, then first split.""" + """An entity not exactly matching a split falls back by config entry, then detaches.""" entry_a = MockConfigEntry( domain="domain_a", subentries_data=[ @@ -5608,8 +5608,8 @@ async def test_migration_repoints_entities_fallbacks( assert ( entity_registry.async_get("sensor.sub").device_id == by_entry[entry_a.entry_id] ) - # No matching config entry falls back to the first split - assert entity_registry.async_get("sensor.none").device_id in {d.id for d in splits} + # No split matches the config entry, so the entity is detached + assert entity_registry.async_get("sensor.none").device_id is None @pytest.mark.parametrize("load_registries", [False]) From 27b5baf5ce12105b2967ab2af9a0cfded6c84d04 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 19 Jul 2026 03:08:11 -0700 Subject: [PATCH 2/5] Fix roborock vacuum segment mapping repair issue (#176778) --- homeassistant/components/roborock/vacuum.py | 5 ++- tests/components/roborock/test_vacuum.py | 38 +++++++++++++++++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/roborock/vacuum.py b/homeassistant/components/roborock/vacuum.py index 96617d4c2fa91..1fae7472c9b1d 100644 --- a/homeassistant/components/roborock/vacuum.py +++ b/homeassistant/components/roborock/vacuum.py @@ -182,13 +182,16 @@ def _handle_coordinator_update(self) -> None: what was available when the area mapping was last configured. """ super()._handle_coordinator_update() + # Avoid creating false-alarm issues if home map info is not yet loaded + if self._home_trait.home_map_info is None: + return last_seen = self.last_seen_segments if last_seen is None: # No area mapping has been configured yet; nothing to check. return current_ids = { f"{map_flag}_{room.segment_id}" - for map_flag, map_info in (self._home_trait.home_map_info or {}).items() + for map_flag, map_info in self._home_trait.home_map_info.items() for room in map_info.rooms } if current_ids != {seg.id for seg in last_seen}: diff --git a/tests/components/roborock/test_vacuum.py b/tests/components/roborock/test_vacuum.py index 67ef48ab82d46..bfb169b7c363f 100644 --- a/tests/components/roborock/test_vacuum.py +++ b/tests/components/roborock/test_vacuum.py @@ -1,5 +1,6 @@ """Tests for Roborock vacuums.""" +from datetime import timedelta from typing import Any from unittest.mock import Mock, call @@ -45,11 +46,12 @@ issue_registry as ir, ) from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from .conftest import FakeDevice, set_trait_attributes from .mock_data import STATUS -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform from tests.typing import WebSocketGenerator ENTITY_ID = "vacuum.roborock_s7_maxv" @@ -582,8 +584,7 @@ async def test_segments_changed_issue( }, ) - coordinator = setup_entry.runtime_data.v1[0] - await coordinator.async_refresh() + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) await hass.async_block_till_done() issue_id = f"segments_changed_{entity_entry.id}" @@ -593,6 +594,37 @@ async def test_segments_changed_issue( assert issue.translation_key == "segments_changed" +async def test_segments_changed_issue_no_map_info( + hass: HomeAssistant, + setup_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + fake_vacuum: FakeDevice, +) -> None: + """Test no repair issue is created when map info is not loaded/empty.""" + entity_entry = entity_registry.async_get(ENTITY_ID) + assert entity_entry is not None + entity_registry.async_update_entity_options( + ENTITY_ID, + VACUUM_DOMAIN, + { + "last_seen_segments": [ + {"id": "1_16", "name": "Example room 1", "group": "Downstairs"}, + {"id": "1_99", "name": "Old room", "group": "Downstairs"}, + ], + }, + ) + + # Map info not loaded + fake_vacuum.v1_properties.home.home_map_info = None + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) + await hass.async_block_till_done() + + issue_id = f"segments_changed_{entity_entry.id}" + issue = ir.async_get(hass).async_get_issue(VACUUM_DOMAIN, issue_id) + assert issue is None + + @pytest.fixture(name="q7_vacuum_api", autouse=False) def fake_q7_vacuum_api_fixture( fake_q7_vacuum: FakeDevice, From 5e5b16a430d81f8746a8f63c1174f8bd60adc6df Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 Jul 2026 00:10:49 -1000 Subject: [PATCH 3/5] Seed esphome reconnect backoff cap from restored device_info (#176738) --- homeassistant/components/esphome/manager.py | 4 +++ tests/components/esphome/test_manager.py | 28 +++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/homeassistant/components/esphome/manager.py b/homeassistant/components/esphome/manager.py index 90e0760da46bb..2f45d283e12ba 100644 --- a/homeassistant/components/esphome/manager.py +++ b/homeassistant/components/esphome/manager.py @@ -1053,6 +1053,10 @@ async def async_start(self) -> None: self._async_cleanup() if device_info.name: reconnect_logic.name = device_info.name + # Seed the backoff cap from the restored device_info so the first + # reconnect after a restart already caps for a deep-sleep device, + # before the first live connect refreshes it. + reconnect_logic.deep_sleep = device_info.has_deep_sleep if ( bluetooth_mac_address := device_info.bluetooth_mac_address ) and entry.data.get(CONF_BLUETOOTH_MAC_ADDRESS) != bluetooth_mac_address: diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 70cdd63c5fa6d..89aeb811dbc5c 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -21,6 +21,7 @@ InvalidAuthAPIError, InvalidEncryptionKeyAPIError, LogLevel, + ReconnectLogic, RequiresEncryptionAPIError, SubDeviceInfo, SupportsResponseType, @@ -182,6 +183,33 @@ async def test_esphome_device_service_calls_not_allowed( ) in caplog.text +@pytest.mark.parametrize("has_deep_sleep", [True, False]) +async def test_reconnect_logic_seeds_deep_sleep_from_restored_device_info( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, + has_deep_sleep: bool, +) -> None: + """Restored device_info seeds reconnect_logic.deep_sleep before the first connect.""" + deep_sleep_at_start: list[bool] = [] + real_start = ReconnectLogic.start + + async def _start(self: ReconnectLogic) -> None: + # Captured before the first connect refreshes it, so this is the + # pre-populated value from the restored device_info. + deep_sleep_at_start.append(self.deep_sleep) + await real_start(self) + + with patch.object(ReconnectLogic, "start", _start): + await mock_esphome_device( + mock_client=mock_client, + device_info={"has_deep_sleep": has_deep_sleep}, + mock_storage=True, + ) + + assert deep_sleep_at_start == [has_deep_sleep] + + async def test_esphome_device_service_calls_allowed( hass: HomeAssistant, mock_config_entry: MockConfigEntry, From 1a15e2a3f043e047125be87c31723ffe40fee27d Mon Sep 17 00:00:00 2001 From: Penny Wood Date: Sun, 19 Jul 2026 20:19:54 +0800 Subject: [PATCH 4/5] Update Harbor device registry snapshot for DeviceRegistryEntrySnapshot fields (#176811) Co-authored-by: Cursor --- tests/components/harbor/snapshots/test_init.ambr | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/components/harbor/snapshots/test_init.ambr b/tests/components/harbor/snapshots/test_init.ambr index 101e73248cb6b..c28770ed0675e 100644 --- a/tests/components/harbor/snapshots/test_init.ambr +++ b/tests/components/harbor/snapshots/test_init.ambr @@ -2,8 +2,8 @@ # name: test_device_registry DeviceRegistryEntrySnapshot({ 'area_id': None, - 'config_entries': , - 'config_entries_subentries': , + 'config_entry_id': , + 'config_subentry_id': , 'configuration_url': None, 'connections': set({ }), @@ -24,7 +24,6 @@ 'model_id': None, 'name': 'Nursery', 'name_by_user': None, - 'primary_config_entry': , 'serial_number': '1234567890', 'sw_version': '1.2.3', 'via_device_id': None, From 1b104d5ad048cf8ef6147cd67bf3e0a61a46f18d Mon Sep 17 00:00:00 2001 From: Martin Hoefling Date: Sun, 19 Jul 2026 14:41:13 +0200 Subject: [PATCH 5/5] Bump knx-telegram-store to 0.10.2 (#176829) Co-authored-by: Claude Fable 5 --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 4c372bb876e74..af9d377e6c5bf 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -14,7 +14,7 @@ "xknx==3.16.0", "xknxproject==3.9.0", "knx-frontend==2026.6.23.203726", - "knx-telegram-store[sqlite,postgres]==0.10.1" + "knx-telegram-store[sqlite,postgres]==0.10.2" ], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index f08b2f99ffdef..399d9e2860aef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1441,7 +1441,7 @@ knocki==0.4.2 knx-frontend==2026.6.23.203726 # homeassistant.components.knx -knx-telegram-store[sqlite,postgres]==0.10.1 +knx-telegram-store[sqlite,postgres]==0.10.2 # homeassistant.components.kraken krakenex==2.2.2