Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion homeassistant/components/cookidoo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@
from .coordinator import CookidooConfigEntry, CookidooDataUpdateCoordinator
from .helpers import cookidoo_from_config_entry

PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.SENSOR, Platform.TODO]
PLATFORMS: list[Platform] = [
Platform.BUTTON,
Platform.CALENDAR,
Platform.SENSOR,
Platform.TODO,
]

_LOGGER = logging.getLogger(__name__)

Expand Down
103 changes: 103 additions & 0 deletions homeassistant/components/cookidoo/calendar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""Calendar platform for the Cookidoo integration."""

from __future__ import annotations

from datetime import date, datetime, timedelta
import logging

from cookidoo_api import CookidooAuthException, CookidooException
from cookidoo_api.types import CookidooCalendarDayRecipe

from homeassistant.components.calendar import CalendarEntity, CalendarEvent
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .const import DOMAIN
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)])


def recipe_to_event(day_date: date, recipe: CookidooCalendarDayRecipe) -> CalendarEvent:
"""Convert a Cookidoo recipe to a CalendarEvent."""
return CalendarEvent(
start=day_date,
end=day_date + timedelta(days=1), # All-day event
summary=recipe.name,
description=f"Total Time: {recipe.total_time}",
)


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 = coordinator.config_entry.unique_id

@property
def event(self) -> CalendarEvent | None:
"""Return the next upcoming event."""
if not self.coordinator.data.week_plan:
return None

today = date.today()
for day_data in self.coordinator.data.week_plan:
day_date = date.fromisoformat(day_data.id)
if day_date >= today and day_data.recipes:
recipe = day_data.recipes[0]
return recipe_to_event(day_date, recipe)
return None

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 CookidooException as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="calendar_fetch_failed",
) from e

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(
recipe_to_event(day_date, recipe) for recipe in day_data.recipes
)
current_day += timedelta(days=7) # Move to the next week
return events
6 changes: 5 additions & 1 deletion homeassistant/components/cookidoo/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

from dataclasses import dataclass
from datetime import timedelta
from datetime import date, timedelta
import logging

from cookidoo_api import (
Expand All @@ -16,6 +16,7 @@
CookidooSubscription,
CookidooUserInfo,
)
from cookidoo_api.types import CookidooCalendarDay

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL
Expand All @@ -37,6 +38,7 @@ class CookidooData:
ingredient_items: list[CookidooIngredientItem]
additional_items: list[CookidooAdditionalItem]
subscription: CookidooSubscription | None
week_plan: list[CookidooCalendarDay]


class CookidooDataUpdateCoordinator(DataUpdateCoordinator[CookidooData]):
Expand Down Expand Up @@ -81,6 +83,7 @@ async def _async_update_data(self) -> CookidooData:
ingredient_items = await self.cookidoo.get_ingredient_items()
additional_items = await self.cookidoo.get_additional_items()
subscription = await self.cookidoo.get_active_subscription()
week_plan = await self.cookidoo.get_recipes_in_calendar_week(date.today())
except CookidooAuthException:
try:
await self.cookidoo.refresh_token()
Expand All @@ -106,4 +109,5 @@ async def _async_update_data(self) -> CookidooData:
ingredient_items=ingredient_items,
additional_items=additional_items,
subscription=subscription,
week_plan=week_plan,
)
8 changes: 8 additions & 0 deletions homeassistant/components/cookidoo/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@
"name": "Clear shopping list and additional purchases"
}
},
"calendar": {
"meal_plan": {
"name": "Meal plan"
}
},
"sensor": {
"expires": {
"name": "Subscription expiration date"
Expand All @@ -80,6 +85,9 @@
"button_clear_todo_failed": {
"message": "Failed to clear all items from the Cookidoo shopping list"
},
"calendar_fetch_failed": {
"message": "Failed to fetch Cookidoo meal plan"
},
"setup_authentication_exception": {
"message": "Authentication failed for {email}, check your email and password"
},
Expand Down
16 changes: 16 additions & 0 deletions tests/components/cookidoo/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
CookidooSubscription,
CookidooUserInfo,
)
from cookidoo_api.types import CookidooCalendarDay, CookidooCalendarDayRecipe
import pytest

from homeassistant.components.cookidoo.const import DOMAIN
Expand Down Expand Up @@ -65,6 +66,21 @@ def mock_cookidoo_client() -> Generator[AsyncMock]:
client.login.return_value = CookidooAuthResponse(
**load_json_object_fixture("login.json", DOMAIN)
)
client.get_recipes_in_calendar_week.return_value = [
CookidooCalendarDay(
id=day["id"],
title=day["title"],
recipes=[
CookidooCalendarDayRecipe(
id=recipe["id"],
name=recipe["name"],
total_time=recipe["total_time"],
)
for recipe in day["recipes"]
],
)
for day in load_json_object_fixture("calendar_week.json", DOMAIN)["data"]
]
yield client


Expand Down
26 changes: 26 additions & 0 deletions tests/components/cookidoo/fixtures/calendar_week.json
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
}
]
}
]
}
69 changes: 69 additions & 0 deletions tests/components/cookidoo/snapshots/test_calendar.ambr
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',
'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',
}),
]),
}),
})
# ---
24 changes: 24 additions & 0 deletions tests/components/cookidoo/snapshots/test_diagnostics.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,30 @@
'subscription_source': 'COMMERCE',
'type': 'REGULAR',
}),
'week_plan': list([
dict({
'id': '2025-03-04',
'recipes': list([
dict({
'id': 'r1',
'name': 'Waffles',
'total_time': 1500,
}),
]),
'title': '2025-03-04',
}),
dict({
'id': '2025-03-05',
'recipes': list([
dict({
'id': 'r2',
'name': 'Mint Tea',
'total_time': 1500,
}),
]),
'title': '2025-03-05',
}),
]),
}),
'entry_data': dict({
'country': 'CH',
Expand Down
Loading
Loading