Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
92 changes: 92 additions & 0 deletions homeassistant/components/cookidoo/calendar.py
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"

Copy link
Copy Markdown
Member

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed _meal_plan suffix


@property
def event(self) -> CalendarEvent | None:
"""Return the next upcoming event."""
return None
Comment on lines +57 to +69

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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 []
Comment thread
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
5 changes: 5 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 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_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',
}),
]),
}),
})
# ---
83 changes: 83 additions & 0 deletions tests/components/cookidoo/test_calendar.py
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
Loading