diff --git a/homeassistant/components/mealie/const.py b/homeassistant/components/mealie/const.py index 95802bfc02a..c040d665794 100644 --- a/homeassistant/components/mealie/const.py +++ b/homeassistant/components/mealie/const.py @@ -15,5 +15,7 @@ ATTR_RECIPE_ID = "recipe_id" ATTR_URL = "url" ATTR_INCLUDE_TAGS = "include_tags" ATTR_ENTRY_TYPE = "entry_type" +ATTR_NOTE_TITLE = "note_title" +ATTR_NOTE_TEXT = "note_text" MIN_REQUIRED_MEALIE_VERSION = AwesomeVersion("v1.0.0") diff --git a/homeassistant/components/mealie/icons.json b/homeassistant/components/mealie/icons.json index 883779a8fb0..16176391701 100644 --- a/homeassistant/components/mealie/icons.json +++ b/homeassistant/components/mealie/icons.json @@ -27,6 +27,7 @@ "get_mealplan": "mdi:food", "get_recipe": "mdi:map", "import_recipe": "mdi:map-search", - "set_random_mealplan": "mdi:dice-multiple" + "set_random_mealplan": "mdi:dice-multiple", + "set_mealplan": "mdi:food" } } diff --git a/homeassistant/components/mealie/services.py b/homeassistant/components/mealie/services.py index 3b1257ff16d..f195be37b11 100644 --- a/homeassistant/components/mealie/services.py +++ b/homeassistant/components/mealie/services.py @@ -28,6 +28,8 @@ from .const import ( ATTR_END_DATE, ATTR_ENTRY_TYPE, ATTR_INCLUDE_TAGS, + ATTR_NOTE_TEXT, + ATTR_NOTE_TITLE, ATTR_RECIPE_ID, ATTR_START_DATE, ATTR_URL, @@ -70,6 +72,31 @@ SERVICE_SET_RANDOM_MEALPLAN_SCHEMA = vol.Schema( } ) +SERVICE_SET_MEALPLAN = "set_mealplan" +SERVICE_SET_MEALPLAN_SCHEMA = vol.Any( + 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] + ), + vol.Required(ATTR_RECIPE_ID): str, + } + ), + 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] + ), + vol.Required(ATTR_NOTE_TITLE): str, + vol.Required(ATTR_NOTE_TEXT): str, + } + ), +) + def async_get_entry(hass: HomeAssistant, config_entry_id: str) -> MealieConfigEntry: """Get the Mealie config entry.""" @@ -170,6 +197,29 @@ def setup_services(hass: HomeAssistant) -> None: return {"mealplan": asdict(mealplan)} return None + async def async_set_mealplan(call: ServiceCall) -> ServiceResponse: + """Set a 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.set_mealplan( + mealplan_date, + entry_type, + recipe_id=call.data.get(ATTR_RECIPE_ID), + note_title=call.data.get(ATTR_NOTE_TITLE), + note_text=call.data.get(ATTR_NOTE_TEXT), + ) + 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, @@ -198,3 +248,10 @@ def setup_services(hass: HomeAssistant) -> None: schema=SERVICE_SET_RANDOM_MEALPLAN_SCHEMA, supports_response=SupportsResponse.OPTIONAL, ) + hass.services.async_register( + DOMAIN, + SERVICE_SET_MEALPLAN, + async_set_mealplan, + schema=SERVICE_SET_MEALPLAN_SCHEMA, + supports_response=SupportsResponse.OPTIONAL, + ) diff --git a/homeassistant/components/mealie/services.yaml b/homeassistant/components/mealie/services.yaml index c569df956e2..47a79ba5756 100644 --- a/homeassistant/components/mealie/services.yaml +++ b/homeassistant/components/mealie/services.yaml @@ -58,3 +58,32 @@ set_random_mealplan: - dinner - side translation_key: mealplan_entry_type + +set_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 + recipe_id: + selector: + text: + note_title: + selector: + text: + note_text: + selector: + text: diff --git a/homeassistant/components/mealie/strings.json b/homeassistant/components/mealie/strings.json index 3524b1a5fb3..785dd98fea6 100644 --- a/homeassistant/components/mealie/strings.json +++ b/homeassistant/components/mealie/strings.json @@ -175,6 +175,36 @@ "description": "The type of dish to randomize." } } + }, + "set_mealplan": { + "name": "Set a mealplan", + "description": "Set a 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": "[%key:component::mealie::services::set_random_mealplan::fields::date::name%]", + "description": "[%key:component::mealie::services::set_random_mealplan::fields::date::description%]" + }, + "entry_type": { + "name": "[%key:component::mealie::services::set_random_mealplan::fields::entry_type::name%]", + "description": "The type of dish to set the recipe to." + }, + "recipe_id": { + "name": "[%key:component::mealie::services::get_recipe::fields::recipe_id::name%]", + "description": "[%key:component::mealie::services::get_recipe::fields::recipe_id::description%]" + }, + "note_title": { + "name": "Meal note title", + "description": "Meal note title for when planning without recipe." + }, + "note_text": { + "name": "Note text", + "description": "Meal note text for when planning without recipe." + } + } } }, "selector": { diff --git a/tests/components/mealie/conftest.py b/tests/components/mealie/conftest.py index 208dd47ddf2..ba42d16e56e 100644 --- a/tests/components/mealie/conftest.py +++ b/tests/components/mealie/conftest.py @@ -74,9 +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) - ) + mealplan = Mealplan.from_json(load_fixture("mealplan.json", DOMAIN)) + client.random_mealplan.return_value = mealplan + client.set_mealplan.return_value = mealplan yield client diff --git a/tests/components/mealie/snapshots/test_services.ambr b/tests/components/mealie/snapshots/test_services.ambr index 293a1d8ee1d..3ae158f1d2d 100644 --- a/tests/components/mealie/snapshots/test_services.ambr +++ b/tests/components/mealie/snapshots/test_services.ambr @@ -675,6 +675,54 @@ }), }) # --- +# name: test_service_set_mealplan[payload0-kwargs0] + 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', + }), + }) +# --- +# name: test_service_set_mealplan[payload1-kwargs1] + 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', + }), + }) +# --- # name: test_service_set_random_mealplan dict({ 'mealplan': dict({ diff --git a/tests/components/mealie/test_services.py b/tests/components/mealie/test_services.py index 06ed714ea01..1c8c6f19de7 100644 --- a/tests/components/mealie/test_services.py +++ b/tests/components/mealie/test_services.py @@ -18,6 +18,8 @@ from homeassistant.components.mealie.const import ( ATTR_END_DATE, ATTR_ENTRY_TYPE, ATTR_INCLUDE_TAGS, + ATTR_NOTE_TEXT, + ATTR_NOTE_TITLE, ATTR_RECIPE_ID, ATTR_START_DATE, ATTR_URL, @@ -27,6 +29,7 @@ from homeassistant.components.mealie.services import ( SERVICE_GET_MEALPLAN, SERVICE_GET_RECIPE, SERVICE_IMPORT_RECIPE, + SERVICE_SET_MEALPLAN, SERVICE_SET_RANDOM_MEALPLAN, ) from homeassistant.const import ATTR_DATE @@ -231,6 +234,71 @@ async def test_service_set_random_mealplan( ) +@pytest.mark.parametrize( + ("payload", "kwargs"), + [ + ( + { + ATTR_RECIPE_ID: "recipe_id", + }, + {"recipe_id": "recipe_id", "note_title": None, "note_text": None}, + ), + ( + { + ATTR_NOTE_TITLE: "Note Title", + ATTR_NOTE_TEXT: "Note Text", + }, + {"recipe_id": None, "note_title": "Note Title", "note_text": "Note Text"}, + ), + ], +) +async def test_service_set_mealplan( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_config_entry: MockConfigEntry, + snapshot: SnapshotAssertion, + payload: dict[str, str], + kwargs: dict[str, str], +) -> None: + """Test the set_mealplan service.""" + + await setup_integration(hass, mock_config_entry) + + response = await hass.services.async_call( + DOMAIN, + SERVICE_SET_MEALPLAN, + { + ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, + ATTR_DATE: "2023-10-21", + ATTR_ENTRY_TYPE: "lunch", + } + | payload, + blocking=True, + return_response=True, + ) + assert response == snapshot + mock_mealie_client.set_mealplan.assert_called_with( + date(2023, 10, 21), MealplanEntryType.LUNCH, **kwargs + ) + + mock_mealie_client.random_mealplan.reset_mock() + await hass.services.async_call( + DOMAIN, + SERVICE_SET_MEALPLAN, + { + ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, + ATTR_DATE: "2023-10-21", + ATTR_ENTRY_TYPE: "lunch", + } + | payload, + blocking=True, + return_response=False, + ) + mock_mealie_client.set_mealplan.assert_called_with( + date(2023, 10, 21), MealplanEntryType.LUNCH, **kwargs + ) + + @pytest.mark.parametrize( ("service", "payload", "function", "exception", "raised_exception", "message"), [ @@ -282,6 +350,18 @@ async def test_service_set_random_mealplan( HomeAssistantError, "Error connecting to Mealie instance", ), + ( + SERVICE_SET_MEALPLAN, + { + ATTR_DATE: "2023-10-21", + ATTR_ENTRY_TYPE: "lunch", + ATTR_RECIPE_ID: "recipe_id", + }, + "set_mealplan", + MealieConnectionError, + HomeAssistantError, + "Error connecting to Mealie instance", + ), ], ) async def test_services_connection_error( @@ -321,6 +401,14 @@ async def test_services_connection_error( SERVICE_SET_RANDOM_MEALPLAN, {ATTR_DATE: "2023-10-21", ATTR_ENTRY_TYPE: "lunch"}, ), + ( + SERVICE_SET_MEALPLAN, + { + ATTR_DATE: "2023-10-21", + ATTR_ENTRY_TYPE: "lunch", + ATTR_RECIPE_ID: "recipe_id", + }, + ), ], ) async def test_service_entry_availability(