diff --git a/homeassistant/components/victron_gx/hub.py b/homeassistant/components/victron_gx/hub.py index ebc81ae65285..0d6d54cd379b 100644 --- a/homeassistant/components/victron_gx/hub.py +++ b/homeassistant/components/victron_gx/hub.py @@ -28,7 +28,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.redact import async_redact_data from .const import CONF_INSTALLATION_ID, CONF_SERIAL, DOMAIN @@ -40,7 +40,7 @@ TO_REDACT = {CONF_USERNAME, CONF_PASSWORD} type VictronGxConfigEntry = ConfigEntry[Hub] type NewMetricCallback = Callable[ - [VictronVenusDevice, VictronVenusMetric, DeviceInfo, str], None + [VictronVenusDevice, VictronVenusMetric, dr.DeviceInfo, str], None ] @@ -64,6 +64,8 @@ class Hub: config = {**entry.data, **entry.options} self.hass = hass self.host = config[CONF_HOST] + self._config_entry_id = entry.entry_id + self._device_registry = dr.async_get(hass) self._hub = VictronVenusHub( host=self.host, @@ -120,15 +122,39 @@ class Hub: if TYPE_CHECKING: assert hub.installation_id is not None device_info = Hub._map_device_info(device, hub.installation_id) + if device.parent_device is not None: + device_info["via_device_id"] = self._ensure_device_registered( + device.parent_device, hub.installation_id + ) callback = self.new_metric_callbacks.get(metric.metric_kind) if callback is not None: callback(device, metric, device_info, hub.installation_id) + def _ensure_device_registered( + self, device: VictronVenusDevice, installation_id: str + ) -> str: + """Register a device and its ancestors (parents first), returning its id. + + Devices are discovered lazily as their metrics arrive, so a child's parent + may not be in the registry yet; registering the ancestor chain here lets + children link to it via via_device_id instead of the deprecated via_device. + """ + device_info = Hub._map_device_info(device, installation_id) + if device.parent_device is not None: + device_info["via_device_id"] = self._ensure_device_registered( + device.parent_device, installation_id + ) + device_entry = self._device_registry.async_get_or_create( + config_entry_id=self._config_entry_id, + **device_info, + ) + return device_entry.id + @staticmethod def _map_device_info( device: VictronVenusDevice, installation_id: str - ) -> DeviceInfo: - device_info = DeviceInfo( + ) -> dr.DeviceInfo: + return dr.DeviceInfo( identifiers={(DOMAIN, f"{installation_id}_{device.unique_id}")}, manufacturer=( device.manufacturer @@ -139,13 +165,6 @@ class Hub: model=device.model, serial_number=device.serial_number, ) - # Set via_device based on parent_device relationship - if device.parent_device is not None: - device_info["via_device"] = ( - DOMAIN, - f"{installation_id}_{device.parent_device.unique_id}", - ) - return device_info def is_device_connected(self, device_identifiers: set[tuple[str, str]]) -> bool: """Check if a device is currently known to the hub.""" diff --git a/tests/components/victron_gx/test_init.py b/tests/components/victron_gx/test_init.py index da5c0ced7d32..0a5bd2975710 100644 --- a/tests/components/victron_gx/test_init.py +++ b/tests/components/victron_gx/test_init.py @@ -154,13 +154,13 @@ async def test_hub_start_success( assert victron_hub.installation_id == MOCK_INSTALLATION_ID -async def test_child_device_via_device_links_to_parent_in_registry( +async def test_device_via_device_links( hass: HomeAssistant, init_integration: tuple[VictronVenusHub, MockConfigEntry], device_registry: dr.DeviceRegistry, ) -> None: - """Test non-root device is linked to its parent device in the HA device registry.""" - victron_hub, _mock_config_entry = init_integration + """Test a child device links to its registered parent via via_device_id.""" + victron_hub, mock_config_entry = init_integration # Inject a system metric first so system_0 is registered as the gateway device. await inject_message( @@ -177,15 +177,15 @@ async def test_child_device_via_device_links_to_parent_in_registry( await finalize_injection(victron_hub) await hass.async_block_till_done() - system_device = device_registry.async_get_device( - identifiers={(DOMAIN, f"{MOCK_INSTALLATION_ID}_system_0")} + system_device = device_registry.async_get_device_by_identifier( + (DOMAIN, f"{MOCK_INSTALLATION_ID}_system_0"), mock_config_entry.entry_id ) assert system_device is not None # The GX gateway has no parent — it IS the root. assert system_device.via_device_id is None - battery_device = device_registry.async_get_device( - identifiers={(DOMAIN, f"{MOCK_INSTALLATION_ID}_battery_0")} + battery_device = device_registry.async_get_device_by_identifier( + (DOMAIN, f"{MOCK_INSTALLATION_ID}_battery_0"), mock_config_entry.entry_id ) assert battery_device is not None # Battery is a child of the GX gateway, not an orphan.