Don't fetch unchanged OurGroceries lists (#105998)
This commit is contained in:
parent
33bcf70bf3
commit
3e07cf50ce
5 changed files with 48 additions and 19 deletions
|
@ -24,16 +24,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
hass.data.setdefault(DOMAIN, {})
|
hass.data.setdefault(DOMAIN, {})
|
||||||
data = entry.data
|
data = entry.data
|
||||||
og = OurGroceries(data[CONF_USERNAME], data[CONF_PASSWORD])
|
og = OurGroceries(data[CONF_USERNAME], data[CONF_PASSWORD])
|
||||||
lists = []
|
|
||||||
try:
|
try:
|
||||||
await og.login()
|
await og.login()
|
||||||
lists = (await og.get_my_lists())["shoppingLists"]
|
|
||||||
except (AsyncIOTimeoutError, ClientError) as error:
|
except (AsyncIOTimeoutError, ClientError) as error:
|
||||||
raise ConfigEntryNotReady from error
|
raise ConfigEntryNotReady from error
|
||||||
except InvalidLoginException:
|
except InvalidLoginException:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
coordinator = OurGroceriesDataUpdateCoordinator(hass, og, lists)
|
coordinator = OurGroceriesDataUpdateCoordinator(hass, og)
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||||
|
|
||||||
|
|
|
@ -20,13 +20,11 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
class OurGroceriesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
|
class OurGroceriesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
|
||||||
"""Class to manage fetching OurGroceries data."""
|
"""Class to manage fetching OurGroceries data."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, hass: HomeAssistant, og: OurGroceries) -> None:
|
||||||
self, hass: HomeAssistant, og: OurGroceries, lists: list[dict]
|
|
||||||
) -> None:
|
|
||||||
"""Initialize global OurGroceries data updater."""
|
"""Initialize global OurGroceries data updater."""
|
||||||
self.og = og
|
self.og = og
|
||||||
self.lists = lists
|
self.lists: list[dict] = []
|
||||||
self._ids = [sl["id"] for sl in lists]
|
self._cache: dict[str, dict] = {}
|
||||||
interval = timedelta(seconds=SCAN_INTERVAL)
|
interval = timedelta(seconds=SCAN_INTERVAL)
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
|
@ -35,13 +33,16 @@ class OurGroceriesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
|
||||||
update_interval=interval,
|
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]:
|
async def _async_update_data(self) -> dict[str, dict]:
|
||||||
"""Fetch data from OurGroceries."""
|
"""Fetch data from OurGroceries."""
|
||||||
return dict(
|
self.lists = (await self.og.get_my_lists())["shoppingLists"]
|
||||||
zip(
|
await asyncio.gather(
|
||||||
self._ids,
|
*[self._update_list(sl["id"], sl["versionId"]) for sl in self.lists]
|
||||||
await asyncio.gather(
|
|
||||||
*[self.og.get_list_items(list_id=id) for id in self._ids]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
return self._cache
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""Tests for the OurGroceries integration."""
|
"""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."""
|
"""Convert a list of items into a shopping list."""
|
||||||
return {"list": {"items": items}}
|
return {"list": {"versionId": version_id, "items": items}}
|
||||||
|
|
|
@ -46,7 +46,7 @@ def mock_ourgroceries(items: list[dict]) -> AsyncMock:
|
||||||
og = AsyncMock()
|
og = AsyncMock()
|
||||||
og.login.return_value = True
|
og.login.return_value = True
|
||||||
og.get_my_lists.return_value = {
|
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)
|
og.get_list_items.return_value = items_to_shopping_list(items)
|
||||||
return og
|
return og
|
||||||
|
|
|
@ -17,6 +17,10 @@ from . import items_to_shopping_list
|
||||||
from tests.common import async_fire_time_changed
|
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(
|
@pytest.mark.parametrize(
|
||||||
("items", "expected_state"),
|
("items", "expected_state"),
|
||||||
[
|
[
|
||||||
|
@ -57,8 +61,10 @@ async def test_add_todo_list_item(
|
||||||
|
|
||||||
ourgroceries.add_item_to_list = AsyncMock()
|
ourgroceries.add_item_to_list = AsyncMock()
|
||||||
# Fake API response when state is refreshed after create
|
# Fake API response when state is refreshed after create
|
||||||
|
_mock_version_id(ourgroceries, 2)
|
||||||
ourgroceries.get_list_items.return_value = items_to_shopping_list(
|
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(
|
await hass.services.async_call(
|
||||||
|
@ -95,6 +101,7 @@ async def test_update_todo_item_status(
|
||||||
ourgroceries.toggle_item_crossed_off = AsyncMock()
|
ourgroceries.toggle_item_crossed_off = AsyncMock()
|
||||||
|
|
||||||
# Fake API response when state is refreshed after crossing off
|
# 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(
|
ourgroceries.get_list_items.return_value = items_to_shopping_list(
|
||||||
[{"id": "12345", "name": "Soda", "crossedOffAt": 1699107501}]
|
[{"id": "12345", "name": "Soda", "crossedOffAt": 1699107501}]
|
||||||
)
|
)
|
||||||
|
@ -118,6 +125,7 @@ async def test_update_todo_item_status(
|
||||||
assert state.state == "0"
|
assert state.state == "0"
|
||||||
|
|
||||||
# Fake API response when state is refreshed after reopen
|
# Fake API response when state is refreshed after reopen
|
||||||
|
_mock_version_id(ourgroceries, 2)
|
||||||
ourgroceries.get_list_items.return_value = items_to_shopping_list(
|
ourgroceries.get_list_items.return_value = items_to_shopping_list(
|
||||||
[{"id": "12345", "name": "Soda"}]
|
[{"id": "12345", "name": "Soda"}]
|
||||||
)
|
)
|
||||||
|
@ -166,6 +174,7 @@ async def test_update_todo_item_summary(
|
||||||
ourgroceries.change_item_on_list = AsyncMock()
|
ourgroceries.change_item_on_list = AsyncMock()
|
||||||
|
|
||||||
# Fake API response when state is refreshed update
|
# Fake API response when state is refreshed update
|
||||||
|
_mock_version_id(ourgroceries, 2)
|
||||||
ourgroceries.get_list_items.return_value = items_to_shopping_list(
|
ourgroceries.get_list_items.return_value = items_to_shopping_list(
|
||||||
[{"id": "12345", "name": "Milk"}]
|
[{"id": "12345", "name": "Milk"}]
|
||||||
)
|
)
|
||||||
|
@ -204,6 +213,7 @@ async def test_remove_todo_item(
|
||||||
|
|
||||||
ourgroceries.remove_item_from_list = AsyncMock()
|
ourgroceries.remove_item_from_list = AsyncMock()
|
||||||
# Fake API response when state is refreshed after remove
|
# Fake API response when state is refreshed after remove
|
||||||
|
_mock_version_id(ourgroceries, 2)
|
||||||
ourgroceries.get_list_items.return_value = items_to_shopping_list([])
|
ourgroceries.get_list_items.return_value = items_to_shopping_list([])
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
|
@ -224,6 +234,25 @@ async def test_remove_todo_item(
|
||||||
assert state.state == "0"
|
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(
|
@pytest.mark.parametrize(
|
||||||
("exception"),
|
("exception"),
|
||||||
[
|
[
|
||||||
|
@ -242,6 +271,7 @@ async def test_coordinator_error(
|
||||||
state = hass.states.get("todo.test_list")
|
state = hass.states.get("todo.test_list")
|
||||||
assert state.state == "0"
|
assert state.state == "0"
|
||||||
|
|
||||||
|
_mock_version_id(ourgroceries, 2)
|
||||||
ourgroceries.get_list_items.side_effect = exception
|
ourgroceries.get_list_items.side_effect = exception
|
||||||
freezer.tick(SCAN_INTERVAL)
|
freezer.tick(SCAN_INTERVAL)
|
||||||
async_fire_time_changed(hass)
|
async_fire_time_changed(hass)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue