diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 7fb96fdf7c8..c699d945e18 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -33,6 +33,7 @@ class DataUpdateCoordinator: update_interval: timedelta, update_method: Optional[Callable[[], Awaitable]] = None, request_refresh_debouncer: Optional[Debouncer] = None, + failed_update_interval: Optional[timedelta] = None, ): """Initialize global data updater.""" self.hass = hass @@ -40,13 +41,17 @@ class DataUpdateCoordinator: self.name = name self.update_method = update_method self.update_interval = update_interval - + if failed_update_interval: + self.failed_update_interval = failed_update_interval + else: + self.failed_update_interval = update_interval self.data: Optional[Any] = None self._listeners: List[CALLBACK_TYPE] = [] self._unsub_refresh: Optional[CALLBACK_TYPE] = None self._request_refresh_task: Optional[asyncio.TimerHandle] = None self.last_update_success = True + self.last_update_success_time = None # type: Optional[datetime] if request_refresh_debouncer is None: request_refresh_debouncer = Debouncer( @@ -99,10 +104,14 @@ class DataUpdateCoordinator: # minimizing the time between the point and the real activation. # That way we obtain a constant update frequency, # as long as the update process takes less than a second + if self.last_update_success: + update_interval = self.update_interval + else: + update_interval = self.failed_update_interval self._unsub_refresh = async_track_point_in_utc_time( self.hass, self._handle_refresh_interval, - utcnow().replace(microsecond=0) + self.update_interval, + utcnow().replace(microsecond=0) + update_interval, ) async def _handle_refresh_interval(self, _now: datetime) -> None: @@ -163,6 +172,7 @@ class DataUpdateCoordinator: if not self.last_update_success: self.last_update_success = True self.logger.info("Fetching %s data recovered", self.name) + self.last_update_success_time = utcnow() finally: self.logger.debug( diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 8d4f6934d78..c161f0b2a66 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -9,7 +9,7 @@ import pytest from homeassistant.helpers import update_coordinator from homeassistant.util.dt import utcnow -from tests.async_mock import AsyncMock, Mock +from tests.async_mock import AsyncMock, Mock, patch from tests.common import async_fire_time_changed LOGGER = logging.getLogger(__name__) @@ -37,12 +37,16 @@ def crd(hass): async def test_async_refresh(crd): """Test async_refresh for update coordinator.""" - assert crd.data is None - await crd.async_refresh() - assert crd.data == 1 - assert crd.last_update_success is True - # Make sure we didn't schedule a refresh because we have 0 listeners - assert crd._unsub_refresh is None + utc_time = utcnow() + with patch("homeassistant.helpers.update_coordinator.utcnow") as mock_utc: + mock_utc.return_value = utc_time + assert crd.data is None + await crd.async_refresh() + assert crd.data == 1 + assert crd.last_update_success is True + assert crd.last_update_success_time == utc_time + # Make sure we didn't schedule a refresh because we have 0 listeners + assert crd._unsub_refresh is None updates = [] @@ -124,31 +128,70 @@ async def test_refresh_no_update_method(crd): async def test_update_interval(hass, crd): """Test update interval works.""" # Test we don't update without subscriber - async_fire_time_changed(hass, utcnow() + crd.update_interval) - await hass.async_block_till_done() - assert crd.data is None + utc_time = utcnow() + with patch("homeassistant.helpers.update_coordinator.utcnow") as mock_utc: + mock_utc.return_value = utc_time + crd.update_interval + async_fire_time_changed(hass, mock_utc.return_value) + await hass.async_block_till_done() + assert crd.data is None + assert crd.last_update_success_time is None - # Add subscriber - update_callback = Mock() - crd.async_add_listener(update_callback) + # Add subscriber + update_callback = Mock() + crd.async_add_listener(update_callback) - # Test twice we update with subscriber - async_fire_time_changed(hass, utcnow() + crd.update_interval) - await hass.async_block_till_done() - assert crd.data == 1 + # Test twice we update with subscriber + mock_utc.return_value += crd.update_interval + async_fire_time_changed(hass, mock_utc.return_value) + await hass.async_block_till_done() + assert crd.data == 1 + assert crd.last_update_success_time == mock_utc.return_value - async_fire_time_changed(hass, utcnow() + crd.update_interval) - await hass.async_block_till_done() - assert crd.data == 2 + mock_utc.return_value += crd.update_interval + async_fire_time_changed(hass, mock_utc.return_value) + await hass.async_block_till_done() + assert crd.data == 2 + assert crd.last_update_success_time == mock_utc.return_value + last_success_time = mock_utc.return_value - # Test removing listener - crd.async_remove_listener(update_callback) + # Test removing listener + crd.async_remove_listener(update_callback) - async_fire_time_changed(hass, utcnow() + crd.update_interval) - await hass.async_block_till_done() + mock_utc.return_value += crd.update_interval + async_fire_time_changed(hass, mock_utc.return_value) + await hass.async_block_till_done() - # Test we stop updating after we lose last subscriber - assert crd.data == 2 + # Test we stop updating after we lose last subscriber + assert crd.data == 2 + assert crd.last_update_success_time == last_success_time + + +async def test_failed_update_interval(crd, hass): + """Test failed update interval.""" + utc_time = utcnow() + with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: + mock_utcnow.return_value = utc_time + old_update_method = crd.update_method + crd.update_method = AsyncMock(side_effect=asyncio.TimeoutError) + crd.failed_update_interval = timedelta(seconds=5) + + def update_callback(): + pass + + crd.async_add_listener(update_callback) + + await crd.async_refresh() + await hass.async_block_till_done() + + assert crd.data is None + assert crd.last_update_success is False + + crd.update_method = old_update_method + mock_utcnow.return_value += timedelta(seconds=5) + async_fire_time_changed(hass, mock_utcnow.return_value) + await hass.async_block_till_done() + assert crd.data == 1 + assert crd.last_update_success is True async def test_refresh_recover(crd, caplog):