mirror of
https://github.com/home-assistant/core.git
synced 2025-08-01 09:38:21 +00:00
Add get recipes search service to Mealie integration (#149348)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
a21af78aa1
commit
91be25a292
@ -17,5 +17,7 @@ ATTR_INCLUDE_TAGS = "include_tags"
|
|||||||
ATTR_ENTRY_TYPE = "entry_type"
|
ATTR_ENTRY_TYPE = "entry_type"
|
||||||
ATTR_NOTE_TITLE = "note_title"
|
ATTR_NOTE_TITLE = "note_title"
|
||||||
ATTR_NOTE_TEXT = "note_text"
|
ATTR_NOTE_TEXT = "note_text"
|
||||||
|
ATTR_SEARCH_TERMS = "search_terms"
|
||||||
|
ATTR_RESULT_LIMIT = "result_limit"
|
||||||
|
|
||||||
MIN_REQUIRED_MEALIE_VERSION = AwesomeVersion("v1.0.0")
|
MIN_REQUIRED_MEALIE_VERSION = AwesomeVersion("v1.0.0")
|
||||||
|
@ -30,6 +30,9 @@
|
|||||||
"get_recipe": {
|
"get_recipe": {
|
||||||
"service": "mdi:map"
|
"service": "mdi:map"
|
||||||
},
|
},
|
||||||
|
"get_recipes": {
|
||||||
|
"service": "mdi:book-open-page-variant"
|
||||||
|
},
|
||||||
"import_recipe": {
|
"import_recipe": {
|
||||||
"service": "mdi:map-search"
|
"service": "mdi:map-search"
|
||||||
},
|
},
|
||||||
|
@ -32,6 +32,8 @@ from .const import (
|
|||||||
ATTR_NOTE_TEXT,
|
ATTR_NOTE_TEXT,
|
||||||
ATTR_NOTE_TITLE,
|
ATTR_NOTE_TITLE,
|
||||||
ATTR_RECIPE_ID,
|
ATTR_RECIPE_ID,
|
||||||
|
ATTR_RESULT_LIMIT,
|
||||||
|
ATTR_SEARCH_TERMS,
|
||||||
ATTR_START_DATE,
|
ATTR_START_DATE,
|
||||||
ATTR_URL,
|
ATTR_URL,
|
||||||
DOMAIN,
|
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 = "import_recipe"
|
||||||
SERVICE_IMPORT_RECIPE_SCHEMA = vol.Schema(
|
SERVICE_IMPORT_RECIPE_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
@ -159,6 +170,27 @@ async def _async_get_recipe(call: ServiceCall) -> ServiceResponse:
|
|||||||
return {"recipe": asdict(recipe)}
|
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:
|
async def _async_import_recipe(call: ServiceCall) -> ServiceResponse:
|
||||||
"""Import a recipe."""
|
"""Import a recipe."""
|
||||||
entry = _async_get_entry(call)
|
entry = _async_get_entry(call)
|
||||||
@ -242,6 +274,13 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
|||||||
schema=SERVICE_GET_RECIPE_SCHEMA,
|
schema=SERVICE_GET_RECIPE_SCHEMA,
|
||||||
supports_response=SupportsResponse.ONLY,
|
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(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_IMPORT_RECIPE,
|
SERVICE_IMPORT_RECIPE,
|
||||||
|
@ -24,6 +24,27 @@ get_recipe:
|
|||||||
selector:
|
selector:
|
||||||
text:
|
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:
|
import_recipe:
|
||||||
fields:
|
fields:
|
||||||
config_entry_id:
|
config_entry_id:
|
||||||
|
@ -109,6 +109,9 @@
|
|||||||
"recipe_not_found": {
|
"recipe_not_found": {
|
||||||
"message": "Recipe with ID or slug `{recipe_id}` 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": {
|
"could_not_import_recipe": {
|
||||||
"message": "Mealie could not import the recipe from the URL."
|
"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": {
|
"import_recipe": {
|
||||||
"name": "Import recipe",
|
"name": "Import recipe",
|
||||||
"description": "Imports a recipe from an URL",
|
"description": "Imports a recipe from an URL",
|
||||||
|
@ -8,6 +8,7 @@ from aiomealie import (
|
|||||||
Mealplan,
|
Mealplan,
|
||||||
MealplanResponse,
|
MealplanResponse,
|
||||||
Recipe,
|
Recipe,
|
||||||
|
RecipesResponse,
|
||||||
ShoppingItemsResponse,
|
ShoppingItemsResponse,
|
||||||
ShoppingListsResponse,
|
ShoppingListsResponse,
|
||||||
Statistics,
|
Statistics,
|
||||||
@ -63,6 +64,8 @@ def mock_mealie_client() -> Generator[AsyncMock]:
|
|||||||
)
|
)
|
||||||
recipe = Recipe.from_json(load_fixture("get_recipe.json", DOMAIN))
|
recipe = Recipe.from_json(load_fixture("get_recipe.json", DOMAIN))
|
||||||
client.get_recipe.return_value = recipe
|
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.import_recipe.return_value = recipe
|
||||||
client.get_shopping_lists.return_value = ShoppingListsResponse.from_json(
|
client.get_shopping_lists.return_value = ShoppingListsResponse.from_json(
|
||||||
load_fixture("get_shopping_lists.json", DOMAIN)
|
load_fixture("get_shopping_lists.json", DOMAIN)
|
||||||
|
1692
tests/components/mealie/fixtures/get_recipes.json
Normal file
1692
tests/components/mealie/fixtures/get_recipes.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -21,6 +21,8 @@ from homeassistant.components.mealie.const import (
|
|||||||
ATTR_NOTE_TEXT,
|
ATTR_NOTE_TEXT,
|
||||||
ATTR_NOTE_TITLE,
|
ATTR_NOTE_TITLE,
|
||||||
ATTR_RECIPE_ID,
|
ATTR_RECIPE_ID,
|
||||||
|
ATTR_RESULT_LIMIT,
|
||||||
|
ATTR_SEARCH_TERMS,
|
||||||
ATTR_START_DATE,
|
ATTR_START_DATE,
|
||||||
ATTR_URL,
|
ATTR_URL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@ -28,6 +30,7 @@ from homeassistant.components.mealie.const import (
|
|||||||
from homeassistant.components.mealie.services import (
|
from homeassistant.components.mealie.services import (
|
||||||
SERVICE_GET_MEALPLAN,
|
SERVICE_GET_MEALPLAN,
|
||||||
SERVICE_GET_RECIPE,
|
SERVICE_GET_RECIPE,
|
||||||
|
SERVICE_GET_RECIPES,
|
||||||
SERVICE_IMPORT_RECIPE,
|
SERVICE_IMPORT_RECIPE,
|
||||||
SERVICE_SET_MEALPLAN,
|
SERVICE_SET_MEALPLAN,
|
||||||
SERVICE_SET_RANDOM_MEALPLAN,
|
SERVICE_SET_RANDOM_MEALPLAN,
|
||||||
@ -150,6 +153,42 @@ async def test_service_recipe(
|
|||||||
assert response == snapshot
|
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(
|
async def test_service_import_recipe(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_mealie_client: AsyncMock,
|
mock_mealie_client: AsyncMock,
|
||||||
@ -332,6 +371,22 @@ async def test_service_set_mealplan(
|
|||||||
ServiceValidationError,
|
ServiceValidationError,
|
||||||
"Recipe with ID or slug `recipe_id` not found",
|
"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,
|
SERVICE_IMPORT_RECIPE,
|
||||||
{ATTR_URL: "http://example.com"},
|
{ATTR_URL: "http://example.com"},
|
||||||
@ -402,6 +457,11 @@ async def test_services_connection_error(
|
|||||||
[
|
[
|
||||||
(SERVICE_GET_MEALPLAN, {}),
|
(SERVICE_GET_MEALPLAN, {}),
|
||||||
(SERVICE_GET_RECIPE, {ATTR_RECIPE_ID: "recipe_id"}),
|
(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_IMPORT_RECIPE, {ATTR_URL: "http://example.com"}),
|
||||||
(
|
(
|
||||||
SERVICE_SET_RANDOM_MEALPLAN,
|
SERVICE_SET_RANDOM_MEALPLAN,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user