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.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady 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 homeassistant.helpers.redact import async_redact_data
from .const import CONF_INSTALLATION_ID, CONF_SERIAL, DOMAIN from .const import CONF_INSTALLATION_ID, CONF_SERIAL, DOMAIN
@@ -40,7 +40,7 @@ TO_REDACT = {CONF_USERNAME, CONF_PASSWORD}
type VictronGxConfigEntry = ConfigEntry[Hub] type VictronGxConfigEntry = ConfigEntry[Hub]
type NewMetricCallback = Callable[ 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} config = {**entry.data, **entry.options}
self.hass = hass self.hass = hass
self.host = config[CONF_HOST] self.host = config[CONF_HOST]
self._config_entry_id = entry.entry_id
self._device_registry = dr.async_get(hass)
self._hub = VictronVenusHub( self._hub = VictronVenusHub(
host=self.host, host=self.host,
@@ -120,15 +122,39 @@ class Hub:
if TYPE_CHECKING: if TYPE_CHECKING:
assert hub.installation_id is not None assert hub.installation_id is not None
device_info = Hub._map_device_info(device, hub.installation_id) 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) callback = self.new_metric_callbacks.get(metric.metric_kind)
if callback is not None: if callback is not None:
callback(device, metric, device_info, hub.installation_id) 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 @staticmethod
def _map_device_info( def _map_device_info(
device: VictronVenusDevice, installation_id: str device: VictronVenusDevice, installation_id: str
) -> DeviceInfo: ) -> dr.DeviceInfo:
device_info = DeviceInfo( return dr.DeviceInfo(
identifiers={(DOMAIN, f"{installation_id}_{device.unique_id}")}, identifiers={(DOMAIN, f"{installation_id}_{device.unique_id}")},
manufacturer=( manufacturer=(
device.manufacturer device.manufacturer
@@ -139,13 +165,6 @@ class Hub:
model=device.model, model=device.model,
serial_number=device.serial_number, 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: def is_device_connected(self, device_identifiers: set[tuple[str, str]]) -> bool:
"""Check if a device is currently known to the hub.""" """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 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, hass: HomeAssistant,
init_integration: tuple[VictronVenusHub, MockConfigEntry], init_integration: tuple[VictronVenusHub, MockConfigEntry],
device_registry: dr.DeviceRegistry, device_registry: dr.DeviceRegistry,
) -> None: ) -> None:
"""Test non-root device is linked to its parent device in the HA device registry.""" """Test a child device links to its registered parent via via_device_id."""
victron_hub, _mock_config_entry = init_integration victron_hub, mock_config_entry = init_integration
# Inject a system metric first so system_0 is registered as the gateway device. # Inject a system metric first so system_0 is registered as the gateway device.
await inject_message( 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 finalize_injection(victron_hub)
await hass.async_block_till_done() await hass.async_block_till_done()
system_device = device_registry.async_get_device( system_device = device_registry.async_get_device_by_identifier(
identifiers={(DOMAIN, f"{MOCK_INSTALLATION_ID}_system_0")} (DOMAIN, f"{MOCK_INSTALLATION_ID}_system_0"), mock_config_entry.entry_id
) )
assert system_device is not None assert system_device is not None
# The GX gateway has no parent — it IS the root. # The GX gateway has no parent — it IS the root.
assert system_device.via_device_id is None assert system_device.via_device_id is None
battery_device = device_registry.async_get_device( battery_device = device_registry.async_get_device_by_identifier(
identifiers={(DOMAIN, f"{MOCK_INSTALLATION_ID}_battery_0")} (DOMAIN, f"{MOCK_INSTALLATION_ID}_battery_0"), mock_config_entry.entry_id
) )
assert battery_device is not None assert battery_device is not None
# Battery is a child of the GX gateway, not an orphan. # Battery is a child of the GX gateway, not an orphan.