From 0f9fdfe2decc32595301afe5154d8285033f122d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:02:59 +0100 Subject: [PATCH] Fix invalid device registry identifiers in eafm (#164654) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- homeassistant/components/eafm/__init__.py | 25 ++++++++ homeassistant/components/eafm/sensor.py | 4 +- tests/components/eafm/conftest.py | 51 ++++++++++++++- .../components/eafm/snapshots/test_init.ambr | 34 ++++++++++ tests/components/eafm/test_init.py | 64 +++++++++++++++++++ 5 files changed, 173 insertions(+), 5 deletions(-) create mode 100644 tests/components/eafm/snapshots/test_init.ambr create mode 100644 tests/components/eafm/test_init.py diff --git a/homeassistant/components/eafm/__init__.py b/homeassistant/components/eafm/__init__.py index e2af2bae9f5..ff1d622139a 100644 --- a/homeassistant/components/eafm/__init__.py +++ b/homeassistant/components/eafm/__init__.py @@ -2,14 +2,39 @@ from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from .const import DOMAIN from .coordinator import EafmConfigEntry, EafmCoordinator PLATFORMS = [Platform.SENSOR] +def _fix_device_registry_identifiers( + hass: HomeAssistant, entry: EafmConfigEntry +) -> None: + """Fix invalid identifiers in device registry. + + Added in 2026.4, can be removed in 2026.10 or later. + """ + device_registry = dr.async_get(hass) + for device_entry in dr.async_entries_for_config_entry( + device_registry, entry.entry_id + ): + old_identifier = (DOMAIN, "measure-id", entry.data["station"]) + if old_identifier not in device_entry.identifiers: # type: ignore[comparison-overlap] + continue + new_identifiers = device_entry.identifiers.copy() + new_identifiers.discard(old_identifier) # type: ignore[arg-type] + new_identifiers.add((DOMAIN, entry.data["station"])) + device_registry.async_update_device( + device_entry.id, new_identifiers=new_identifiers + ) + + async def async_setup_entry(hass: HomeAssistant, entry: EafmConfigEntry) -> bool: """Set up flood monitoring sensors for this config entry.""" + _fix_device_registry_identifiers(hass, entry) coordinator = EafmCoordinator(hass, entry=entry) await coordinator.async_config_entry_first_refresh() entry.runtime_data = coordinator diff --git a/homeassistant/components/eafm/sensor.py b/homeassistant/components/eafm/sensor.py index 5d0af596521..ce5aa35e6a2 100644 --- a/homeassistant/components/eafm/sensor.py +++ b/homeassistant/components/eafm/sensor.py @@ -94,11 +94,11 @@ class Measurement(CoordinatorEntity, SensorEntity): return self.coordinator.data["measures"][self.key]["parameterName"] @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device info.""" return DeviceInfo( entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, "measure-id", self.station_id)}, + identifiers={(DOMAIN, self.station_id)}, manufacturer="https://environment.data.gov.uk/", model=self.parameter_name, name=f"{self.station_name} {self.parameter_name} {self.qualifier}", diff --git a/tests/components/eafm/conftest.py b/tests/components/eafm/conftest.py index 5dbdc98ad29..0197cd6e2d9 100644 --- a/tests/components/eafm/conftest.py +++ b/tests/components/eafm/conftest.py @@ -1,19 +1,64 @@ """eafm fixtures.""" -from unittest.mock import patch +from collections.abc import Generator +from typing import Any +from unittest.mock import AsyncMock, patch import pytest +from homeassistant.components.eafm.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + @pytest.fixture -def mock_get_stations(): +def mock_get_stations() -> Generator[AsyncMock]: """Mock aioeafm.get_stations.""" with patch("homeassistant.components.eafm.config_flow.get_stations") as patched: + patched.return_value = [ + {"label": "My station", "stationReference": "L12345", "RLOIid": "R12345"} + ] yield patched @pytest.fixture -def mock_get_station(): +def mock_get_station(initial_value: dict[str, Any]) -> Generator[AsyncMock]: """Mock aioeafm.get_station.""" with patch("homeassistant.components.eafm.coordinator.get_station") as patched: + patched.return_value = initial_value yield patched + + +@pytest.fixture +def initial_value() -> dict[str, Any]: + """Mock aioeafm.get_station.""" + return { + "label": "My station", + "measures": [ + { + "@id": "really-long-unique-id", + "label": "York Viking Recorder - level-stage-i-15_min----", + "qualifier": "Stage", + "parameterName": "Water Level", + "latestReading": {"value": 5}, + "stationReference": "L1234", + "unit": "http://qudt.org/1.1/vocab/unit#Meter", + "unitName": "m", + } + ], + } + + +@pytest.fixture +def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry: + """Create a dummy config entry for testing.""" + entry = MockConfigEntry( + version=1, + domain=DOMAIN, + entry_id="VikingRecorder1234", + data={"station": "L1234"}, + title="Viking Recorder", + ) + entry.add_to_hass(hass) + return entry diff --git a/tests/components/eafm/snapshots/test_init.ambr b/tests/components/eafm/snapshots/test_init.ambr new file mode 100644 index 00000000000..39a5978315c --- /dev/null +++ b/tests/components/eafm/snapshots/test_init.ambr @@ -0,0 +1,34 @@ +# serializer version: 1 +# name: test_load_unload_entry + list([ + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': , + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'eafm', + 'L1234', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'https://environment.data.gov.uk/', + 'model': 'Water Level', + 'model_id': None, + 'name': 'My station Water Level Stage', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }), + ]) +# --- diff --git a/tests/components/eafm/test_init.py b/tests/components/eafm/test_init.py new file mode 100644 index 00000000000..6591fa1cb4f --- /dev/null +++ b/tests/components/eafm/test_init.py @@ -0,0 +1,64 @@ +"""Tests for initialization.""" + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.eafm.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from tests.common import MockConfigEntry + + +@pytest.mark.usefixtures("mock_get_station") +async def test_load_unload_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test being able to load and unload an entry.""" + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + assert mock_config_entry.state is ConfigEntryState.LOADED + await hass.async_block_till_done() + + assert ( + dr.async_entries_for_config_entry(device_registry, mock_config_entry.entry_id) + == snapshot + ) + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + + +@pytest.mark.usefixtures("mock_get_station") +async def test_update_device_identifiers( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + device_registry: dr.DeviceRegistry, +) -> None: + """Test being able to update device identifiers.""" + device_entry = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={(DOMAIN, "measure-id", "L1234")}, + ) + + entries = dr.async_entries_for_config_entry( + device_registry, mock_config_entry.entry_id + ) + assert len(entries) == 1 + device_entry = entries[0] + assert (DOMAIN, "measure-id", "L1234") in device_entry.identifiers + assert (DOMAIN, "L1234") not in device_entry.identifiers + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + assert mock_config_entry.state is ConfigEntryState.LOADED + await hass.async_block_till_done() + + entries = dr.async_entries_for_config_entry( + device_registry, mock_config_entry.entry_id + ) + assert len(entries) == 1 + device_entry = entries[0] + assert (DOMAIN, "measure-id", "L1234") not in device_entry.identifiers + assert (DOMAIN, "L1234") in device_entry.identifiers