From c9e7abba18e85adabc39e3f1d50d529c1bc11f68 Mon Sep 17 00:00:00 2001 From: Christian Lackas Date: Fri, 8 May 2026 13:32:05 +0200 Subject: [PATCH] Skip offline devices instead of aborting ViCare diagnostics A single device with an offline gateway raises PyViCareDeviceCommunicationError from dump_secure() and aborts the entire diagnostics download with a 500. Wrap each device dump in try/except and emit a placeholder entry (id, modelId, type, status, roles, error) so the rest of the devices can still be inspected. --- .../components/vicare/diagnostics.py | 27 ++++++++++++++---- tests/components/vicare/test_diagnostics.py | 28 +++++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/vicare/diagnostics.py b/homeassistant/components/vicare/diagnostics.py index f3f99132398bd..008c533b430ba 100644 --- a/homeassistant/components/vicare/diagnostics.py +++ b/homeassistant/components/vicare/diagnostics.py @@ -3,6 +3,8 @@ import json from typing import Any +from PyViCare.PyViCareUtils import PyViCareDeviceCommunicationError + from homeassistant.components.diagnostics import async_redact_data from homeassistant.const import ( CONF_ACCESS_TOKEN, @@ -30,11 +32,26 @@ async def async_get_config_entry_diagnostics( """Return diagnostics for a config entry.""" def dump_devices() -> list[dict[str, Any]]: - """Dump devices.""" - return [ - json.loads(device.dump_secure()) - for device in entry.runtime_data.client.all_devices - ] + """Dump devices, tolerating per-device communication failures.""" + devices: list[dict[str, Any]] = [] + for device in entry.runtime_data.client.all_devices: + try: + devices.append(json.loads(device.dump_secure())) + except PyViCareDeviceCommunicationError as err: + # One offline gateway must not abort the whole diagnostics dump. + devices.append( + { + "device": { + "id": device.device_id, + "modelId": device.device_model, + "type": device.device_type, + "status": device.status, + "roles": device.roles, + }, + "error": f"{type(err).__name__}: {err}", + } + ) + return devices return { "entry": async_redact_data(entry.as_dict(), TO_REDACT), diff --git a/tests/components/vicare/test_diagnostics.py b/tests/components/vicare/test_diagnostics.py index 6adf4fe0edcd2..7c25fa8a8bdac 100644 --- a/tests/components/vicare/test_diagnostics.py +++ b/tests/components/vicare/test_diagnostics.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock +from PyViCare.PyViCareUtils import PyViCareDeviceCommunicationError from syrupy.assertion import SnapshotAssertion from syrupy.filters import props @@ -23,3 +24,30 @@ async def test_diagnostics( ) assert diag == snapshot(exclude=props("created_at", "modified_at")) + + +async def test_diagnostics_with_offline_device( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_vicare_gas_boiler: MagicMock, +) -> None: + """Test that an offline gateway on one device does not abort diagnostics.""" + config_entry = hass.config_entries.async_entries("vicare")[0] + devices = config_entry.runtime_data.client.all_devices + + # Force the first device to fail with GATEWAY_OFFLINE; the rest must still dump. + devices[0].dump_secure = MagicMock( + side_effect=PyViCareDeviceCommunicationError( + {"extendedPayload": {"reason": "GATEWAY_OFFLINE"}} + ) + ) + + diag = await get_diagnostics_for_config_entry( + hass, hass_client, mock_vicare_gas_boiler + ) + + assert len(diag["data"]) == len(devices) + error_entry = diag["data"][0] + assert "error" in error_entry + assert "GATEWAY_OFFLINE" in error_entry["error"] + assert error_entry["device"]["id"] == devices[0].device_id