Add get recipes search service to Mealie integration (#149348)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
lucasfijen 2025-07-30 15:43:10 +02:00 committed by GitHub
parent a21af78aa1
commit 91be25a292
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 3079 additions and 0 deletions

View File

@ -17,5 +17,7 @@ ATTR_INCLUDE_TAGS = "include_tags"
ATTR_ENTRY_TYPE = "entry_type"
ATTR_NOTE_TITLE = "note_title"
ATTR_NOTE_TEXT = "note_text"
ATTR_SEARCH_TERMS = "search_terms"
ATTR_RESULT_LIMIT = "result_limit"
MIN_REQUIRED_MEALIE_VERSION = AwesomeVersion("v1.0.0")

View File

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

View File

@ -32,6 +32,8 @@ from .const import (
ATTR_NOTE_TEXT,
ATTR_NOTE_TITLE,
ATTR_RECIPE_ID,
ATTR_RESULT_LIMIT,
ATTR_SEARCH_TERMS,
ATTR_START_DATE,
ATTR_URL,
DOMAIN,
@ -55,6 +57,15 @@ SERVICE_GET_RECIPE_SCHEMA = vol.Schema(
}
)
SERVICE_GET_RECIPES = "get_recipes"
SERVICE_GET_RECIPES_SCHEMA = vol.Schema(
{
vol.Required(ATTR_CONFIG_ENTRY_ID): str,
vol.Optional(ATTR_SEARCH_TERMS): str,
vol.Optional(ATTR_RESULT_LIMIT): int,
}
)
SERVICE_IMPORT_RECIPE = "import_recipe"
SERVICE_IMPORT_RECIPE_SCHEMA = vol.Schema(
{
@ -159,6 +170,27 @@ async def _async_get_recipe(call: ServiceCall) -> ServiceResponse:
return {"recipe": asdict(recipe)}
async def _async_get_recipes(call: ServiceCall) -> ServiceResponse:
"""Get recipes."""
entry = _async_get_entry(call)
search_terms = call.data.get(ATTR_SEARCH_TERMS)
result_limit = call.data.get(ATTR_RESULT_LIMIT, 10)
client = entry.runtime_data.client
try:
recipes = await client.get_recipes(search=search_terms, per_page=result_limit)
except MealieConnectionError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="connection_error",
) from err
except MealieNotFoundError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="no_recipes_found",
) from err
return {"recipes": asdict(recipes)}
async def _async_import_recipe(call: ServiceCall) -> ServiceResponse:
"""Import a recipe."""
entry = _async_get_entry(call)
@ -242,6 +274,13 @@ def async_setup_services(hass: HomeAssistant) -> None:
schema=SERVICE_GET_RECIPE_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
SERVICE_GET_RECIPES,
_async_get_recipes,
schema=SERVICE_GET_RECIPES_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
SERVICE_IMPORT_RECIPE,

View File

@ -24,6 +24,27 @@ get_recipe:
selector:
text:
get_recipes:
fields:
config_entry_id:
required: true
selector:
config_entry:
integration: mealie
search_terms:
required: false
selector:
text:
result_limit:
required: false
default: 10
selector:
number:
min: 1
max: 100
mode: box
unit_of_measurement: recipes
import_recipe:
fields:
config_entry_id:

View File

@ -109,6 +109,9 @@
"recipe_not_found": {
"message": "Recipe with ID or slug `{recipe_id}` not found."
},
"no_recipes_found": {
"message": "No recipes found matching your search."
},
"could_not_import_recipe": {
"message": "Mealie could not import the recipe from the URL."
},
@ -176,6 +179,24 @@
}
}
},
"get_recipes": {
"name": "Get recipes",
"description": "Searches for recipes with any matching properties in Mealie",
"fields": {
"config_entry_id": {
"name": "[%key:component::mealie::services::get_mealplan::fields::config_entry_id::name%]",
"description": "[%key:component::mealie::services::get_mealplan::fields::config_entry_id::description%]"
},
"search_terms": {
"name": "Search terms",
"description": "Terms to search for in recipe properties."
},
"result_limit": {
"name": "Result limit",
"description": "Maximum number of recipes to return (default: 10)."
}
}
},
"import_recipe": {
"name": "Import recipe",
"description": "Imports a recipe from an URL",

View File

@ -8,6 +8,7 @@ from aiomealie import (
Mealplan,
MealplanResponse,
Recipe,
RecipesResponse,
ShoppingItemsResponse,
ShoppingListsResponse,
Statistics,
@ -63,6 +64,8 @@ def mock_mealie_client() -> Generator[AsyncMock]:
)
recipe = Recipe.from_json(load_fixture("get_recipe.json", DOMAIN))
client.get_recipe.return_value = recipe
recipes = RecipesResponse.from_json(load_fixture("get_recipes.json", DOMAIN))
client.get_recipes.return_value = recipes
client.import_recipe.return_value = recipe
client.get_shopping_lists.return_value = ShoppingListsResponse.from_json(
load_fixture("get_shopping_lists.json", DOMAIN)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,8 @@ from homeassistant.components.mealie.const import (
ATTR_NOTE_TEXT,
ATTR_NOTE_TITLE,
ATTR_RECIPE_ID,
ATTR_RESULT_LIMIT,
ATTR_SEARCH_TERMS,
ATTR_START_DATE,
ATTR_URL,
DOMAIN,
@ -28,6 +30,7 @@ from homeassistant.components.mealie.const import (
from homeassistant.components.mealie.services import (
SERVICE_GET_MEALPLAN,
SERVICE_GET_RECIPE,
SERVICE_GET_RECIPES,
SERVICE_IMPORT_RECIPE,
SERVICE_SET_MEALPLAN,
SERVICE_SET_RANDOM_MEALPLAN,
@ -150,6 +153,42 @@ async def test_service_recipe(
assert response == snapshot
@pytest.mark.parametrize(
"service_data",
[
# Default call
{ATTR_CONFIG_ENTRY_ID: "mock_entry_id"},
# With search terms and result limit
{
ATTR_CONFIG_ENTRY_ID: "mock_entry_id",
ATTR_SEARCH_TERMS: "pasta",
ATTR_RESULT_LIMIT: 5,
},
],
)
async def test_service_get_recipes(
hass: HomeAssistant,
mock_mealie_client: AsyncMock,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
service_data: dict,
) -> None:
"""Test the get_recipes service."""
await setup_integration(hass, mock_config_entry)
# Patch entry_id into service_data for each run
service_data = {**service_data, ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id}
response = await hass.services.async_call(
DOMAIN,
SERVICE_GET_RECIPES,
service_data,
blocking=True,
return_response=True,
)
assert response == snapshot
async def test_service_import_recipe(
hass: HomeAssistant,
mock_mealie_client: AsyncMock,
@ -332,6 +371,22 @@ async def test_service_set_mealplan(
ServiceValidationError,
"Recipe with ID or slug `recipe_id` not found",
),
(
SERVICE_GET_RECIPES,
{},
"get_recipes",
MealieConnectionError,
HomeAssistantError,
"Error connecting to Mealie instance",
),
(
SERVICE_GET_RECIPES,
{ATTR_SEARCH_TERMS: "pasta"},
"get_recipes",
MealieNotFoundError,
ServiceValidationError,
"No recipes found matching your search",
),
(
SERVICE_IMPORT_RECIPE,
{ATTR_URL: "http://example.com"},
@ -402,6 +457,11 @@ async def test_services_connection_error(
[
(SERVICE_GET_MEALPLAN, {}),
(SERVICE_GET_RECIPE, {ATTR_RECIPE_ID: "recipe_id"}),
(SERVICE_GET_RECIPES, {}),
(
SERVICE_GET_RECIPES,
{ATTR_SEARCH_TERMS: "pasta", ATTR_RESULT_LIMIT: 5},
),
(SERVICE_IMPORT_RECIPE, {ATTR_URL: "http://example.com"}),
(
SERVICE_SET_RANDOM_MEALPLAN,