From 406d37b0b7b1d7a75edd672705c4e212a64c4c6f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 3 Aug 2026 17:50:33 +0200 Subject: [PATCH] Fix via_device race in guardian (#177717) --- homeassistant/components/guardian/__init__.py | 13 ++++++ homeassistant/components/guardian/entity.py | 7 ++- tests/components/guardian/conftest.py | 11 ++++- tests/components/guardian/test_init.py | 45 +++++++++++++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 tests/components/guardian/test_init.py diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index afc47be08a3d..300308eaa71d 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -115,6 +115,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: GuardianConfigEntry) -> paired_sensor_manager=paired_sensor_manager, ) + # Register the valve controller device up front so paired sensors can resolve + # it as their via device when they are added: + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, entry.data[CONF_UID])}, + manufacturer="Elexa", + name=f"Guardian valve controller {entry.data[CONF_UID]}", + sw_version=valve_controller_coordinators[API_SYSTEM_DIAGNOSTICS].data[ + "firmware" + ], + ) + # Set up all of the Guardian entity platforms: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/guardian/entity.py b/homeassistant/components/guardian/entity.py index 6f4c4f964e49..e8ce957a706c 100644 --- a/homeassistant/components/guardian/entity.py +++ b/homeassistant/components/guardian/entity.py @@ -2,6 +2,7 @@ from dataclasses import dataclass +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -43,7 +44,11 @@ class PairedSensorEntity(GuardianEntity): manufacturer="Elexa", model=coordinator.data["codename"], name=f"Guardian paired sensor {paired_sensor_uid}", - via_device=(DOMAIN, entry.data[CONF_UID]), + via_device_id=dr.async_get_device_id_by_identifier( + coordinator.hass, + (DOMAIN, entry.data[CONF_UID]), + config_entry_id=entry.entry_id, + ), ) self._attr_unique_id = f"{paired_sensor_uid}_{description.key}" diff --git a/tests/components/guardian/conftest.py b/tests/components/guardian/conftest.py index 61813cb1df55..2b9fd3aff814 100644 --- a/tests/components/guardian/conftest.py +++ b/tests/components/guardian/conftest.py @@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, patch import pytest from homeassistant.components.guardian import CONF_UID, DOMAIN -from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT +from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT, Platform from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.json import JsonObjectType @@ -97,10 +97,17 @@ def data_wifi_status_fixture() -> JsonObjectType: return load_json_object_fixture("wifi_status_data.json", "guardian") +@pytest.fixture(name="platforms") +def platforms_fixture() -> list[Platform]: + """Define a platforms fixture.""" + return [] + + @pytest.fixture(name="setup_guardian") async def setup_guardian_fixture( hass: HomeAssistant, config: dict[str, Any], + platforms: list[Platform], data_sensor_pair_dump: JsonObjectType, data_sensor_pair_sensor: JsonObjectType, data_sensor_paired_sensor_status: JsonObjectType, @@ -150,7 +157,7 @@ async def setup_guardian_fixture( ), patch( "homeassistant.components.guardian.PLATFORMS", - [], + platforms, ), ): assert await async_setup_component(hass, DOMAIN, config) diff --git a/tests/components/guardian/test_init.py b/tests/components/guardian/test_init.py new file mode 100644 index 000000000000..e2550a6a5dc8 --- /dev/null +++ b/tests/components/guardian/test_init.py @@ -0,0 +1,45 @@ +"""Test the Elexa Guardian init module.""" + +from datetime import timedelta + +from freezegun.api import FrozenDateTimeFactory +import pytest + +from homeassistant.components.guardian import CONF_UID, DOMAIN +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from tests.common import MockConfigEntry, async_fire_time_changed + + +@pytest.fixture(name="platforms") +def platforms_fixture() -> list[Platform]: + """Override to exercise the paired sensor entity setup.""" + return [Platform.SENSOR] + + +async def test_paired_sensor_via_device_id( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + device_registry: dr.DeviceRegistry, + config_entry: MockConfigEntry, + setup_guardian: None, # relies on config_entry fixture +) -> None: + """Test that a paired sensor is linked to the valve controller via via_device_id.""" + # Advance time so the sensor pair dump coordinator refreshes and discovers the + # paired sensor from the fixture data: + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + valve_controller_device = device_registry.async_get_device_by_identifier( + (DOMAIN, config_entry.data[CONF_UID]), config_entry.entry_id + ) + paired_sensor_device = device_registry.async_get_device_by_identifier( + (DOMAIN, "AABBCCDDEEFF"), config_entry.entry_id + ) + + assert valve_controller_device is not None + assert paired_sensor_device is not None + assert paired_sensor_device.via_device_id == valve_controller_device.id