Fix via_device race in victron_gx

This commit is contained in:
Erik
2026-08-03 13:16:26 +02:00
parent 82ee383dc4
commit 1485808da2
2 changed files with 37 additions and 18 deletions
+30 -11
View File
@@ -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."""
+7 -7
View File
@@ -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.