From fee52805abcd8a2b57ec4f7791e724a371508f47 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 19 Sep 2026 11:24:03 +0200 Subject: [PATCH] Handle unavailable UniFi configuration endpoint (#182574) --- homeassistant/components/unifi/coordinator.py | 24 +++++++++- .../components/unifi/hub/entity_loader.py | 4 +- tests/components/unifi/test_hub.py | 44 +++++++++++++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/coordinator.py b/homeassistant/components/unifi/coordinator.py index cca4293d7566..daee12d4f157 100644 --- a/homeassistant/components/unifi/coordinator.py +++ b/homeassistant/components/unifi/coordinator.py @@ -3,10 +3,11 @@ from datetime import timedelta from typing import TYPE_CHECKING, override +from aiounifi import EndpointNotFound from aiounifi.interfaces.api_handlers import APIHandler, ItemEvent from homeassistant.core import callback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import LOGGER @@ -25,6 +26,8 @@ class UnifiDataUpdateCoordinator[HandlerT: APIHandler]( self, hub: UnifiHub, handler: HandlerT, + *, + disable_polling_on_endpoint_not_found: bool = False, ) -> None: """Initialize coordinator.""" supports_websocket = bool(handler.process_messages or handler.remove_messages) @@ -36,6 +39,10 @@ class UnifiDataUpdateCoordinator[HandlerT: APIHandler]( update_interval=None if supports_websocket else POLL_INTERVAL, ) self._handler = handler + self._disable_polling_on_endpoint_not_found = ( + disable_polling_on_endpoint_not_found + ) + self._endpoint_not_found_logged = False hub.config.entry.async_on_unload(handler.subscribe(self._async_handle_update)) @@ -47,7 +54,20 @@ class UnifiDataUpdateCoordinator[HandlerT: APIHandler]( @override async def _async_update_data(self) -> None: """Update data from the API handler.""" - await self._handler.update() + try: + await self._handler.update() + except EndpointNotFound as err: + if ( + self._disable_polling_on_endpoint_not_found + and not self._endpoint_not_found_logged + ): + self._endpoint_not_found_logged = True + self.update_interval = None + self.logger.warning( + "UniFi %s endpoint is unavailable; disabling polling", + type(self._handler).__name__, + ) + raise UpdateFailed(str(err)) from err @callback def _async_handle_update(self, event: ItemEvent, obj_id: str) -> None: diff --git a/homeassistant/components/unifi/hub/entity_loader.py b/homeassistant/components/unifi/hub/entity_loader.py index e38db332d659..462df0f86d13 100644 --- a/homeassistant/components/unifi/hub/entity_loader.py +++ b/homeassistant/components/unifi/hub/entity_loader.py @@ -53,7 +53,9 @@ class UnifiEntityLoader: hub, hub.api.firewall_policies ), id(hub.api.object_oriented_network_configs): UnifiDataUpdateCoordinator( - hub, hub.api.object_oriented_network_configs + hub, + hub.api.object_oriented_network_configs, + disable_polling_on_endpoint_not_found=True, ), id(hub.api.port_forwarding): UnifiDataUpdateCoordinator( hub, hub.api.port_forwarding diff --git a/tests/components/unifi/test_hub.py b/tests/components/unifi/test_hub.py index ab3821fdb851..d1242cf03870 100644 --- a/tests/components/unifi/test_hub.py +++ b/tests/components/unifi/test_hub.py @@ -6,6 +6,7 @@ from typing import Any from unittest.mock import patch import aiounifi +from aiounifi import EndpointNotFound from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.models.message import MessageKey import pytest @@ -122,6 +123,49 @@ async def test_polling_coordinator_refreshes_after_interval( assert mock_update.call_count >= 1 +async def test_endpoint_not_found_disables_object_oriented_network_config_polling( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + config_entry_setup: MockConfigEntry, +) -> None: + """Ensure an unavailable optional endpoint stops polling after one warning.""" + loader = config_entry_setup.runtime_data.entity_loader + api = config_entry_setup.runtime_data.api + coordinator = loader.get_data_update_coordinator( + api.object_oriented_network_configs + ) + traffic_rules_coordinator = loader.get_data_update_coordinator(api.traffic_rules) + + with patch.object( + coordinator.handler, + "update", + side_effect=EndpointNotFound("endpoint not found"), + ) as mock_update: + await coordinator.async_refresh() + await hass.async_block_till_done() + + assert coordinator.update_interval is None + assert coordinator.last_update_success is False + assert mock_update.call_count == 1 + + async_fire_time_changed(hass, dt_util.utcnow() + POLL_INTERVAL) + await hass.async_block_till_done() + + assert mock_update.call_count == 1 + + await coordinator.async_refresh() + await hass.async_block_till_done() + + assert mock_update.call_count == 2 + assert traffic_rules_coordinator.update_interval == POLL_INTERVAL + assert ( + caplog.text.count( + "UniFi ObjectOrientedNetworkConfigs endpoint is unavailable; disabling polling" + ) + == 1 + ) + + @pytest.mark.parametrize( "object_oriented_network_config_payload", [