diff --git a/homeassistant/components/mealie/const.py b/homeassistant/components/mealie/const.py index 800cfd21db3..95802bfc02a 100644 --- a/homeassistant/components/mealie/const.py +++ b/homeassistant/components/mealie/const.py @@ -14,5 +14,6 @@ ATTR_END_DATE = "end_date" ATTR_RECIPE_ID = "recipe_id" ATTR_URL = "url" ATTR_INCLUDE_TAGS = "include_tags" +ATTR_ENTRY_TYPE = "entry_type" MIN_REQUIRED_MEALIE_VERSION = AwesomeVersion("v1.0.0") diff --git a/homeassistant/components/mealie/icons.json b/homeassistant/components/mealie/icons.json index f509985eb72..883779a8fb0 100644 --- a/homeassistant/components/mealie/icons.json +++ b/homeassistant/components/mealie/icons.json @@ -26,6 +26,7 @@ "services": { "get_mealplan": "mdi:food", "get_recipe": "mdi:map", - "import_recipe": "mdi:map-search" + "import_recipe": "mdi:map-search", + "set_random_mealplan": "mdi:dice-multiple" } } diff --git a/homeassistant/components/mealie/services.py b/homeassistant/components/mealie/services.py index d7be0885f3c..3b1257ff16d 100644 --- a/homeassistant/components/mealie/services.py +++ b/homeassistant/components/mealie/services.py @@ -4,10 +4,16 @@ from dataclasses import asdict from datetime import date from typing import cast -from aiomealie import MealieConnectionError, MealieNotFoundError, MealieValidationError +from aiomealie import ( + MealieConnectionError, + MealieNotFoundError, + MealieValidationError, + MealplanEntryType, +) import voluptuous as vol from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ATTR_DATE from homeassistant.core import ( HomeAssistant, ServiceCall, @@ -20,6 +26,7 @@ from homeassistant.helpers import config_validation as cv from .const import ( ATTR_CONFIG_ENTRY_ID, ATTR_END_DATE, + ATTR_ENTRY_TYPE, ATTR_INCLUDE_TAGS, ATTR_RECIPE_ID, ATTR_START_DATE, @@ -54,6 +61,15 @@ SERVICE_IMPORT_RECIPE_SCHEMA = vol.Schema( } ) +SERVICE_SET_RANDOM_MEALPLAN = "set_random_mealplan" +SERVICE_SET_RANDOM_MEALPLAN_SCHEMA = vol.Schema( + { + vol.Required(ATTR_CONFIG_ENTRY_ID): str, + vol.Required(ATTR_DATE): cv.date, + vol.Required(ATTR_ENTRY_TYPE): vol.In([x.lower() for x in MealplanEntryType]), + } +) + def async_get_entry(hass: HomeAssistant, config_entry_id: str) -> MealieConfigEntry: """Get the Mealie config entry.""" @@ -137,6 +153,23 @@ def setup_services(hass: HomeAssistant) -> None: return {"recipe": asdict(recipe)} return None + async def async_set_random_mealplan(call: ServiceCall) -> ServiceResponse: + """Set a random mealplan.""" + entry = async_get_entry(hass, call.data[ATTR_CONFIG_ENTRY_ID]) + mealplan_date = call.data[ATTR_DATE] + entry_type = MealplanEntryType(call.data[ATTR_ENTRY_TYPE]) + client = entry.runtime_data.client + try: + mealplan = await client.random_mealplan(mealplan_date, entry_type) + except MealieConnectionError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="connection_error", + ) from err + if call.return_response: + return {"mealplan": asdict(mealplan)} + return None + hass.services.async_register( DOMAIN, SERVICE_GET_MEALPLAN, @@ -158,3 +191,10 @@ def setup_services(hass: HomeAssistant) -> None: schema=SERVICE_IMPORT_RECIPE_SCHEMA, supports_response=SupportsResponse.OPTIONAL, ) + hass.services.async_register( + DOMAIN, + SERVICE_SET_RANDOM_MEALPLAN, + async_set_random_mealplan, + schema=SERVICE_SET_RANDOM_MEALPLAN_SCHEMA, + supports_response=SupportsResponse.OPTIONAL, + ) diff --git a/homeassistant/components/mealie/services.yaml b/homeassistant/components/mealie/services.yaml index 21043112579..c569df956e2 100644 --- a/homeassistant/components/mealie/services.yaml +++ b/homeassistant/components/mealie/services.yaml @@ -11,6 +11,7 @@ get_mealplan: end_date: selector: date: + get_recipe: fields: config_entry_id: @@ -22,6 +23,7 @@ get_recipe: required: true selector: text: + import_recipe: fields: config_entry_id: @@ -36,3 +38,23 @@ import_recipe: include_tags: selector: boolean: + +set_random_mealplan: + fields: + config_entry_id: + required: true + selector: + config_entry: + integration: mealie + date: + selector: + date: + entry_type: + selector: + select: + options: + - breakfast + - lunch + - dinner + - side + translation_key: mealplan_entry_type diff --git a/homeassistant/components/mealie/strings.json b/homeassistant/components/mealie/strings.json index 7e1b307d18b..3524b1a5fb3 100644 --- a/homeassistant/components/mealie/strings.json +++ b/homeassistant/components/mealie/strings.json @@ -157,6 +157,34 @@ "description": "Include tags from the website to the recipe." } } + }, + "set_random_mealplan": { + "name": "Set random mealplan", + "description": "Set a random mealplan for a specific date", + "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%]" + }, + "date": { + "name": "Date", + "description": "The date to set the mealplan for." + }, + "entry_type": { + "name": "Entry type", + "description": "The type of dish to randomize." + } + } + } + }, + "selector": { + "mealplan_entry_type": { + "options": { + "breakfast": "[%key:component::mealie::entity::calendar::breakfast::name%]", + "lunch": "[%key:component::mealie::entity::calendar::lunch::name%]", + "dinner": "[%key:component::mealie::entity::calendar::dinner::name%]", + "side": "[%key:component::mealie::entity::calendar::side::name%]" + } } } } diff --git a/tests/components/mealie/conftest.py b/tests/components/mealie/conftest.py index 2916159a799..208dd47ddf2 100644 --- a/tests/components/mealie/conftest.py +++ b/tests/components/mealie/conftest.py @@ -74,6 +74,9 @@ def mock_mealie_client() -> Generator[AsyncMock]: client.get_statistics.return_value = Statistics.from_json( load_fixture("statistics.json", DOMAIN) ) + client.random_mealplan.return_value = Mealplan.from_json( + load_fixture("mealplan.json", DOMAIN) + ) yield client diff --git a/tests/components/mealie/fixtures/mealplan.json b/tests/components/mealie/fixtures/mealplan.json new file mode 100644 index 00000000000..b540280d83f --- /dev/null +++ b/tests/components/mealie/fixtures/mealplan.json @@ -0,0 +1,34 @@ +{ + "date": "2024-01-22", + "entryType": "dinner", + "title": "", + "text": "", + "recipeId": "c5f00a93-71a2-4e48-900f-d9ad0bb9de93", + "id": 230, + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "recipe": { + "id": "c5f00a93-71a2-4e48-900f-d9ad0bb9de93", + "userId": "1ce8b5fe-04e8-4b80-aab1-d92c94685c6d", + "groupId": "0bf60b2e-ca89-42a9-94d4-8f67ca72b157", + "name": "Zoete aardappel curry traybake", + "slug": "zoete-aardappel-curry-traybake", + "image": "AiIo", + "recipeYield": "2 servings", + "totalTime": "40 Minutes", + "prepTime": null, + "cookTime": null, + "performTime": null, + "description": "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", + "recipeCategory": [], + "tags": [], + "tools": [], + "rating": null, + "orgURL": "https://chickslovefood.com/recept/zoete-aardappel-curry-traybake/", + "dateAdded": "2024-01-22", + "dateUpdated": "2024-01-22T00:27:46.324512", + "createdAt": "2024-01-22T00:27:46.327546", + "updateAt": "2024-01-22T00:27:46.327548", + "lastMade": null + } +} diff --git a/tests/components/mealie/snapshots/test_services.ambr b/tests/components/mealie/snapshots/test_services.ambr index 7bda79e14a6..293a1d8ee1d 100644 --- a/tests/components/mealie/snapshots/test_services.ambr +++ b/tests/components/mealie/snapshots/test_services.ambr @@ -675,3 +675,27 @@ }), }) # --- +# name: test_service_set_random_mealplan + dict({ + 'mealplan': dict({ + 'description': None, + 'entry_type': , + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'mealplan_date': datetime.date(2024, 1, 22), + 'mealplan_id': 230, + 'recipe': dict({ + 'description': "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", + 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', + 'image': 'AiIo', + 'name': 'Zoete aardappel curry traybake', + 'original_url': 'https://chickslovefood.com/recept/zoete-aardappel-curry-traybake/', + 'recipe_id': 'c5f00a93-71a2-4e48-900f-d9ad0bb9de93', + 'recipe_yield': '2 servings', + 'slug': 'zoete-aardappel-curry-traybake', + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + 'title': None, + 'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d', + }), + }) +# --- diff --git a/tests/components/mealie/test_services.py b/tests/components/mealie/test_services.py index 7af1bc251d4..b44d67d5eec 100644 --- a/tests/components/mealie/test_services.py +++ b/tests/components/mealie/test_services.py @@ -3,7 +3,12 @@ from datetime import date from unittest.mock import AsyncMock -from aiomealie import MealieConnectionError, MealieNotFoundError, MealieValidationError +from aiomealie import ( + MealieConnectionError, + MealieNotFoundError, + MealieValidationError, + MealplanEntryType, +) from freezegun.api import FrozenDateTimeFactory import pytest from syrupy import SnapshotAssertion @@ -11,6 +16,7 @@ from syrupy import SnapshotAssertion from homeassistant.components.mealie.const import ( ATTR_CONFIG_ENTRY_ID, ATTR_END_DATE, + ATTR_ENTRY_TYPE, ATTR_INCLUDE_TAGS, ATTR_RECIPE_ID, ATTR_START_DATE, @@ -21,7 +27,9 @@ from homeassistant.components.mealie.services import ( SERVICE_GET_MEALPLAN, SERVICE_GET_RECIPE, SERVICE_IMPORT_RECIPE, + SERVICE_SET_RANDOM_MEALPLAN, ) +from homeassistant.const import ATTR_DATE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError @@ -246,6 +254,74 @@ async def test_service_import_recipe_exceptions( ) +async def test_service_set_random_mealplan( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test the set_random_mealplan service.""" + + await setup_integration(hass, mock_config_entry) + + response = await hass.services.async_call( + DOMAIN, + SERVICE_SET_RANDOM_MEALPLAN, + { + ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, + ATTR_DATE: "2023-10-21", + ATTR_ENTRY_TYPE: "lunch", + }, + blocking=True, + return_response=True, + ) + assert response == snapshot + mock_mealie_client.random_mealplan.assert_called_with( + date(2023, 10, 21), MealplanEntryType.LUNCH + ) + + mock_mealie_client.random_mealplan.reset_mock() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_RANDOM_MEALPLAN, + { + ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, + ATTR_DATE: "2023-10-21", + ATTR_ENTRY_TYPE: "lunch", + }, + blocking=True, + return_response=False, + ) + mock_mealie_client.random_mealplan.assert_called_with( + date(2023, 10, 21), MealplanEntryType.LUNCH + ) + + +async def test_service_set_random_mealplan_exceptions( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the exceptions of the set_random_mealplan service.""" + + await setup_integration(hass, mock_config_entry) + + mock_mealie_client.random_mealplan.side_effect = MealieConnectionError + + with pytest.raises(HomeAssistantError, match="Error connecting to Mealie instance"): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_RANDOM_MEALPLAN, + { + ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, + ATTR_DATE: "2023-10-21", + ATTR_ENTRY_TYPE: "lunch", + }, + blocking=True, + return_response=True, + ) + + async def test_service_mealplan_connection_error( hass: HomeAssistant, mock_mealie_client: AsyncMock,