-
-
Notifications
You must be signed in to change notification settings - Fork 38.2k
Add Cookidoo planned meals calendar #159456
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
2ee9556
33bd4d9
7336962
057317e
dcdd4ff
a6dfe1f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| """Calendar platform for the Cookidoo integration.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from datetime import date, datetime, timedelta | ||
| import logging | ||
|
|
||
| from cookidoo_api import ( | ||
| CookidooAuthException, | ||
| CookidooException, | ||
| CookidooRequestException, | ||
| ) | ||
|
|
||
| from homeassistant.components.calendar import CalendarEntity, CalendarEvent | ||
| from homeassistant.core import HomeAssistant | ||
| from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback | ||
|
|
||
| from .coordinator import CookidooConfigEntry, CookidooDataUpdateCoordinator | ||
| from .entity import CookidooBaseEntity | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| PARALLEL_UPDATES = 0 | ||
|
|
||
|
|
||
| async def async_setup_entry( | ||
| hass: HomeAssistant, | ||
| config_entry: CookidooConfigEntry, | ||
| async_add_entities: AddConfigEntryEntitiesCallback, | ||
| ) -> None: | ||
| """Set up the calendar platform for entity.""" | ||
| coordinator = config_entry.runtime_data | ||
|
|
||
| async_add_entities([CookidooCalendarEntity(coordinator)]) | ||
|
|
||
|
|
||
| class CookidooCalendarEntity(CookidooBaseEntity, CalendarEntity): | ||
| """A calendar entity.""" | ||
|
|
||
| _attr_translation_key = "meal_plan" | ||
|
|
||
| def __init__(self, coordinator: CookidooDataUpdateCoordinator) -> None: | ||
| """Initialize the entity.""" | ||
| super().__init__(coordinator) | ||
| assert coordinator.config_entry.unique_id | ||
| self._attr_unique_id = f"{coordinator.config_entry.unique_id}_meal_plan" | ||
|
|
||
| @property | ||
| def event(self) -> CalendarEvent | None: | ||
| """Return the next upcoming event.""" | ||
| return None | ||
|
Comment on lines
+57
to
+69
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why don't we return the next one? In theory we can add this to the coordinator to get all the meals for this week, and return the next one of this week
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Returned next event |
||
|
|
||
| async def _fetch_week_plan(self, week_day: date) -> list: | ||
| """Fetch a single Cookidoo week plan, retrying once on auth failure.""" | ||
| try: | ||
| return await self.coordinator.cookidoo.get_recipes_in_calendar_week( | ||
| week_day | ||
| ) | ||
| except CookidooAuthException: | ||
| await self.coordinator.cookidoo.refresh_token() | ||
| return await self.coordinator.cookidoo.get_recipes_in_calendar_week( | ||
| week_day | ||
| ) | ||
| except CookidooRequestException as e: | ||
| _LOGGER.error("Failed to fetch Cookidoo week plan: %s", e) | ||
| return [] | ||
| except CookidooException as e: | ||
| _LOGGER.error("Unknown Cookidoo error: %s", e) | ||
| return [] | ||
|
surfingbytes marked this conversation as resolved.
Outdated
|
||
|
|
||
| async def async_get_events( | ||
| self, hass: HomeAssistant, start_date: datetime, end_date: datetime | ||
| ) -> list[CalendarEvent]: | ||
| """Get all events in a specific time frame.""" | ||
| events: list[CalendarEvent] = [] | ||
| current_day = start_date.date() | ||
| while current_day <= end_date.date(): | ||
| week_plan = await self._fetch_week_plan(current_day) | ||
| for day_data in week_plan: | ||
| day_date = date.fromisoformat(day_data.id) | ||
| if start_date.date() <= day_date <= end_date.date(): | ||
| events.extend( | ||
| CalendarEvent( | ||
| start=day_date, | ||
| end=day_date + timedelta(days=1), # All-day event | ||
| summary=recipe.name, | ||
| description=f"Total Time: {recipe.total_time}", | ||
| ) | ||
| for recipe in day_data.recipes | ||
| ) | ||
| current_day += timedelta(days=7) # Move to the next week | ||
| return events | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| { | ||
| "data": [ | ||
| { | ||
| "id": "2025-03-04", | ||
| "title": "2025-03-04", | ||
| "recipes": [ | ||
| { | ||
| "id": "r1", | ||
| "name": "Waffles", | ||
| "total_time": 1500 | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "id": "2025-03-05", | ||
| "title": "2025-03-05", | ||
| "recipes": [ | ||
| { | ||
| "id": "r2", | ||
| "name": "Mint Tea", | ||
| "total_time": 1500 | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # serializer version: 1 | ||
| # name: test_calendar[calendar.cookidoo_meal_plan-entry] | ||
| EntityRegistryEntrySnapshot({ | ||
| 'aliases': set({ | ||
| }), | ||
| 'area_id': None, | ||
| 'capabilities': None, | ||
| 'config_entry_id': <ANY>, | ||
| 'config_subentry_id': <ANY>, | ||
| 'device_class': None, | ||
| 'device_id': <ANY>, | ||
| 'disabled_by': None, | ||
| 'domain': 'calendar', | ||
| 'entity_category': None, | ||
| 'entity_id': 'calendar.cookidoo_meal_plan', | ||
| 'has_entity_name': True, | ||
| 'hidden_by': None, | ||
| 'icon': None, | ||
| 'id': <ANY>, | ||
| 'labels': set({ | ||
| }), | ||
| 'name': None, | ||
| 'options': dict({ | ||
| }), | ||
| 'original_device_class': None, | ||
| 'original_icon': None, | ||
| 'original_name': 'Meal plan', | ||
| 'platform': 'cookidoo', | ||
| 'previous_unique_id': None, | ||
| 'suggested_object_id': None, | ||
| 'supported_features': 0, | ||
| 'translation_key': 'meal_plan', | ||
| 'unique_id': 'sub_uuid_meal_plan', | ||
| 'unit_of_measurement': None, | ||
| }) | ||
| # --- | ||
| # name: test_calendar[calendar.cookidoo_meal_plan-state] | ||
| StateSnapshot({ | ||
| 'attributes': ReadOnlyDict({ | ||
| 'friendly_name': 'Cookidoo Meal plan', | ||
| }), | ||
| 'context': <ANY>, | ||
| 'entity_id': 'calendar.cookidoo_meal_plan', | ||
| 'last_changed': <ANY>, | ||
| 'last_reported': <ANY>, | ||
| 'last_updated': <ANY>, | ||
| 'state': 'off', | ||
| }) | ||
| # --- | ||
| # name: test_get_events | ||
| dict({ | ||
| 'calendar.cookidoo_meal_plan': dict({ | ||
| 'events': list([ | ||
| dict({ | ||
| 'description': 'Total Time: 1500', | ||
| 'end': '2025-03-05', | ||
| 'start': '2025-03-04', | ||
| 'summary': 'Waffles', | ||
| }), | ||
| dict({ | ||
| 'description': 'Total Time: 1500', | ||
| 'end': '2025-03-06', | ||
| 'start': '2025-03-05', | ||
| 'summary': 'Mint Tea', | ||
| }), | ||
| ]), | ||
| }), | ||
| }) | ||
| # --- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| """Test for calendar platform of the Cookidoo integration.""" | ||
|
|
||
| from collections.abc import Generator | ||
| from datetime import UTC, datetime | ||
| from unittest.mock import AsyncMock, patch | ||
|
|
||
| import pytest | ||
| from syrupy.assertion import SnapshotAssertion | ||
|
|
||
| from homeassistant.config_entries import ConfigEntryState | ||
| from homeassistant.const import Platform | ||
| from homeassistant.core import HomeAssistant | ||
| from homeassistant.helpers import entity_registry as er | ||
|
|
||
| from . import setup_integration | ||
|
|
||
| from tests.common import MockConfigEntry, snapshot_platform | ||
|
|
||
|
|
||
| @pytest.fixture(autouse=True) | ||
| def calendar_only() -> Generator[None]: | ||
| """Enable only the calendar platform.""" | ||
| with patch( | ||
| "homeassistant.components.cookidoo.PLATFORMS", | ||
| [Platform.CALENDAR], | ||
| ): | ||
| yield | ||
|
|
||
|
|
||
| @pytest.mark.usefixtures("mock_cookidoo_client") | ||
| async def test_calendar( | ||
| hass: HomeAssistant, | ||
| cookidoo_config_entry: MockConfigEntry, | ||
| snapshot: SnapshotAssertion, | ||
| entity_registry: er.EntityRegistry, | ||
| ) -> None: | ||
| """Snapshot test states of calendar platform.""" | ||
|
|
||
| with patch("homeassistant.components.cookidoo.PLATFORMS", [Platform.CALENDAR]): | ||
| await setup_integration(hass, cookidoo_config_entry) | ||
|
|
||
| assert cookidoo_config_entry.state is ConfigEntryState.LOADED | ||
|
|
||
| await snapshot_platform( | ||
| hass, entity_registry, snapshot, cookidoo_config_entry.entry_id | ||
| ) | ||
|
|
||
|
|
||
| @pytest.mark.usefixtures("entity_registry_enabled_by_default") | ||
| @pytest.mark.usefixtures("mock_cookidoo_client") | ||
| async def test_get_events( | ||
| hass: HomeAssistant, | ||
| cookidoo_config_entry: MockConfigEntry, | ||
| mock_cookidoo_client: AsyncMock, | ||
| entity_registry: er.EntityRegistry, | ||
| snapshot: SnapshotAssertion, | ||
| ) -> None: | ||
| """Test fetching events from Cookidoo calendar.""" | ||
|
|
||
| with patch("homeassistant.components.cookidoo.PLATFORMS", [Platform.CALENDAR]): | ||
| await setup_integration(hass, cookidoo_config_entry) | ||
|
|
||
| assert cookidoo_config_entry.state is ConfigEntryState.LOADED | ||
|
|
||
| entities = er.async_entries_for_config_entry( | ||
| entity_registry, cookidoo_config_entry.entry_id | ||
| ) | ||
| assert len(entities) == 1 | ||
| entity_id = entities[0].entity_id | ||
|
|
||
| resp = await hass.services.async_call( | ||
| "calendar", | ||
| "get_events", | ||
| { | ||
| "start_date_time": datetime(2025, 3, 4, tzinfo=UTC), | ||
| "end_date_time": datetime(2025, 3, 6, tzinfo=UTC), | ||
| }, | ||
| target={"entity_id": entity_id}, | ||
| blocking=True, | ||
| return_response=True, | ||
| ) | ||
|
|
||
| assert resp == snapshot |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we expect more calendar entities, we can keep the meal_plan suffix, otherwise, unique ids are unique per platform per integration
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed _meal_plan suffix