Avoid firing update coordinator callbacks when nothing has changed (#97268)
parent
3a54448836
commit
13349e76ed
|
@ -54,7 +54,12 @@ class BaseDataUpdateCoordinatorProtocol(Protocol):
|
|||
|
||||
|
||||
class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
|
||||
"""Class to manage fetching data from single endpoint."""
|
||||
"""Class to manage fetching data from single endpoint.
|
||||
|
||||
Setting :attr:`always_update` to ``False`` will cause coordinator to only
|
||||
callback listeners when data has changed. This requires that the data
|
||||
implements ``__eq__`` or uses a python object that already does.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -65,6 +70,7 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
|
|||
update_interval: timedelta | None = None,
|
||||
update_method: Callable[[], Awaitable[_DataT]] | None = None,
|
||||
request_refresh_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None,
|
||||
always_update: bool = True,
|
||||
) -> None:
|
||||
"""Initialize global data updater."""
|
||||
self.hass = hass
|
||||
|
@ -74,6 +80,7 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
|
|||
self.update_interval = update_interval
|
||||
self._shutdown_requested = False
|
||||
self.config_entry = config_entries.current_entry.get()
|
||||
self.always_update = always_update
|
||||
|
||||
# It's None before the first successful update.
|
||||
# Components should call async_config_entry_first_refresh
|
||||
|
@ -277,7 +284,10 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
|
|||
|
||||
if log_timing := self.logger.isEnabledFor(logging.DEBUG):
|
||||
start = monotonic()
|
||||
|
||||
auth_failed = False
|
||||
previous_update_success = self.last_update_success
|
||||
previous_data = self.data
|
||||
|
||||
try:
|
||||
self.data = await self._async_update_data()
|
||||
|
@ -371,7 +381,15 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]):
|
|||
if not auth_failed and self._listeners and not self.hass.is_stopping:
|
||||
self._schedule_refresh()
|
||||
|
||||
self.async_update_listeners()
|
||||
if not self.last_update_success and not previous_update_success:
|
||||
return
|
||||
|
||||
if (
|
||||
self.always_update
|
||||
or self.last_update_success != previous_update_success
|
||||
or previous_data != self.data
|
||||
):
|
||||
self.async_update_listeners()
|
||||
|
||||
@callback
|
||||
def async_set_update_error(self, err: Exception) -> None:
|
||||
|
|
|
@ -594,3 +594,118 @@ async def test_async_set_update_error(
|
|||
|
||||
# Remove callbacks to avoid lingering timers
|
||||
remove_callbacks()
|
||||
|
||||
|
||||
async def test_only_callback_on_change_when_always_update_is_false(
|
||||
crd: update_coordinator.DataUpdateCoordinator[int], caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test we do not callback listeners unless something has actually changed when always_update is false."""
|
||||
update_callback = Mock()
|
||||
crd.always_update = False
|
||||
remove_callbacks = crd.async_add_listener(update_callback)
|
||||
mocked_data = None
|
||||
mocked_exception = None
|
||||
|
||||
async def _update_method() -> int:
|
||||
nonlocal mocked_data
|
||||
nonlocal mocked_exception
|
||||
if mocked_exception is not None:
|
||||
raise mocked_exception
|
||||
return mocked_data
|
||||
|
||||
crd.update_method = _update_method
|
||||
|
||||
mocked_data = {"a": 1}
|
||||
await crd.async_refresh()
|
||||
update_callback.assert_called_once()
|
||||
update_callback.reset_mock()
|
||||
|
||||
mocked_data = {"a": 1}
|
||||
await crd.async_refresh()
|
||||
update_callback.assert_not_called()
|
||||
update_callback.reset_mock()
|
||||
|
||||
mocked_data = None
|
||||
mocked_exception = aiohttp.ClientError("Client Failure #1")
|
||||
await crd.async_refresh()
|
||||
update_callback.assert_called_once()
|
||||
update_callback.reset_mock()
|
||||
|
||||
mocked_data = None
|
||||
mocked_exception = aiohttp.ClientError("Client Failure #1")
|
||||
await crd.async_refresh()
|
||||
update_callback.assert_not_called()
|
||||
update_callback.reset_mock()
|
||||
|
||||
mocked_exception = None
|
||||
mocked_data = {"a": 1}
|
||||
await crd.async_refresh()
|
||||
update_callback.assert_called_once()
|
||||
update_callback.reset_mock()
|
||||
|
||||
mocked_data = {"a": 1}
|
||||
await crd.async_refresh()
|
||||
update_callback.assert_not_called()
|
||||
update_callback.reset_mock()
|
||||
|
||||
mocked_data = {"a": 2}
|
||||
await crd.async_refresh()
|
||||
update_callback.assert_called_once()
|
||||
update_callback.reset_mock()
|
||||
|
||||
mocked_data = {"a": 2}
|
||||
await crd.async_refresh()
|
||||
update_callback.assert_not_called()
|
||||
update_callback.reset_mock()
|
||||
|
||||
mocked_data = {"a": 2, "b": 3}
|
||||
await crd.async_refresh()
|
||||
update_callback.assert_called_once()
|
||||
update_callback.reset_mock()
|
||||
|
||||
remove_callbacks()
|
||||
|
||||
|
||||
async def test_always_callback_when_always_update_is_true(
|
||||
crd: update_coordinator.DataUpdateCoordinator[int], caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test we callback listeners even though the data is the same when always_update is True."""
|
||||
update_callback = Mock()
|
||||
remove_callbacks = crd.async_add_listener(update_callback)
|
||||
mocked_data = None
|
||||
mocked_exception = None
|
||||
|
||||
async def _update_method() -> int:
|
||||
nonlocal mocked_data
|
||||
nonlocal mocked_exception
|
||||
if mocked_exception is not None:
|
||||
raise mocked_exception
|
||||
return mocked_data
|
||||
|
||||
crd.update_method = _update_method
|
||||
|
||||
mocked_data = {"a": 1}
|
||||
await crd.async_refresh()
|
||||
update_callback.assert_called_once()
|
||||
update_callback.reset_mock()
|
||||
|
||||
mocked_data = {"a": 1}
|
||||
await crd.async_refresh()
|
||||
update_callback.assert_called_once()
|
||||
update_callback.reset_mock()
|
||||
|
||||
# But still don't fire it if we are only getting
|
||||
# failure over and over
|
||||
mocked_data = None
|
||||
mocked_exception = aiohttp.ClientError("Client Failure #1")
|
||||
await crd.async_refresh()
|
||||
update_callback.assert_called_once()
|
||||
update_callback.reset_mock()
|
||||
|
||||
mocked_data = None
|
||||
mocked_exception = aiohttp.ClientError("Client Failure #1")
|
||||
await crd.async_refresh()
|
||||
update_callback.assert_not_called()
|
||||
update_callback.reset_mock()
|
||||
|
||||
remove_callbacks()
|
||||
|
|
Loading…
Reference in New Issue