diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 1f7f0f0699d1ba..e08aec135e6d88 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1757,6 +1757,25 @@ def _unindex_entry( if not self._orphaned_identifiers[identifier]: del self._orphaned_identifiers[identifier] + def get_orphaned_entries( + self, + identifiers: set[tuple[str, str]], + connections: set[tuple[str, str]], + domain: str, + ) -> list[DeletedDeviceEntry]: + """Get the orphans of a domain holding any of the given keys. + + Orphans are matched on their recorded domain so a chance identifier or connection + collision doesn't match another integration's device. connections must be + normalized. + """ + orphans: dict[str, DeletedDeviceEntry] = {} + for identifier in identifiers: + orphans.update(self._orphaned_identifiers.get(identifier, {})) + for connection in connections: + orphans.update(self._orphaned_connections.get(connection, {})) + return [entry for entry in orphans.values() if entry.domain == domain] + def get_orphaned_entry( self, identifiers: set[tuple[str, str]] | None, @@ -1770,15 +1789,10 @@ def get_orphaned_entry( (carried over by the migration with no recoverable domain) is left for the periodic purge rather than restored. """ - orphans: dict[str, DeletedDeviceEntry] = {} - for identifier in identifiers or (): - orphans.update(self._orphaned_identifiers.get(identifier, {})) - for connection in _normalize_connections(connections or set()): - orphans.update(self._orphaned_connections.get(connection, {})) - for entry in orphans.values(): - if entry.domain == domain: - return entry - return None + orphans = self.get_orphaned_entries( + identifiers or set(), _normalize_connections(connections or set()), domain + ) + return orphans[0] if orphans else None class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): @@ -4368,16 +4382,10 @@ def _async_orphan_deleted_device( # device from the same integration is orphaned, drop any existing orphan # it overlaps so the newest one wins deterministically instead of shadowing # it. - for existing in list(self._deleted_devices.values()): - if ( - existing.config_entry_id is None - and existing.domain == domain - and ( - existing.connections & deleted_device.connections - or existing.identifiers & deleted_device.identifiers - ) - ): - del self._deleted_devices[existing.id] + for existing in self._deleted_devices.get_orphaned_entries( + deleted_device.identifiers, deleted_device.connections, domain + ): + del self._deleted_devices[existing.id] self._deleted_devices[deleted_device.id] = attr.evolve( deleted_device, config_entry_id=None, diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 30cfabe0a4fdab..66456791d4a892 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -48,7 +48,7 @@ Jinja2==3.1.6 lru-dict==1.4.1 mutagen==1.48.1 openai==2.45.0 -orjson==3.12.0 +orjson==3.11.9 packaging>=23.1 paho-mqtt==2.1.0 Pillow==12.3.0 diff --git a/pyproject.toml b/pyproject.toml index 85e74c8facf1cc..95561ae862dffe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,10 @@ dependencies = [ "Pillow==12.3.0", "propcache==0.5.2", "pyOpenSSL==26.2.0", - "orjson==3.12.0", + # Do not bump orjson until the very aggressive buffer resizing introduced in + # 3.12.0, which causes memory use to balloon, has been adjusted or a workaround + # is available. + "orjson==3.11.9", "packaging>=23.1", "psutil-home-assistant==0.0.1", "python-slugify==8.0.4", diff --git a/requirements.txt b/requirements.txt index 87ed11507892c3..88cee4233102d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,7 +35,7 @@ infrared-protocols==10.0.0 Jinja2==3.1.6 lru-dict==1.4.1 mutagen==1.48.1 -orjson==3.12.0 +orjson==3.11.9 packaging>=23.1 Pillow==12.3.0 probatio==0.11.4 diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 60daa8d8ceea09..249dab632fe5e2 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -891,7 +891,7 @@ def _corrupt_store(): assert issue_entry.translation_placeholders["storage_key"] == storage_key assert issue_entry.issue_domain == HOMEASSISTANT_DOMAIN assert ( - "unexpected character, expected a JSON value: line 1 column 1 (char 0)" + "unexpected character: line 1 column 1 (char 0)" in issue_entry.translation_placeholders["error"] )