From 1cd34e84779ada0ef6bfa736e72a42deb9a0c3ce Mon Sep 17 00:00:00 2001 From: Tomer <57483589+tomer-w@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:06:13 +0300 Subject: [PATCH] Victron GX stale devices (#168706) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Joost Lekkerkerker --- .../components/victron_gx/__init__.py | 11 +++++ homeassistant/components/victron_gx/hub.py | 9 ++++ .../components/victron_gx/quality_scale.yaml | 2 +- tests/components/victron_gx/test_init.py | 44 +++++++++++++++++++ 4 files changed, 65 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/victron_gx/__init__.py b/homeassistant/components/victron_gx/__init__.py index 1b02faa14101..76f6e5f9dccc 100644 --- a/homeassistant/components/victron_gx/__init__.py +++ b/homeassistant/components/victron_gx/__init__.py @@ -6,6 +6,7 @@ import logging from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import Event, HomeAssistant +from homeassistant.helpers import device_registry as dr from .hub import Hub, VictronGxConfigEntry @@ -65,3 +66,13 @@ async def async_unload_entry(hass: HomeAssistant, entry: VictronGxConfigEntry) - hub.unregister_all_new_metric_callbacks() return unload_ok + + +async def async_remove_config_entry_device( + hass: HomeAssistant, + config_entry: VictronGxConfigEntry, + device_entry: dr.DeviceEntry, +) -> bool: + """Remove a device from the config entry if the device is no longer known.""" + hub: Hub = config_entry.runtime_data + return not hub.is_device_connected(device_entry.identifiers) diff --git a/homeassistant/components/victron_gx/hub.py b/homeassistant/components/victron_gx/hub.py index 5e6b579bac0e..9de6c78859af 100644 --- a/homeassistant/components/victron_gx/hub.py +++ b/homeassistant/components/victron_gx/hub.py @@ -142,6 +142,15 @@ class Hub: device_info["via_device"] = (DOMAIN, f"{installation_id}_system_0") 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.""" + known_devices = self._hub.devices + return any( + identifier[1].removeprefix(f"{self._hub.installation_id}_") in known_devices + for identifier in device_identifiers + if identifier[0] == DOMAIN + ) + def get_diagnostics_data(self) -> dict[str, Any]: """Return diagnostics data for the hub's device and entity tree.""" return { diff --git a/homeassistant/components/victron_gx/quality_scale.yaml b/homeassistant/components/victron_gx/quality_scale.yaml index 0aec6d676e4c..9f4d8900dcaf 100644 --- a/homeassistant/components/victron_gx/quality_scale.yaml +++ b/homeassistant/components/victron_gx/quality_scale.yaml @@ -64,7 +64,7 @@ rules: Not relevant. reconfiguration-flow: todo repair-issues: todo - stale-devices: todo + stale-devices: done # Platinum async-dependency: done diff --git a/tests/components/victron_gx/test_init.py b/tests/components/victron_gx/test_init.py index 4b54f79c1bc7..961a5b76d73c 100644 --- a/tests/components/victron_gx/test_init.py +++ b/tests/components/victron_gx/test_init.py @@ -9,10 +9,14 @@ from victron_mqtt import ( Hub as VictronVenusHub, MetricKind, ) +from victron_mqtt.testing import finalize_injection, inject_message +from homeassistant.components.victron_gx import async_remove_config_entry_device +from homeassistant.components.victron_gx.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr from .const import MOCK_INSTALLATION_ID @@ -210,3 +214,43 @@ async def test_hub_stop_disconnect_error( await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_remove_config_entry_device( + hass: HomeAssistant, + init_integration: tuple[VictronVenusHub, MockConfigEntry], + device_registry: dr.DeviceRegistry, +) -> None: + """Test removing a device from the config entry.""" + victron_hub, mock_config_entry = init_integration + + # A device that was never discovered should be removable + device_entry = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={(DOMAIN, f"{MOCK_INSTALLATION_ID}_test_device")}, + ) + + result = await async_remove_config_entry_device( + hass, mock_config_entry, device_entry + ) + assert result is True + + # Inject a sensor to make battery_0 a known device + await inject_message( + victron_hub, + f"N/{MOCK_INSTALLATION_ID}/battery/0/Dc/0/Current", + '{"value": 10.5}', + ) + await finalize_injection(victron_hub) + await hass.async_block_till_done() + + # A device that is currently connected should NOT be removable + connected_device = device_registry.async_get_or_create( + config_entry_id=mock_config_entry.entry_id, + identifiers={(DOMAIN, f"{MOCK_INSTALLATION_ID}_battery_0")}, + ) + + result = await async_remove_config_entry_device( + hass, mock_config_entry, connected_device + ) + assert result is False