Mealie add get shopping list items action (#163090)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Andrew Jackson
2026-02-16 20:43:36 +00:00
committed by GitHub
parent 667a77502d
commit 76ebc134f3
7 changed files with 248 additions and 2 deletions

View File

@@ -33,6 +33,9 @@
"get_recipes": {
"service": "mdi:book-open-page-variant"
},
"get_shopping_list_items": {
"service": "mdi:basket"
},
"import_recipe": {
"service": "mdi:map-search"
},

View File

@@ -12,6 +12,7 @@ from aiomealie import (
from awesomeversion import AwesomeVersion
import voluptuous as vol
from homeassistant.components.todo import DOMAIN as TODO_DOMAIN
from homeassistant.const import ATTR_CONFIG_ENTRY_ID, ATTR_DATE
from homeassistant.core import (
HomeAssistant,
@@ -64,6 +65,8 @@ SERVICE_GET_RECIPES_SCHEMA = vol.Schema(
}
)
SERVICE_GET_SHOPPING_LIST_ITEMS = "get_shopping_list_items"
SERVICE_IMPORT_RECIPE = "import_recipe"
SERVICE_IMPORT_RECIPE_SCHEMA = vol.Schema(
{
@@ -321,3 +324,12 @@ def async_setup_services(hass: HomeAssistant) -> None:
schema=SERVICE_SET_MEALPLAN_SCHEMA,
supports_response=SupportsResponse.OPTIONAL,
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_GET_SHOPPING_LIST_ITEMS,
entity_domain=TODO_DOMAIN,
schema=None,
func="async_get_shopping_list_items",
supports_response=SupportsResponse.ONLY,
)

View File

@@ -45,6 +45,12 @@ get_recipes:
mode: box
unit_of_measurement: recipes
get_shopping_list_items:
target:
entity:
integration: mealie
domain: todo
import_recipe:
fields:
config_entry_id:

View File

@@ -147,6 +147,9 @@
"setup_failed": {
"message": "Could not connect to the Mealie instance."
},
"shopping_list_not_found": {
"message": "Shopping list with name or ID `{shopping_list}` not found."
},
"update_failed_mealplan": {
"message": "Could not fetch mealplan data."
},
@@ -227,6 +230,10 @@
},
"name": "Get recipes"
},
"get_shopping_list_items": {
"description": "Gets items from a shopping list in Mealie",
"name": "Get shopping list items"
},
"import_recipe": {
"description": "Imports a recipe from an URL",
"fields": {

View File

@@ -2,7 +2,15 @@
from __future__ import annotations
from aiomealie import MealieError, MutateShoppingItem, ShoppingItem, ShoppingList
from dataclasses import asdict
from aiomealie import (
MealieConnectionError,
MealieError,
MutateShoppingItem,
ShoppingItem,
ShoppingList,
)
from homeassistant.components.todo import (
DOMAIN as TODO_DOMAIN,
@@ -11,7 +19,7 @@ from homeassistant.components.todo import (
TodoListEntity,
TodoListEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, ServiceResponse
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -265,3 +273,18 @@ class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity):
def available(self) -> bool:
"""Return False if shopping list no longer available."""
return super().available and self._shopping_list_id in self.coordinator.data
async def async_get_shopping_list_items(self) -> ServiceResponse:
"""Get structured shopping list items."""
client = self.coordinator.client
try:
shopping_items = await client.get_shopping_items(self._shopping_list_id)
except MealieConnectionError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="connection_error",
) from err
return {
"name": self.shopping_list.name,
"items": [asdict(item) for item in shopping_items.items],
}

View File

@@ -1637,6 +1637,159 @@
}),
})
# ---
# name: test_service_get_shopping_list_items
dict({
'todo.mealie_supermarket': dict({
'items': list([
dict({
'checked': False,
'disable_amount': None,
'display': '2 Apples',
'food': None,
'food_id': None,
'is_food': None,
'item_id': 'f45430f7-3edf-45a9-a50f-73bb375090be',
'label': None,
'label_id': None,
'list_id': '9ce096fe-ded2-4077-877d-78ba450ab13e',
'note': 'Apples',
'position': 0,
'quantity': 2.0,
'unit': None,
'unit_id': None,
}),
dict({
'checked': False,
'disable_amount': False,
'display': '1 can acorn squash',
'food': dict({
'aliases': list([
]),
'created_at': datetime.datetime(2024, 5, 14, 14, 45, 4, 454134),
'description': '',
'extras': dict({
}),
'food_id': '09322430-d24c-4b1a-abb6-22b6ed3a88f5',
'households_with_ingredient_food': None,
'label': None,
'label_id': None,
'name': 'acorn squash',
'plural_name': None,
'updated_at': datetime.datetime(2024, 5, 14, 14, 45, 4, 454141),
}),
'food_id': '09322430-d24c-4b1a-abb6-22b6ed3a88f5',
'is_food': True,
'item_id': '84d8fd74-8eb0-402e-84b6-71f251bfb7cc',
'label': None,
'label_id': None,
'list_id': '9ce096fe-ded2-4077-877d-78ba450ab13e',
'note': '',
'position': 1,
'quantity': 1.0,
'unit': dict({
'abbreviation': '',
'aliases': list([
]),
'created_at': datetime.datetime(2024, 5, 14, 14, 45, 2, 464122),
'description': '',
'extras': dict({
}),
'fraction': True,
'name': 'can',
'plural_abbreviation': '',
'plural_name': None,
'unit_id': '7bf539d4-fc78-48bc-b48e-c35ccccec34a',
'updated_at': datetime.datetime(2024, 5, 14, 14, 45, 2, 464124),
'use_abbreviation': False,
}),
'unit_id': '7bf539d4-fc78-48bc-b48e-c35ccccec34a',
}),
dict({
'checked': False,
'disable_amount': False,
'display': 'aubergine',
'food': dict({
'aliases': list([
]),
'created_at': datetime.datetime(2024, 5, 14, 14, 45, 3, 868792),
'description': '',
'extras': dict({
}),
'food_id': '96801494-4e26-4148-849a-8155deb76327',
'households_with_ingredient_food': None,
'label': None,
'label_id': None,
'name': 'aubergine',
'plural_name': None,
'updated_at': datetime.datetime(2024, 5, 14, 14, 45, 3, 868794),
}),
'food_id': '96801494-4e26-4148-849a-8155deb76327',
'is_food': True,
'item_id': '69913b9a-7c75-4935-abec-297cf7483f88',
'label': None,
'label_id': None,
'list_id': '9ce096fe-ded2-4077-877d-78ba450ab13e',
'note': '',
'position': 2,
'quantity': 0.0,
'unit': None,
'unit_id': None,
}),
dict({
'checked': False,
'disable_amount': None,
'display': '1 US cup flour',
'food': dict({
'aliases': list([
]),
'created_at': datetime.datetime(2024, 8, 25, 13, 29, 29, 40354, tzinfo=datetime.timezone.utc),
'description': '',
'extras': dict({
}),
'food_id': '8d2ef4d7-bfc2-4420-9cba-152016c1ee7c',
'households_with_ingredient_food': list([
]),
'label': None,
'label_id': None,
'name': 'flour',
'plural_name': None,
'updated_at': datetime.datetime(2024, 8, 25, 13, 29, 29, 40371, tzinfo=datetime.timezone.utc),
}),
'food_id': '8d2ef4d7-bfc2-4420-9cba-152016c1ee7c',
'is_food': None,
'item_id': '22b389bb-e079-481c-915d-394e5edb20a5',
'label': dict({
'label_id': '0e55cae5-6037-4cbb-8d4f-1042cbb83fd0',
'name': 'Household',
}),
'label_id': None,
'list_id': 'a33af640-4704-453c-ab03-a95a393bf1c4',
'note': '',
'position': 0,
'quantity': 1.0,
'unit': dict({
'abbreviation': 'US cup',
'aliases': list([
]),
'created_at': datetime.datetime(2024, 8, 25, 13, 29, 25, 477518, tzinfo=datetime.timezone.utc),
'description': '',
'extras': dict({
}),
'fraction': True,
'name': 'US cup',
'plural_abbreviation': '',
'plural_name': None,
'unit_id': '89765d44-8412-4ab5-a6de-594aa8eac44c',
'updated_at': datetime.datetime(2024, 8, 25, 13, 29, 25, 477535, tzinfo=datetime.timezone.utc),
'use_abbreviation': False,
}),
'unit_id': '89765d44-8412-4ab5-a6de-594aa8eac44c',
}),
]),
'name': 'Supermarket',
}),
})
# ---
# name: test_service_import_recipe
dict({
'recipe': dict({

View File

@@ -31,6 +31,7 @@ from homeassistant.components.mealie.services import (
SERVICE_GET_MEALPLAN,
SERVICE_GET_RECIPE,
SERVICE_GET_RECIPES,
SERVICE_GET_SHOPPING_LIST_ITEMS,
SERVICE_IMPORT_RECIPE,
SERVICE_SET_MEALPLAN,
SERVICE_SET_RANDOM_MEALPLAN,
@@ -395,6 +396,47 @@ async def test_service_set_mealplan_invalid_entry_type(
mock_mealie_client.set_mealplan.assert_not_called()
async def test_service_get_shopping_list_items(
hass: HomeAssistant,
mock_mealie_client: AsyncMock,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test the get_shopping_list_items service."""
await setup_integration(hass, mock_config_entry)
response = await hass.services.async_call(
DOMAIN,
SERVICE_GET_SHOPPING_LIST_ITEMS,
target={"entity_id": "todo.mealie_supermarket"},
blocking=True,
return_response=True,
)
assert response == snapshot
async def test_service_get_shopping_list_items_connection_error(
hass: HomeAssistant,
mock_mealie_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the get_shopping_list_items service with connection error."""
await setup_integration(hass, mock_config_entry)
mock_mealie_client.get_shopping_items.side_effect = MealieConnectionError
with pytest.raises(HomeAssistantError, match="Error connecting to Mealie instance"):
await hass.services.async_call(
DOMAIN,
SERVICE_GET_SHOPPING_LIST_ITEMS,
target={"entity_id": "todo.mealie_supermarket"},
blocking=True,
return_response=True,
)
@pytest.mark.parametrize(
("service", "payload", "function", "exception", "raised_exception", "message"),
[