Don't fetch unchanged OurGroceries lists (#105998)

This commit is contained in:
On Freund 2023-12-20 11:35:42 +02:00 committed by GitHub
parent 33bcf70bf3
commit 3e07cf50ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 48 additions and 19 deletions

View File

@ -24,16 +24,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {})
data = entry.data
og = OurGroceries(data[CONF_USERNAME], data[CONF_PASSWORD])
lists = []
try:
await og.login()
lists = (await og.get_my_lists())["shoppingLists"]
except (AsyncIOTimeoutError, ClientError) as error:
raise ConfigEntryNotReady from error
except InvalidLoginException:
return False
coordinator = OurGroceriesDataUpdateCoordinator(hass, og, lists)
coordinator = OurGroceriesDataUpdateCoordinator(hass, og)
await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = coordinator

View File

@ -20,13 +20,11 @@ _LOGGER = logging.getLogger(__name__)
class OurGroceriesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
"""Class to manage fetching OurGroceries data."""
def __init__(
self, hass: HomeAssistant, og: OurGroceries, lists: list[dict]
) -> None:
def __init__(self, hass: HomeAssistant, og: OurGroceries) -> None:
"""Initialize global OurGroceries data updater."""
self.og = og
self.lists = lists
self._ids = [sl["id"] for sl in lists]
self.lists: list[dict] = []
self._cache: dict[str, dict] = {}
interval = timedelta(seconds=SCAN_INTERVAL)
super().__init__(
hass,
@ -35,13 +33,16 @@ class OurGroceriesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
update_interval=interval,
)
async def _update_list(self, list_id: str, version_id: str) -> None:
old_version = self._cache.get(list_id, {}).get("list", {}).get("versionId", "")
if old_version == version_id:
return
self._cache[list_id] = await self.og.get_list_items(list_id=list_id)
async def _async_update_data(self) -> dict[str, dict]:
"""Fetch data from OurGroceries."""
return dict(
zip(
self._ids,
await asyncio.gather(
*[self.og.get_list_items(list_id=id) for id in self._ids]
),
)
self.lists = (await self.og.get_my_lists())["shoppingLists"]
await asyncio.gather(
*[self._update_list(sl["id"], sl["versionId"]) for sl in self.lists]
)
return self._cache

View File

@ -1,6 +1,6 @@
"""Tests for the OurGroceries integration."""
def items_to_shopping_list(items: list) -> dict[dict[list]]:
def items_to_shopping_list(items: list, version_id: str = "1") -> dict[dict[list]]:
"""Convert a list of items into a shopping list."""
return {"list": {"items": items}}
return {"list": {"versionId": version_id, "items": items}}

View File

@ -46,7 +46,7 @@ def mock_ourgroceries(items: list[dict]) -> AsyncMock:
og = AsyncMock()
og.login.return_value = True
og.get_my_lists.return_value = {
"shoppingLists": [{"id": "test_list", "name": "Test List"}]
"shoppingLists": [{"id": "test_list", "name": "Test List", "versionId": "1"}]
}
og.get_list_items.return_value = items_to_shopping_list(items)
return og

View File

@ -17,6 +17,10 @@ from . import items_to_shopping_list
from tests.common import async_fire_time_changed
def _mock_version_id(og: AsyncMock, version: int) -> None:
og.get_my_lists.return_value["shoppingLists"][0]["versionId"] = str(version)
@pytest.mark.parametrize(
("items", "expected_state"),
[
@ -57,8 +61,10 @@ async def test_add_todo_list_item(
ourgroceries.add_item_to_list = AsyncMock()
# Fake API response when state is refreshed after create
_mock_version_id(ourgroceries, 2)
ourgroceries.get_list_items.return_value = items_to_shopping_list(
[{"id": "12345", "name": "Soda"}]
[{"id": "12345", "name": "Soda"}],
version_id="2",
)
await hass.services.async_call(
@ -95,6 +101,7 @@ async def test_update_todo_item_status(
ourgroceries.toggle_item_crossed_off = AsyncMock()
# Fake API response when state is refreshed after crossing off
_mock_version_id(ourgroceries, 2)
ourgroceries.get_list_items.return_value = items_to_shopping_list(
[{"id": "12345", "name": "Soda", "crossedOffAt": 1699107501}]
)
@ -118,6 +125,7 @@ async def test_update_todo_item_status(
assert state.state == "0"
# Fake API response when state is refreshed after reopen
_mock_version_id(ourgroceries, 2)
ourgroceries.get_list_items.return_value = items_to_shopping_list(
[{"id": "12345", "name": "Soda"}]
)
@ -166,6 +174,7 @@ async def test_update_todo_item_summary(
ourgroceries.change_item_on_list = AsyncMock()
# Fake API response when state is refreshed update
_mock_version_id(ourgroceries, 2)
ourgroceries.get_list_items.return_value = items_to_shopping_list(
[{"id": "12345", "name": "Milk"}]
)
@ -204,6 +213,7 @@ async def test_remove_todo_item(
ourgroceries.remove_item_from_list = AsyncMock()
# Fake API response when state is refreshed after remove
_mock_version_id(ourgroceries, 2)
ourgroceries.get_list_items.return_value = items_to_shopping_list([])
await hass.services.async_call(
@ -224,6 +234,25 @@ async def test_remove_todo_item(
assert state.state == "0"
async def test_version_id_optimization(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
setup_integration: None,
ourgroceries: AsyncMock,
) -> None:
"""Test that list items aren't being retrieved if version id stays the same."""
state = hass.states.get("todo.test_list")
assert state.state == "0"
assert ourgroceries.get_list_items.call_count == 1
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("todo.test_list")
assert state.state == "0"
assert ourgroceries.get_list_items.call_count == 1
@pytest.mark.parametrize(
("exception"),
[
@ -242,6 +271,7 @@ async def test_coordinator_error(
state = hass.states.get("todo.test_list")
assert state.state == "0"
_mock_version_id(ourgroceries, 2)
ourgroceries.get_list_items.side_effect = exception
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)