From 809070d2ada8adfaac6e12934db44166c6c158d8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 6 Oct 2025 21:31:55 +0200 Subject: [PATCH] Catch update exception in AirGradient (#153828) --- .../components/airgradient/update.py | 29 +++++++-- tests/components/airgradient/test_update.py | 65 ++++++++++++++++++- 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/airgradient/update.py b/homeassistant/components/airgradient/update.py index 97cb8576e794..3f2422078d49 100644 --- a/homeassistant/components/airgradient/update.py +++ b/homeassistant/components/airgradient/update.py @@ -1,7 +1,9 @@ """Airgradient Update platform.""" from datetime import timedelta +import logging +from airgradient import AirGradientConnectionError from propcache.api import cached_property from homeassistant.components.update import UpdateDeviceClass, UpdateEntity @@ -13,6 +15,7 @@ from .entity import AirGradientEntity PARALLEL_UPDATES = 1 SCAN_INTERVAL = timedelta(hours=1) +_LOGGER = logging.getLogger(__name__) async def async_setup_entry( @@ -31,6 +34,7 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity): """Representation of Airgradient Update.""" _attr_device_class = UpdateDeviceClass.FIRMWARE + _server_unreachable_logged = False def __init__(self, coordinator: AirGradientCoordinator) -> None: """Initialize the entity.""" @@ -47,10 +51,27 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity): """Return the installed version of the entity.""" return self.coordinator.data.measures.firmware_version + @property + def available(self) -> bool: + """Return if entity is available.""" + return super().available and self._attr_available + async def async_update(self) -> None: """Update the entity.""" - self._attr_latest_version = ( - await self.coordinator.client.get_latest_firmware_version( - self.coordinator.serial_number + try: + self._attr_latest_version = ( + await self.coordinator.client.get_latest_firmware_version( + self.coordinator.serial_number + ) ) - ) + except AirGradientConnectionError: + self._attr_latest_version = None + self._attr_available = False + if not self._server_unreachable_logged: + _LOGGER.error( + "Unable to connect to AirGradient server to check for updates" + ) + self._server_unreachable_logged = True + else: + self._server_unreachable_logged = False + self._attr_available = True diff --git a/tests/components/airgradient/test_update.py b/tests/components/airgradient/test_update.py index 65614312b464..1ef2122f9484 100644 --- a/tests/components/airgradient/test_update.py +++ b/tests/components/airgradient/test_update.py @@ -3,10 +3,12 @@ from datetime import timedelta from unittest.mock import AsyncMock, patch +from airgradient import AirGradientConnectionError from freezegun.api import FrozenDateTimeFactory +import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.const import STATE_OFF, STATE_ON, Platform +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -67,3 +69,64 @@ async def test_update_mechanism( assert state.state == STATE_ON assert state.attributes["installed_version"] == "3.1.4" assert state.attributes["latest_version"] == "3.1.5" + + +async def test_update_errors( + hass: HomeAssistant, + mock_airgradient_client: AsyncMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test update entity errors.""" + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("update.airgradient_firmware") + assert state.state == STATE_ON + mock_airgradient_client.get_latest_firmware_version.side_effect = ( + AirGradientConnectionError("Boom") + ) + + freezer.tick(timedelta(hours=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("update.airgradient_firmware") + assert state.state == STATE_UNAVAILABLE + + assert "Unable to connect to AirGradient server to check for updates" in caplog.text + + caplog.clear() + + freezer.tick(timedelta(hours=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("update.airgradient_firmware") + assert state.state == STATE_UNAVAILABLE + + assert ( + "Unable to connect to AirGradient server to check for updates" + not in caplog.text + ) + + mock_airgradient_client.get_latest_firmware_version.side_effect = None + + freezer.tick(timedelta(hours=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("update.airgradient_firmware") + assert state.state == STATE_ON + mock_airgradient_client.get_latest_firmware_version.side_effect = ( + AirGradientConnectionError("Boom") + ) + + freezer.tick(timedelta(hours=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("update.airgradient_firmware") + assert state.state == STATE_UNAVAILABLE + + assert "Unable to connect to AirGradient server to check for updates" in caplog.text