diff --git a/homeassistant/components/hydrawise/__init__.py b/homeassistant/components/hydrawise/__init__.py index 03cb40fecce9..50b005d2d409 100644 --- a/homeassistant/components/hydrawise/__init__.py +++ b/homeassistant/components/hydrawise/__init__.py @@ -3,10 +3,11 @@ from pydrawise import auth, hybrid from homeassistant.const import CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers import device_registry as dr -from .const import APP_ID +from .const import APP_ID, DOMAIN, MANUFACTURER from .coordinator import ( HydrawiseConfigEntry, HydrawiseMainDataUpdateCoordinator, @@ -46,6 +47,31 @@ async def async_setup_entry( water_use_coordinator = HydrawiseWaterUseDataUpdateCoordinator( hass, config_entry, hydrawise, main_coordinator ) + + device_registry = dr.async_get(hass) + + @callback + def _async_register_controller_devices() -> None: + """Register controller devices so children can resolve via_device_id. + + Controllers can appear on later coordinator updates, so this runs on + every update before the zone-tracking listener, keeping the via_device + parents registered before their child entities are constructed. + """ + for controller in main_coordinator.data.controllers.values(): + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, str(controller.id))}, + manufacturer=MANUFACTURER, + model=controller.hardware.model.description, + name=controller.name, + ) + + _async_register_controller_devices() + config_entry.async_on_unload( + main_coordinator.async_add_listener(_async_register_controller_devices) + ) + # async_track_zones is registered first on water_use_coordinator, # so the water-use coordinator's data is in sync before # callbacks below construct entities for newly added zones. diff --git a/homeassistant/components/hydrawise/entity.py b/homeassistant/components/hydrawise/entity.py index 53ddaa3d0b7f..7d88d4314e94 100644 --- a/homeassistant/components/hydrawise/entity.py +++ b/homeassistant/components/hydrawise/entity.py @@ -5,6 +5,7 @@ from typing import override from pydrawise.schema import Controller, Sensor, Zone from homeassistant.core import callback +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 @@ -47,7 +48,13 @@ class HydrawiseEntity(CoordinatorEntity[HydrawiseDataUpdateCoordinator]): manufacturer=MANUFACTURER, ) if zone_id is not None or sensor_id is not None: - self._attr_device_info["via_device"] = (DOMAIN, str(controller.id)) + self._attr_device_info["via_device_id"] = ( + dr.async_get_device_id_by_identifier( + self.coordinator.hass, + (DOMAIN, str(controller.id)), + config_entry_id=self.coordinator.config_entry.entry_id, + ) + ) self._update_attrs() @property diff --git a/tests/components/hydrawise/test_device.py b/tests/components/hydrawise/test_device.py index 9d98f2a7b442..1aec2c3a0aa0 100644 --- a/tests/components/hydrawise/test_device.py +++ b/tests/components/hydrawise/test_device.py @@ -2,6 +2,8 @@ from unittest.mock import Mock +import pytest + from homeassistant.components.hydrawise.const import DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -38,3 +40,21 @@ def test_controller_in_device_registry( assert device is not None assert device.name == "Home Controller" assert device.manufacturer == "Hydrawise" + + +@pytest.mark.usefixtures("mock_pydrawise") +def test_zone_via_device_links_to_controller( + device_registry: dr.DeviceRegistry, + mock_added_config_entry: ConfigEntry, +) -> None: + """Test that a zone device links to its controller via via_device_id.""" + controller = device_registry.async_get_device_by_identifier( + (DOMAIN, "52496"), mock_added_config_entry.entry_id + ) + assert controller is not None + + zone = device_registry.async_get_device_by_identifier( + (DOMAIN, "5965394"), mock_added_config_entry.entry_id + ) + assert zone is not None + assert zone.via_device_id == controller.id