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
pull/121619/head
Andrew Jackson 2024-07-09 17:39:22 +01:00 committed by GitHub
parent 5b25c24539
commit 898803abe9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 1692 additions and 19 deletions

View File

@ -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)

View File

@ -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())

View File

@ -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

View File

@ -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

View File

@ -1,4 +1,11 @@
{
"entity": {
"todo": {
"shopping_list": {
"default": "mdi:basket"
}
}
},
"services": {
"get_mealplan": "mdi:food",
"get_recipe": "mdi:map"

View File

@ -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": {

View File

@ -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
)

View File

@ -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)
)

View File

@ -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
}

View File

@ -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
}

View File

@ -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': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'todo',
'entity_category': None,
'entity_id': 'todo.mealie_freezer',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Freezer',
'platform': 'mealie',
'previous_unique_id': None,
'supported_features': <TodoListEntityFeature: 15>,
'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': <TodoListEntityFeature: 15>,
}),
'context': <ANY>,
'entity_id': 'todo.mealie_freezer',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '3',
})
# ---
# name: test_entities[todo.mealie_special_groceries-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'todo',
'entity_category': None,
'entity_id': 'todo.mealie_special_groceries',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'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': <TodoListEntityFeature: 15>,
'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': <TodoListEntityFeature: 15>,
}),
'context': <ANY>,
'entity_id': 'todo.mealie_special_groceries',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '3',
})
# ---
# name: test_entities[todo.mealie_supermarket-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'todo',
'entity_category': None,
'entity_id': 'todo.mealie_supermarket',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Supermarket',
'platform': 'mealie',
'previous_unique_id': None,
'supported_features': <TodoListEntityFeature: 15>,
'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': <TodoListEntityFeature: 15>,
}),
'context': <ANY>,
'entity_id': 'todo.mealie_supermarket',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '3',
})
# ---
# name: test_get_todo_list_items
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mealie Supermarket',
'supported_features': <TodoListEntityFeature: 15>,
}),
'context': <ANY>,
'entity_id': 'todo.mealie_supermarket',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '3',
})
# ---

View File

@ -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)

View File

@ -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

View File

@ -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,
)