mirror of
https://github.com/home-assistant/core.git
synced 2026-08-03 20:24:55 +02:00
Fix via_device race in victron_gx
This commit is contained in:
@@ -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."""
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user