From 898803abe9eae9ed24f8024d7226e5a12968a103 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Tue, 9 Jul 2024 17:39:22 +0100 Subject: [PATCH] Add shopping lists to Mealie integration (#121534) * Add mealie shopping lists & tests * Add shopping_lists init failure * Fix coordinator name * Fixes * Add available, fix merge * Fixes * Fixes * Add todo failure tests * Fix tests --- homeassistant/components/mealie/__init__.py | 21 +- homeassistant/components/mealie/calendar.py | 6 +- .../components/mealie/coordinator.py | 94 +- homeassistant/components/mealie/entity.py | 6 +- homeassistant/components/mealie/icons.json | 7 + homeassistant/components/mealie/strings.json | 12 + homeassistant/components/mealie/todo.py | 243 +++++ tests/components/mealie/conftest.py | 19 +- .../mealie/fixtures/get_shopping_items.json | 108 +++ .../mealie/fixtures/get_shopping_lists.json | 838 ++++++++++++++++++ .../mealie/snapshots/test_todo.ambr | 156 ++++ tests/components/mealie/test_calendar.py | 6 +- tests/components/mealie/test_init.py | 46 +- tests/components/mealie/test_todo.py | 149 ++++ 14 files changed, 1692 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/mealie/todo.py create mode 100644 tests/components/mealie/fixtures/get_shopping_items.json create mode 100644 tests/components/mealie/fixtures/get_shopping_lists.json create mode 100644 tests/components/mealie/snapshots/test_todo.ambr create mode 100644 tests/components/mealie/test_todo.py diff --git a/homeassistant/components/mealie/__init__.py b/homeassistant/components/mealie/__init__.py index 697410a25f7..2d261af37a2 100644 --- a/homeassistant/components/mealie/__init__.py +++ b/homeassistant/components/mealie/__init__.py @@ -13,10 +13,15 @@ from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.typing import ConfigType from .const import DOMAIN -from .coordinator import MealieConfigEntry, MealieCoordinator +from .coordinator import ( + MealieConfigEntry, + MealieData, + MealieMealplanCoordinator, + MealieShoppingListCoordinator, +) from .services import setup_services -PLATFORMS: list[Platform] = [Platform.CALENDAR] +PLATFORMS: list[Platform] = [Platform.CALENDAR, Platform.TODO] CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) @@ -50,11 +55,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: MealieConfigEntry) -> bo sw_version=about.version, ) - coordinator = MealieCoordinator(hass, client) + mealplan_coordinator = MealieMealplanCoordinator(hass, client) + shoppinglist_coordinator = MealieShoppingListCoordinator(hass, client) - await coordinator.async_config_entry_first_refresh() + await mealplan_coordinator.async_config_entry_first_refresh() - entry.runtime_data = coordinator + await shoppinglist_coordinator.async_get_shopping_lists() + await shoppinglist_coordinator.async_config_entry_first_refresh() + + entry.runtime_data = MealieData( + client, mealplan_coordinator, shoppinglist_coordinator + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/mealie/calendar.py b/homeassistant/components/mealie/calendar.py index 6969f3f30f9..4c11c639c79 100644 --- a/homeassistant/components/mealie/calendar.py +++ b/homeassistant/components/mealie/calendar.py @@ -10,7 +10,7 @@ from homeassistant.components.calendar import CalendarEntity, CalendarEvent from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .coordinator import MealieConfigEntry, MealieCoordinator +from .coordinator import MealieConfigEntry, MealieMealplanCoordinator from .entity import MealieEntity @@ -20,7 +20,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the calendar platform for entity.""" - coordinator = entry.runtime_data + coordinator = entry.runtime_data.mealplan_coordinator async_add_entities( MealieMealplanCalendarEntity(coordinator, entry_type) @@ -47,7 +47,7 @@ class MealieMealplanCalendarEntity(MealieEntity, CalendarEntity): """A calendar entity.""" def __init__( - self, coordinator: MealieCoordinator, entry_type: MealplanEntryType + self, coordinator: MealieMealplanCoordinator, entry_type: MealplanEntryType ) -> None: """Create the Calendar entity.""" super().__init__(coordinator, entry_type.name.lower()) diff --git a/homeassistant/components/mealie/coordinator.py b/homeassistant/components/mealie/coordinator.py index 6d8e64730e0..135100e1b07 100644 --- a/homeassistant/components/mealie/coordinator.py +++ b/homeassistant/components/mealie/coordinator.py @@ -2,6 +2,7 @@ from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta from aiomealie import ( @@ -10,6 +11,8 @@ from aiomealie import ( MealieConnectionError, Mealplan, MealplanEntryType, + ShoppingItem, + ShoppingList, ) from homeassistant.config_entries import ConfigEntry @@ -22,18 +25,53 @@ from .const import LOGGER WEEK = timedelta(days=7) -type MealieConfigEntry = ConfigEntry[MealieCoordinator] + +@dataclass +class MealieData: + """Mealie data type.""" + + client: MealieClient + mealplan_coordinator: MealieMealplanCoordinator + shoppinglist_coordinator: MealieShoppingListCoordinator -class MealieCoordinator(DataUpdateCoordinator[dict[MealplanEntryType, list[Mealplan]]]): - """Class to manage fetching Mealie data.""" +type MealieConfigEntry = ConfigEntry[MealieData] + + +class MealieDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]): + """Base coordinator.""" config_entry: MealieConfigEntry + def __init__( + self, + hass: HomeAssistant, + name: str, + client: MealieClient, + update_interval: timedelta, + ) -> None: + """Initialize the Withings data coordinator.""" + super().__init__( + hass, + LOGGER, + name=name, + update_interval=update_interval, + ) + self.client = client + + +class MealieMealplanCoordinator( + MealieDataUpdateCoordinator[dict[MealplanEntryType, list[Mealplan]]] +): + """Class to manage fetching Mealie data.""" + def __init__(self, hass: HomeAssistant, client: MealieClient) -> None: """Initialize coordinator.""" super().__init__( - hass, logger=LOGGER, name="Mealie", update_interval=timedelta(hours=1) + hass, + name="MealieMealplan", + client=client, + update_interval=timedelta(hours=1), ) self.client = client @@ -56,3 +94,51 @@ class MealieCoordinator(DataUpdateCoordinator[dict[MealplanEntryType, list[Mealp for meal in data: res[meal.entry_type].append(meal) return res + + +class MealieShoppingListCoordinator( + MealieDataUpdateCoordinator[dict[str, list[ShoppingItem]]] +): + """Class to manage fetching Mealie Shopping list data.""" + + def __init__(self, hass: HomeAssistant, client: MealieClient) -> None: + """Initialize coordinator.""" + super().__init__( + hass, + name="MealieShoppingLists", + client=client, + update_interval=timedelta(minutes=5), + ) + self.shopping_lists: list[ShoppingList] + + async def async_get_shopping_lists(self) -> list[ShoppingList]: + """Return shopping lists.""" + try: + self.shopping_lists = (await self.client.get_shopping_lists()).items + except MealieAuthenticationError as error: + raise ConfigEntryError("Authentication failed") from error + except MealieConnectionError as error: + raise UpdateFailed(error) from error + return self.shopping_lists + + async def _async_update_data( + self, + ) -> dict[str, list[ShoppingItem]]: + shopping_list_items: dict[str, list[ShoppingItem]] = {} + + try: + for shopping_list in self.shopping_lists: + shopping_list_id = shopping_list.list_id + + shopping_items = ( + await self.client.get_shopping_items(shopping_list_id) + ).items + + shopping_list_items[shopping_list_id] = shopping_items + + except MealieAuthenticationError as error: + raise ConfigEntryError("Authentication failed") from error + except MealieConnectionError as error: + raise UpdateFailed(error) from error + + return shopping_list_items diff --git a/homeassistant/components/mealie/entity.py b/homeassistant/components/mealie/entity.py index 349304f1965..6989e3e4f10 100644 --- a/homeassistant/components/mealie/entity.py +++ b/homeassistant/components/mealie/entity.py @@ -4,15 +4,15 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import MealieCoordinator +from .coordinator import MealieDataUpdateCoordinator -class MealieEntity(CoordinatorEntity[MealieCoordinator]): +class MealieEntity(CoordinatorEntity[MealieDataUpdateCoordinator]): """Defines a base Mealie entity.""" _attr_has_entity_name = True - def __init__(self, coordinator: MealieCoordinator, key: str) -> None: + def __init__(self, coordinator: MealieDataUpdateCoordinator, key: str) -> None: """Initialize Mealie entity.""" super().__init__(coordinator) unique_id = coordinator.config_entry.unique_id diff --git a/homeassistant/components/mealie/icons.json b/homeassistant/components/mealie/icons.json index 9d683f1ce81..2be9b1f9b20 100644 --- a/homeassistant/components/mealie/icons.json +++ b/homeassistant/components/mealie/icons.json @@ -1,4 +1,11 @@ { + "entity": { + "todo": { + "shopping_list": { + "default": "mdi:basket" + } + } + }, "services": { "get_mealplan": "mdi:food", "get_recipe": "mdi:map" diff --git a/homeassistant/components/mealie/strings.json b/homeassistant/components/mealie/strings.json index 4b7cdc6d780..63097ae3368 100644 --- a/homeassistant/components/mealie/strings.json +++ b/homeassistant/components/mealie/strings.json @@ -51,6 +51,18 @@ }, "recipe_not_found": { "message": "Recipe with ID or slug `{recipe_id}` not found." + }, + "add_item_error": { + "message": "An error occurred adding an item to {shopping_list_name}." + }, + "update_item_error": { + "message": "An error occurred updating an item in {shopping_list_name}." + }, + "delete_item_error": { + "message": "An error occurred deleting an item in {shopping_list_name}." + }, + "item_not_found_error": { + "message": "Item {shopping_list_item} not found." } }, "services": { diff --git a/homeassistant/components/mealie/todo.py b/homeassistant/components/mealie/todo.py new file mode 100644 index 00000000000..7009dedf105 --- /dev/null +++ b/homeassistant/components/mealie/todo.py @@ -0,0 +1,243 @@ +"""Todo platform for Mealie.""" + +from __future__ import annotations + +from aiomealie import MealieError, MutateShoppingItem, ShoppingItem, ShoppingList + +from homeassistant.components.todo import ( + TodoItem, + TodoItemStatus, + TodoListEntity, + TodoListEntityFeature, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import MealieConfigEntry, MealieShoppingListCoordinator +from .entity import MealieEntity + +TODO_STATUS_MAP = { + False: TodoItemStatus.NEEDS_ACTION, + True: TodoItemStatus.COMPLETED, +} +TODO_STATUS_MAP_INV = {v: k for k, v in TODO_STATUS_MAP.items()} + + +def _convert_api_item(item: ShoppingItem) -> TodoItem: + """Convert Mealie shopping list items into a TodoItem.""" + + return TodoItem( + summary=item.display, + uid=item.item_id, + status=TODO_STATUS_MAP.get( + item.checked, + TodoItemStatus.NEEDS_ACTION, + ), + due=None, + description=None, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: MealieConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the todo platform for entity.""" + coordinator = entry.runtime_data.shoppinglist_coordinator + + async_add_entities( + MealieShoppingListTodoListEntity(coordinator, shopping_list) + for shopping_list in coordinator.shopping_lists + ) + + +class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity): + """A todo list entity.""" + + _attr_supported_features = ( + TodoListEntityFeature.CREATE_TODO_ITEM + | TodoListEntityFeature.UPDATE_TODO_ITEM + | TodoListEntityFeature.DELETE_TODO_ITEM + | TodoListEntityFeature.MOVE_TODO_ITEM + ) + + _attr_translation_key = "shopping_list" + + coordinator: MealieShoppingListCoordinator + + def __init__( + self, coordinator: MealieShoppingListCoordinator, shopping_list: ShoppingList + ) -> None: + """Create the todo entity.""" + super().__init__(coordinator, shopping_list.list_id) + self._shopping_list = shopping_list + self._attr_name = shopping_list.name + + @property + def shopping_items(self) -> list[ShoppingItem]: + """Get the shopping items for this list.""" + return self.coordinator.data[self._shopping_list.list_id] + + @property + def todo_items(self) -> list[TodoItem] | None: + """Get the current set of To-do items.""" + return [_convert_api_item(item) for item in self.shopping_items] + + async def async_create_todo_item(self, item: TodoItem) -> None: + """Add an item to the list.""" + position = 0 + if len(self.shopping_items) > 0: + position = self.shopping_items[-1].position + 1 + + new_shopping_item = MutateShoppingItem( + list_id=self._shopping_list.list_id, + note=item.summary.strip() if item.summary else item.summary, + position=position, + ) + try: + await self.coordinator.client.add_shopping_item(new_shopping_item) + except MealieError as exception: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="add_item_error", + translation_placeholders={ + "shopping_list_name": self._shopping_list.name + }, + ) from exception + finally: + await self.coordinator.async_refresh() + + async def async_update_todo_item(self, item: TodoItem) -> None: + """Update an item on the list.""" + list_items = self.shopping_items + + for items in list_items: + if items.item_id == item.uid: + position = items.position + break + + list_item: ShoppingItem | None = next( + (x for x in list_items if x.item_id == item.uid), None + ) + + if not list_item: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="item_not_found_error", + translation_placeholders={"shopping_list_item": item.uid or ""}, + ) + + udpdate_shopping_item = MutateShoppingItem( + item_id=list_item.item_id, + list_id=list_item.list_id, + note=list_item.note, + display=list_item.display, + checked=item.status == TodoItemStatus.COMPLETED, + position=list_item.position, + is_food=list_item.is_food, + disable_amount=list_item.disable_amount, + quantity=list_item.quantity, + label_id=list_item.label_id, + food_id=list_item.food_id, + unit_id=list_item.unit_id, + ) + + stripped_item_summary = item.summary.strip() if item.summary else item.summary + + if list_item.display.strip() != stripped_item_summary: + udpdate_shopping_item.note = stripped_item_summary + udpdate_shopping_item.position = position + udpdate_shopping_item.is_food = False + udpdate_shopping_item.food_id = None + udpdate_shopping_item.quantity = 0.0 + udpdate_shopping_item.checked = item.status == TodoItemStatus.COMPLETED + + try: + await self.coordinator.client.update_shopping_item( + list_item.item_id, udpdate_shopping_item + ) + except MealieError as exception: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="update_item_error", + translation_placeholders={ + "shopping_list_name": self._shopping_list.name + }, + ) from exception + finally: + await self.coordinator.async_refresh() + + async def async_delete_todo_items(self, uids: list[str]) -> None: + """Delete items from the list.""" + try: + for uid in uids: + await self.coordinator.client.delete_shopping_item(uid) + except MealieError as exception: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="delete_item_error", + translation_placeholders={ + "shopping_list_name": self._shopping_list.name + }, + ) from exception + finally: + await self.coordinator.async_refresh() + + async def async_move_todo_item( + self, uid: str, previous_uid: str | None = None + ) -> None: + """Re-order an item on the list.""" + if uid == previous_uid: + return + list_items: list[ShoppingItem] = self.shopping_items + + item_idx = {itm.item_id: idx for idx, itm in enumerate(list_items)} + if uid not in item_idx: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="item_not_found_error", + translation_placeholders={"shopping_list_item": uid}, + ) + if previous_uid and previous_uid not in item_idx: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="item_not_found_error", + translation_placeholders={"shopping_list_item": previous_uid}, + ) + dst_idx = item_idx[previous_uid] + 1 if previous_uid else 0 + src_idx = item_idx[uid] + src_item = list_items.pop(src_idx) + if dst_idx > src_idx: + dst_idx -= 1 + list_items.insert(dst_idx, src_item) + + for position, item in enumerate(list_items): + mutate_shopping_item = MutateShoppingItem() + mutate_shopping_item.list_id = item.list_id + mutate_shopping_item.item_id = item.item_id + mutate_shopping_item.position = position + mutate_shopping_item.is_food = item.is_food + mutate_shopping_item.quantity = item.quantity + mutate_shopping_item.label_id = item.label_id + mutate_shopping_item.note = item.note + mutate_shopping_item.checked = item.checked + + if item.is_food: + mutate_shopping_item.food_id = item.food_id + mutate_shopping_item.unit_id = item.unit_id + + await self.coordinator.client.update_shopping_item( + mutate_shopping_item.item_id, mutate_shopping_item + ) + + await self.coordinator.async_refresh() + + @property + def available(self) -> bool: + """Return False if shopping list no longer available.""" + return ( + super().available and self._shopping_list.list_id in self.coordinator.data + ) diff --git a/tests/components/mealie/conftest.py b/tests/components/mealie/conftest.py index 6a1db534811..c91efe7f767 100644 --- a/tests/components/mealie/conftest.py +++ b/tests/components/mealie/conftest.py @@ -3,7 +3,15 @@ from collections.abc import Generator from unittest.mock import patch -from aiomealie import About, Mealplan, MealplanResponse, Recipe, UserInfo +from aiomealie import ( + About, + Mealplan, + MealplanResponse, + Recipe, + ShoppingItemsResponse, + ShoppingListsResponse, + UserInfo, +) from mashumaro.codecs.orjson import ORJSONDecoder import pytest @@ -13,6 +21,9 @@ from homeassistant.const import CONF_API_TOKEN, CONF_HOST from tests.common import MockConfigEntry, load_fixture from tests.components.smhi.common import AsyncMock +SHOPPING_LIST_ID = "list-id-1" +SHOPPING_ITEM_NOTE = "Shopping Item 1" + @pytest.fixture def mock_setup_entry() -> Generator[AsyncMock]: @@ -50,6 +61,12 @@ def mock_mealie_client() -> Generator[AsyncMock]: client.get_about.return_value = About.from_json( load_fixture("about.json", DOMAIN) ) + client.get_shopping_lists.return_value = ShoppingListsResponse.from_json( + load_fixture("get_shopping_lists.json", DOMAIN) + ) + client.get_shopping_items.return_value = ShoppingItemsResponse.from_json( + load_fixture("get_shopping_items.json", DOMAIN) + ) client.get_recipe.return_value = Recipe.from_json( load_fixture("get_recipe.json", DOMAIN) ) diff --git a/tests/components/mealie/fixtures/get_shopping_items.json b/tests/components/mealie/fixtures/get_shopping_items.json new file mode 100644 index 00000000000..1016440816b --- /dev/null +++ b/tests/components/mealie/fixtures/get_shopping_items.json @@ -0,0 +1,108 @@ +{ + "page": 1, + "per_page": 1000, + "total": 3, + "total_pages": 1, + "items": [ + { + "quantity": 2.0, + "unit": null, + "food": null, + "note": "Apples", + "isFood": false, + "disableAmount": true, + "display": "2 Apples", + "shoppingListId": "9ce096fe-ded2-4077-877d-78ba450ab13e", + "checked": false, + "position": 0, + "foodId": null, + "labelId": null, + "unitId": null, + "extras": {}, + "id": "f45430f7-3edf-45a9-a50f-73bb375090be", + "label": null, + "recipeReferences": [], + "createdAt": "2024-06-25T10:45:03.362623", + "updateAt": "2024-06-25T11:57:22.412650" + }, + { + "quantity": 1.0, + "unit": { + "id": "7bf539d4-fc78-48bc-b48e-c35ccccec34a", + "name": "can", + "pluralName": null, + "description": "", + "extras": {}, + "fraction": true, + "abbreviation": "", + "pluralAbbreviation": "", + "useAbbreviation": false, + "aliases": [], + "createdAt": "2024-05-14T14:45:02.464122", + "updateAt": "2024-05-14T14:45:02.464124" + }, + "food": { + "id": "09322430-d24c-4b1a-abb6-22b6ed3a88f5", + "name": "acorn squash", + "pluralName": null, + "description": "", + "extras": {}, + "labelId": null, + "aliases": [], + "label": null, + "createdAt": "2024-05-14T14:45:04.454134", + "updateAt": "2024-05-14T14:45:04.454141" + }, + "note": "", + "isFood": true, + "disableAmount": false, + "display": "1 can acorn squash", + "shoppingListId": "9ce096fe-ded2-4077-877d-78ba450ab13e", + "checked": false, + "position": 1, + "foodId": "09322430-d24c-4b1a-abb6-22b6ed3a88f5", + "labelId": null, + "unitId": "7bf539d4-fc78-48bc-b48e-c35ccccec34a", + "extras": {}, + "id": "84d8fd74-8eb0-402e-84b6-71f251bfb7cc", + "label": null, + "recipeReferences": [], + "createdAt": "2024-06-25T10:45:14.547922", + "updateAt": "2024-06-25T10:45:14.547925" + }, + { + "quantity": 0.0, + "unit": null, + "food": { + "id": "96801494-4e26-4148-849a-8155deb76327", + "name": "aubergine", + "pluralName": null, + "description": "", + "extras": {}, + "labelId": null, + "aliases": [], + "label": null, + "createdAt": "2024-05-14T14:45:03.868792", + "updateAt": "2024-05-14T14:45:03.868794" + }, + "note": "", + "isFood": true, + "disableAmount": false, + "display": "aubergine", + "shoppingListId": "9ce096fe-ded2-4077-877d-78ba450ab13e", + "checked": false, + "position": 2, + "foodId": "96801494-4e26-4148-849a-8155deb76327", + "labelId": null, + "unitId": null, + "extras": {}, + "id": "69913b9a-7c75-4935-abec-297cf7483f88", + "label": null, + "recipeReferences": [], + "createdAt": "2024-06-25T11:56:59.656699", + "updateAt": "2024-06-25T11:56:59.656701" + } + ], + "next": null, + "previous": null +} diff --git a/tests/components/mealie/fixtures/get_shopping_lists.json b/tests/components/mealie/fixtures/get_shopping_lists.json new file mode 100644 index 00000000000..7b7ba0aaa7a --- /dev/null +++ b/tests/components/mealie/fixtures/get_shopping_lists.json @@ -0,0 +1,838 @@ +{ + "page": 1, + "per_page": 50, + "total": 3, + "total_pages": 1, + "items": [ + { + "name": "Supermarket", + "extras": {}, + "createdAt": "2024-06-17T11:01:54.267314", + "updateAt": "2024-06-22T10:22:13.555389", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "userId": "90b03954-00e1-46de-9520-f0305022b84f", + "id": "27edbaab-2ec6-441f-8490-0283ea77585f", + "recipeReferences": [], + "labelSettings": [ + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "0f63545a-606a-47ea-a784-452d45de6158", + "position": 0, + "id": "ad5f48b0-5b26-4c2d-a2aa-79b0beae1e42", + "label": { + "name": "Alcohol", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "0f63545a-606a-47ea-a784-452d45de6158" + } + }, + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "0c2d6111-9837-4319-acb5-490a32979993", + "position": 1, + "id": "c9b8289a-6693-4bec-9841-d7d08c3b240b", + "label": { + "name": "Baked Goods", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "0c2d6111-9837-4319-acb5-490a32979993" + } + }, + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "3922802c-8e8c-47d4-9c68-e60b0a1338b6", + "position": 2, + "id": "9be06f8a-6c23-476b-a8cc-334884bcdd40", + "label": { + "name": "Beverages", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "3922802c-8e8c-47d4-9c68-e60b0a1338b6" + } + }, + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "4111bfff-d834-4e8c-88ed-5eff761e06db", + "position": 3, + "id": "47bc36ae-1ee4-40be-ad68-ad8662c26cae", + "label": { + "name": "Canned Goods", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "4111bfff-d834-4e8c-88ed-5eff761e06db" + } + }, + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "24fa2836-25e8-44af-b497-ad0d428a7f78", + "position": 4, + "id": "ad41f42c-08c3-49ef-8b96-dc1740ec95b6", + "label": { + "name": "Condiments", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "24fa2836-25e8-44af-b497-ad0d428a7f78" + } + }, + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "20a735de-c75b-4fdb-abaf-b8d71ef192f8", + "position": 5, + "id": "5514842f-8c05-4003-a42d-7a5a70d80148", + "label": { + "name": "Confectionary", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "20a735de-c75b-4fdb-abaf-b8d71ef192f8" + } + }, + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "71178428-70aa-4491-b5b4-b8d93e7b04cf", + "position": 6, + "id": "0465a139-6571-4599-836b-a562afc95536", + "label": { + "name": "Dairy Products", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "71178428-70aa-4491-b5b4-b8d93e7b04cf" + } + }, + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "c58ed864-b5bf-4aac-88a1-007833c706c7", + "position": 7, + "id": "8d85fe1b-ec4d-49d0-aecc-15f9dbc66fd0", + "label": { + "name": "Frozen Foods", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "c58ed864-b5bf-4aac-88a1-007833c706c7" + } + }, + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "f398f1a4-ce53-42df-95d4-8a3403bb6a38", + "position": 8, + "id": "b6980720-bd88-4703-a115-50c0b915f607", + "label": { + "name": "Fruits", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "f398f1a4-ce53-42df-95d4-8a3403bb6a38" + } + }, + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "fd936065-3d53-4844-99df-9332f1bf0c8a", + "position": 9, + "id": "5d69d13c-5d7f-45af-9ecc-045ca914f7ca", + "label": { + "name": "Grains", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "fd936065-3d53-4844-99df-9332f1bf0c8a" + } + }, + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "db7b685a-4aeb-4ebd-9b64-0c14827d9eaf", + "position": 10, + "id": "a5e65ce7-3588-412b-a118-2fe1a2ca0104", + "label": { + "name": "Health Foods", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "db7b685a-4aeb-4ebd-9b64-0c14827d9eaf" + } + }, + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "28bbdab4-7eab-4fb2-b0e1-b0f2c10e489b", + "position": 11, + "id": "9890d86a-98e9-4599-8daf-82d341ef1e8d", + "label": { + "name": "Household", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "28bbdab4-7eab-4fb2-b0e1-b0f2c10e489b" + } + }, + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "cf136576-1929-4fc9-a3da-34c49ff58920", + "position": 12, + "id": "18fc0f39-3e45-412f-afa7-7eb779f7bfdf", + "label": { + "name": "Meat", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "cf136576-1929-4fc9-a3da-34c49ff58920" + } + }, + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "5b7d69d0-4d9f-48f9-96f1-8cb843227baa", + "position": 13, + "id": "4cd55de7-7c2e-4078-8c61-87d40b33ebda", + "label": { + "name": "Meat Products", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "5b7d69d0-4d9f-48f9-96f1-8cb843227baa" + } + }, + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "2a035661-fd5d-462c-8eb0-6b78af982e0c", + "position": 14, + "id": "21c55b4a-c1b1-44c0-962e-040bbfa5e148", + "label": { + "name": "Other", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "2a035661-fd5d-462c-8eb0-6b78af982e0c" + } + }, + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "af147838-d114-4a92-bd0f-08f05f59bbe5", + "position": 15, + "id": "b295a6be-1437-4415-92bb-4eee21d3195d", + "label": { + "name": "Produce", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "af147838-d114-4a92-bd0f-08f05f59bbe5" + } + }, + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "cf7672b8-036a-45a4-8323-6a167d2731be", + "position": 16, + "id": "d3ae533f-c1a8-4f08-8a0f-a88914b2c84b", + "label": { + "name": "Regular", + "color": "#2E7D32FF", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "cf7672b8-036a-45a4-8323-6a167d2731be" + } + }, + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "bbcfaf8b-02e6-4c3d-98a6-6863b36bef18", + "position": 17, + "id": "572dbf60-4308-499e-ad7c-d806462ee501", + "label": { + "name": "Seafood", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "bbcfaf8b-02e6-4c3d-98a6-6863b36bef18" + } + }, + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "1c59a263-227a-4f43-a450-d53ca1485b36", + "position": 18, + "id": "5321b4d8-3aba-4a64-95b2-03ac533dda32", + "label": { + "name": "Snacks", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "1c59a263-227a-4f43-a450-d53ca1485b36" + } + }, + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "189099a9-0033-4783-804a-ec6805e7d557", + "position": 19, + "id": "98aebebf-27fe-4834-b3d3-0e45201a182f", + "label": { + "name": "Spices", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "189099a9-0033-4783-804a-ec6805e7d557" + } + }, + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "c28efdde-5993-4044-b824-f111f3a118ef", + "position": 20, + "id": "3e3aa706-3008-4280-b332-a7d2c31cf683", + "label": { + "name": "Sweets", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "c28efdde-5993-4044-b824-f111f3a118ef" + } + }, + { + "shoppingListId": "27edbaab-2ec6-441f-8490-0283ea77585f", + "labelId": "3f151d15-27f9-41c7-9dfc-2ae1024b1c7c", + "position": 21, + "id": "48f109ca-c57a-4828-98ab-a2db1e6514c6", + "label": { + "name": "Vegetables", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "3f151d15-27f9-41c7-9dfc-2ae1024b1c7c" + } + } + ] + }, + { + "name": "Special groceries", + "extras": {}, + "createdAt": "2024-06-07T07:17:05.479808", + "updateAt": "2024-06-12T08:44:58.831239", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "userId": "90b03954-00e1-46de-9520-f0305022b84f", + "id": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "recipeReferences": [], + "labelSettings": [ + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "0f63545a-606a-47ea-a784-452d45de6158", + "position": 0, + "id": "1a5dc45b-e6ae-4db2-bd2f-fa3c07efedeb", + "label": { + "name": "Alcohol", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "0f63545a-606a-47ea-a784-452d45de6158" + } + }, + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "0c2d6111-9837-4319-acb5-490a32979993", + "position": 1, + "id": "d1594c9d-f1b6-4160-a4eb-0686499a40ea", + "label": { + "name": "Baked Goods", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "0c2d6111-9837-4319-acb5-490a32979993" + } + }, + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "3922802c-8e8c-47d4-9c68-e60b0a1338b6", + "position": 2, + "id": "077106d0-5c85-493c-ae6b-dea06002c824", + "label": { + "name": "Beverages", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "3922802c-8e8c-47d4-9c68-e60b0a1338b6" + } + }, + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "4111bfff-d834-4e8c-88ed-5eff761e06db", + "position": 3, + "id": "bf66b7e8-3758-4f9e-9e13-c7b9ff564889", + "label": { + "name": "Canned Goods", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "4111bfff-d834-4e8c-88ed-5eff761e06db" + } + }, + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "24fa2836-25e8-44af-b497-ad0d428a7f78", + "position": 4, + "id": "bb34f741-10b4-490a-a512-67bbd374427c", + "label": { + "name": "Condiments", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "24fa2836-25e8-44af-b497-ad0d428a7f78" + } + }, + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "20a735de-c75b-4fdb-abaf-b8d71ef192f8", + "position": 5, + "id": "d88b23a5-e397-4cf2-b527-d8982ecf89e0", + "label": { + "name": "Confectionary", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "20a735de-c75b-4fdb-abaf-b8d71ef192f8" + } + }, + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "71178428-70aa-4491-b5b4-b8d93e7b04cf", + "position": 6, + "id": "82d44804-5bef-4cc3-9d1f-0d8e879783c0", + "label": { + "name": "Dairy Products", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "71178428-70aa-4491-b5b4-b8d93e7b04cf" + } + }, + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "c58ed864-b5bf-4aac-88a1-007833c706c7", + "position": 7, + "id": "0ae70dde-7403-408f-a6c6-c19b8c0f6a4d", + "label": { + "name": "Frozen Foods", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "c58ed864-b5bf-4aac-88a1-007833c706c7" + } + }, + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "f398f1a4-ce53-42df-95d4-8a3403bb6a38", + "position": 8, + "id": "7667a581-8d63-4785-a013-8e164994dfc4", + "label": { + "name": "Fruits", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "f398f1a4-ce53-42df-95d4-8a3403bb6a38" + } + }, + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "fd936065-3d53-4844-99df-9332f1bf0c8a", + "position": 9, + "id": "749c8cbd-c4e5-4879-bce1-40c3b62ada71", + "label": { + "name": "Grains", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "fd936065-3d53-4844-99df-9332f1bf0c8a" + } + }, + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "db7b685a-4aeb-4ebd-9b64-0c14827d9eaf", + "position": 10, + "id": "e7979797-7679-47be-b14f-5fdcfe1c987d", + "label": { + "name": "Health Foods", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "db7b685a-4aeb-4ebd-9b64-0c14827d9eaf" + } + }, + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "28bbdab4-7eab-4fb2-b0e1-b0f2c10e489b", + "position": 11, + "id": "1a9b6d19-d8b5-41a0-8e75-548c36fc0b1b", + "label": { + "name": "Household", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "28bbdab4-7eab-4fb2-b0e1-b0f2c10e489b" + } + }, + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "cf136576-1929-4fc9-a3da-34c49ff58920", + "position": 12, + "id": "0df24ff7-1767-46a1-9841-97f816079580", + "label": { + "name": "Meat", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "cf136576-1929-4fc9-a3da-34c49ff58920" + } + }, + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "5b7d69d0-4d9f-48f9-96f1-8cb843227baa", + "position": 13, + "id": "761b5985-9f49-450b-a33c-5b85366501da", + "label": { + "name": "Meat Products", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "5b7d69d0-4d9f-48f9-96f1-8cb843227baa" + } + }, + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "2a035661-fd5d-462c-8eb0-6b78af982e0c", + "position": 14, + "id": "cd993b6c-2c06-40b3-8fe2-8f9613d29b8e", + "label": { + "name": "Other", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "2a035661-fd5d-462c-8eb0-6b78af982e0c" + } + }, + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "af147838-d114-4a92-bd0f-08f05f59bbe5", + "position": 15, + "id": "9c9f8e0d-a9e8-4503-ad98-ee7039ec6eec", + "label": { + "name": "Produce", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "af147838-d114-4a92-bd0f-08f05f59bbe5" + } + }, + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "cf7672b8-036a-45a4-8323-6a167d2731be", + "position": 16, + "id": "f2a1fa92-1ee3-47b5-9d5f-1ac21e0d6bf3", + "label": { + "name": "Regular", + "color": "#2E7D32FF", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "cf7672b8-036a-45a4-8323-6a167d2731be" + } + }, + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "bbcfaf8b-02e6-4c3d-98a6-6863b36bef18", + "position": 17, + "id": "bf2eb5db-bf88-44bc-a83f-7c69c38fc03f", + "label": { + "name": "Seafood", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "bbcfaf8b-02e6-4c3d-98a6-6863b36bef18" + } + }, + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "1c59a263-227a-4f43-a450-d53ca1485b36", + "position": 18, + "id": "14f5ca34-fcec-4847-8ee7-71b29488dc5b", + "label": { + "name": "Snacks", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "1c59a263-227a-4f43-a450-d53ca1485b36" + } + }, + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "189099a9-0033-4783-804a-ec6805e7d557", + "position": 19, + "id": "197f3d41-27a6-4782-a78d-60ea582108c8", + "label": { + "name": "Spices", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "189099a9-0033-4783-804a-ec6805e7d557" + } + }, + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "c28efdde-5993-4044-b824-f111f3a118ef", + "position": 20, + "id": "b5021331-2004-4570-a2bb-c6f364787bcc", + "label": { + "name": "Sweets", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "c28efdde-5993-4044-b824-f111f3a118ef" + } + }, + { + "shoppingListId": "f8438635-8211-4be8-80d0-0aa42e37a5f2", + "labelId": "3f151d15-27f9-41c7-9dfc-2ae1024b1c7c", + "position": 21, + "id": "98e9ecff-d650-4717-96fe-d7744258bf43", + "label": { + "name": "Vegetables", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "3f151d15-27f9-41c7-9dfc-2ae1024b1c7c" + } + } + ] + }, + { + "name": "Freezer", + "extras": {}, + "createdAt": "2024-06-05T09:49:00.404632", + "updateAt": "2024-06-23T08:21:51.764793", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "userId": "90b03954-00e1-46de-9520-f0305022b84f", + "id": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "recipeReferences": [], + "labelSettings": [ + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "0f63545a-606a-47ea-a784-452d45de6158", + "position": 0, + "id": "666b5b98-dcf6-4121-a5a6-2782f06f5f7e", + "label": { + "name": "Alcohol", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "0f63545a-606a-47ea-a784-452d45de6158" + } + }, + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "0c2d6111-9837-4319-acb5-490a32979993", + "position": 1, + "id": "6d25fc7e-33d2-459c-ba14-7e0aaf30a522", + "label": { + "name": "Baked Goods", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "0c2d6111-9837-4319-acb5-490a32979993" + } + }, + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "3922802c-8e8c-47d4-9c68-e60b0a1338b6", + "position": 2, + "id": "56402a4e-c94e-4480-9f68-87370dbda209", + "label": { + "name": "Beverages", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "3922802c-8e8c-47d4-9c68-e60b0a1338b6" + } + }, + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "4111bfff-d834-4e8c-88ed-5eff761e06db", + "position": 3, + "id": "743e9e2b-a13a-4d80-b203-431d1c23f691", + "label": { + "name": "Canned Goods", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "4111bfff-d834-4e8c-88ed-5eff761e06db" + } + }, + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "24fa2836-25e8-44af-b497-ad0d428a7f78", + "position": 4, + "id": "93b46c6e-0542-4adf-ad9d-8942b47dd9e3", + "label": { + "name": "Condiments", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "24fa2836-25e8-44af-b497-ad0d428a7f78" + } + }, + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "20a735de-c75b-4fdb-abaf-b8d71ef192f8", + "position": 5, + "id": "8c6f20ff-a5e3-4c64-a1ff-aa07bbdd455a", + "label": { + "name": "Confectionary", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "20a735de-c75b-4fdb-abaf-b8d71ef192f8" + } + }, + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "71178428-70aa-4491-b5b4-b8d93e7b04cf", + "position": 6, + "id": "02995d80-108f-4949-bd58-d04d670b388d", + "label": { + "name": "Dairy Products", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "71178428-70aa-4491-b5b4-b8d93e7b04cf" + } + }, + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "c58ed864-b5bf-4aac-88a1-007833c706c7", + "position": 7, + "id": "b20c178c-e719-4159-b199-91a6dd25dcd3", + "label": { + "name": "Frozen Foods", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "c58ed864-b5bf-4aac-88a1-007833c706c7" + } + }, + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "f398f1a4-ce53-42df-95d4-8a3403bb6a38", + "position": 8, + "id": "5ff12e47-9b84-46d2-aabf-da4165a68f65", + "label": { + "name": "Fruits", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "f398f1a4-ce53-42df-95d4-8a3403bb6a38" + } + }, + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "fd936065-3d53-4844-99df-9332f1bf0c8a", + "position": 9, + "id": "e0ec7da9-c0b8-4d78-a5b8-591c99d87370", + "label": { + "name": "Grains", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "fd936065-3d53-4844-99df-9332f1bf0c8a" + } + }, + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "db7b685a-4aeb-4ebd-9b64-0c14827d9eaf", + "position": 10, + "id": "3dc2d2e7-274e-40ec-8ba1-09ce1820b29b", + "label": { + "name": "Health Foods", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "db7b685a-4aeb-4ebd-9b64-0c14827d9eaf" + } + }, + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "28bbdab4-7eab-4fb2-b0e1-b0f2c10e489b", + "position": 11, + "id": "e30fa937-4bb1-4ff9-b163-2da67e2749ca", + "label": { + "name": "Household", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "28bbdab4-7eab-4fb2-b0e1-b0f2c10e489b" + } + }, + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "cf136576-1929-4fc9-a3da-34c49ff58920", + "position": 12, + "id": "ecd715af-fafe-4d32-a376-538e476bf215", + "label": { + "name": "Meat", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "cf136576-1929-4fc9-a3da-34c49ff58920" + } + }, + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "5b7d69d0-4d9f-48f9-96f1-8cb843227baa", + "position": 13, + "id": "5ded867c-473f-456d-b0a0-83cae279df71", + "label": { + "name": "Meat Products", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "5b7d69d0-4d9f-48f9-96f1-8cb843227baa" + } + }, + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "2a035661-fd5d-462c-8eb0-6b78af982e0c", + "position": 14, + "id": "eb88d477-cd50-4b84-a1bb-5adc077d38e5", + "label": { + "name": "Other", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "2a035661-fd5d-462c-8eb0-6b78af982e0c" + } + }, + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "af147838-d114-4a92-bd0f-08f05f59bbe5", + "position": 15, + "id": "ab7e96e3-f8d5-4e4e-91ee-b966bd980cf0", + "label": { + "name": "Produce", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "af147838-d114-4a92-bd0f-08f05f59bbe5" + } + }, + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "cf7672b8-036a-45a4-8323-6a167d2731be", + "position": 16, + "id": "3fcf5e5a-f8e2-4174-be79-2496a1cb505a", + "label": { + "name": "Regular", + "color": "#2E7D32FF", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "cf7672b8-036a-45a4-8323-6a167d2731be" + } + }, + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "bbcfaf8b-02e6-4c3d-98a6-6863b36bef18", + "position": 17, + "id": "e768c9e7-c568-44d1-a263-081d93fd1298", + "label": { + "name": "Seafood", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "bbcfaf8b-02e6-4c3d-98a6-6863b36bef18" + } + }, + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "1c59a263-227a-4f43-a450-d53ca1485b36", + "position": 18, + "id": "f8a78147-c6d1-4a86-b159-5f178ae72089", + "label": { + "name": "Snacks", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "1c59a263-227a-4f43-a450-d53ca1485b36" + } + }, + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "189099a9-0033-4783-804a-ec6805e7d557", + "position": 19, + "id": "23253f2f-bc71-4ecf-837c-d1697738b505", + "label": { + "name": "Spices", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "189099a9-0033-4783-804a-ec6805e7d557" + } + }, + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "c28efdde-5993-4044-b824-f111f3a118ef", + "position": 20, + "id": "706d656b-3755-46f7-8c12-c9196730baf2", + "label": { + "name": "Sweets", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "c28efdde-5993-4044-b824-f111f3a118ef" + } + }, + { + "shoppingListId": "e9d78ff2-4b23-4b77-a3a8-464827100b46", + "labelId": "3f151d15-27f9-41c7-9dfc-2ae1024b1c7c", + "position": 21, + "id": "d9d60d8d-f2de-4636-864f-d7262e24ead3", + "label": { + "name": "Vegetables", + "color": "#E0E0E0", + "groupId": "9ed7c880-3e85-4955-9318-1443d6e080fe", + "id": "3f151d15-27f9-41c7-9dfc-2ae1024b1c7c" + } + } + ] + } + ], + "next": null, + "previous": null +} diff --git a/tests/components/mealie/snapshots/test_todo.ambr b/tests/components/mealie/snapshots/test_todo.ambr new file mode 100644 index 00000000000..a580862535e --- /dev/null +++ b/tests/components/mealie/snapshots/test_todo.ambr @@ -0,0 +1,156 @@ +# serializer version: 1 +# name: test_entities[todo.mealie_freezer-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'todo', + 'entity_category': None, + 'entity_id': 'todo.mealie_freezer', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Freezer', + 'platform': 'mealie', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'shopping_list', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_e9d78ff2-4b23-4b77-a3a8-464827100b46', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[todo.mealie_freezer-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mealie Freezer', + 'supported_features': , + }), + 'context': , + 'entity_id': 'todo.mealie_freezer', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3', + }) +# --- +# name: test_entities[todo.mealie_special_groceries-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'todo', + 'entity_category': None, + 'entity_id': 'todo.mealie_special_groceries', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Special groceries', + 'platform': 'mealie', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'shopping_list', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_f8438635-8211-4be8-80d0-0aa42e37a5f2', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[todo.mealie_special_groceries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mealie Special groceries', + 'supported_features': , + }), + 'context': , + 'entity_id': 'todo.mealie_special_groceries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3', + }) +# --- +# name: test_entities[todo.mealie_supermarket-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'todo', + 'entity_category': None, + 'entity_id': 'todo.mealie_supermarket', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Supermarket', + 'platform': 'mealie', + 'previous_unique_id': None, + 'supported_features': , + 'translation_key': 'shopping_list', + 'unique_id': 'bf1c62fe-4941-4332-9886-e54e88dbdba0_27edbaab-2ec6-441f-8490-0283ea77585f', + 'unit_of_measurement': None, + }) +# --- +# name: test_entities[todo.mealie_supermarket-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mealie Supermarket', + 'supported_features': , + }), + 'context': , + 'entity_id': 'todo.mealie_supermarket', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3', + }) +# --- +# name: test_get_todo_list_items + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mealie Supermarket', + 'supported_features': , + }), + 'context': , + 'entity_id': 'todo.mealie_supermarket', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3', + }) +# --- diff --git a/tests/components/mealie/test_calendar.py b/tests/components/mealie/test_calendar.py index 9df2c1810fd..d11fe5d2354 100644 --- a/tests/components/mealie/test_calendar.py +++ b/tests/components/mealie/test_calendar.py @@ -2,10 +2,11 @@ from datetime import date from http import HTTPStatus -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch from syrupy.assertion import SnapshotAssertion +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -40,7 +41,8 @@ async def test_entities( mock_config_entry: MockConfigEntry, ) -> None: """Test the API returns the calendar.""" - await setup_integration(hass, mock_config_entry) + with patch("homeassistant.components.mealie.PLATFORMS", [Platform.CALENDAR]): + await setup_integration(hass, mock_config_entry) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/mealie/test_init.py b/tests/components/mealie/test_init.py index 5a7a5387897..bec03ab3719 100644 --- a/tests/components/mealie/test_init.py +++ b/tests/components/mealie/test_init.py @@ -55,7 +55,7 @@ async def test_load_unload_entry( (MealieAuthenticationError, ConfigEntryState.SETUP_ERROR), ], ) -async def test_initialization_failure( +async def test_mealplan_initialization_failure( hass: HomeAssistant, mock_mealie_client: AsyncMock, mock_config_entry: MockConfigEntry, @@ -68,3 +68,47 @@ async def test_initialization_failure( await setup_integration(hass, mock_config_entry) assert mock_config_entry.state is state + + +@pytest.mark.parametrize( + ("exc", "state"), + [ + (MealieConnectionError, ConfigEntryState.SETUP_RETRY), + (MealieAuthenticationError, ConfigEntryState.SETUP_ERROR), + ], +) +async def test_shoppingitems_initialization_failure( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + exc: Exception, + state: ConfigEntryState, +) -> None: + """Test initialization failure.""" + mock_mealie_client.get_shopping_items.side_effect = exc + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is state + + +@pytest.mark.parametrize( + ("exc", "state"), + [ + (MealieConnectionError, ConfigEntryState.SETUP_ERROR), + (MealieAuthenticationError, ConfigEntryState.SETUP_ERROR), + ], +) +async def test_shoppinglists_initialization_failure( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + exc: Exception, + state: ConfigEntryState, +) -> None: + """Test initialization failure.""" + mock_mealie_client.get_shopping_lists.side_effect = exc + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is state diff --git a/tests/components/mealie/test_todo.py b/tests/components/mealie/test_todo.py new file mode 100644 index 00000000000..2b65d8b468f --- /dev/null +++ b/tests/components/mealie/test_todo.py @@ -0,0 +1,149 @@ +"""Tests for the Mealie todo.""" + +from unittest.mock import AsyncMock, patch + +from aiomealie.exceptions import MealieError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.todo import DOMAIN as TODO_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test todo entities.""" + with patch("homeassistant.components.mealie.PLATFORMS", [Platform.TODO]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_add_todo_list_item( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test for adding a To-do Item.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + TODO_DOMAIN, + "add_item", + {"item": "Soda"}, + target={ATTR_ENTITY_ID: "todo.mealie_supermarket"}, + blocking=True, + ) + + mock_mealie_client.add_shopping_item.assert_called_once() + + +async def test_add_todo_list_item_error( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test for failing to add a To-do Item.""" + await setup_integration(hass, mock_config_entry) + + mock_mealie_client.add_shopping_item.side_effect = MealieError + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + TODO_DOMAIN, + "add_item", + {"item": "Soda"}, + target={ATTR_ENTITY_ID: "todo.mealie_supermarket"}, + blocking=True, + ) + + +async def test_update_todo_list_item( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test for updating a To-do Item.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + TODO_DOMAIN, + "update_item", + {"item": "aubergine", "rename": "Eggplant", "status": "completed"}, + target={ATTR_ENTITY_ID: "todo.mealie_supermarket"}, + blocking=True, + ) + + mock_mealie_client.update_shopping_item.assert_called_once() + + +async def test_update_todo_list_item_error( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test for failing to update a To-do Item.""" + await setup_integration(hass, mock_config_entry) + + mock_mealie_client.update_shopping_item.side_effect = MealieError + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + TODO_DOMAIN, + "update_item", + {"item": "aubergine", "rename": "Eggplant", "status": "completed"}, + target={ATTR_ENTITY_ID: "todo.mealie_supermarket"}, + blocking=True, + ) + + +async def test_delete_todo_list_item( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test for deleting a To-do Item.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + TODO_DOMAIN, + "remove_item", + {"item": "aubergine"}, + target={ATTR_ENTITY_ID: "todo.mealie_supermarket"}, + blocking=True, + ) + + mock_mealie_client.delete_shopping_item.assert_called_once() + + +async def test_delete_todo_list_item_error( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test for failing to delete a To-do Item.""" + await setup_integration(hass, mock_config_entry) + + mock_mealie_client.delete_shopping_item = AsyncMock() + mock_mealie_client.delete_shopping_item.side_effect = MealieError + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + TODO_DOMAIN, + "remove_item", + {"item": "aubergine"}, + target={ATTR_ENTITY_ID: "todo.mealie_supermarket"}, + blocking=True, + )