Update cookidoo API requirement to version 0.17.2 (#171793)

This commit is contained in:
Cyrill Raccaud
2026-05-24 19:57:44 +02:00
committed by GitHub
parent cfbd0f3217
commit 1ebccd9fa2
14 changed files with 196 additions and 67 deletions
+53 -28
View File
@@ -42,6 +42,35 @@ async def async_unload_entry(hass: HomeAssistant, entry: CookidooConfigEntry) ->
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
def _migrate_identifiers(
hass: HomeAssistant,
config_entry: CookidooConfigEntry,
old_prefix: str,
new_unique_id: str,
) -> None:
"""Migrate device identifiers and entity unique_ids from old to new prefix."""
device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass)
device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry_id=config_entry.entry_id
)
entity_entries = er.async_entries_for_config_entry(
entity_registry, config_entry_id=config_entry.entry_id
)
for dev in device_entries:
new_identifiers = {
(DOMAIN, new_unique_id) if domain == DOMAIN else (domain, identifier)
for domain, identifier in dev.identifiers
}
device_registry.async_update_device(dev.id, new_identifiers=new_identifiers)
for ent in entity_entries:
if ent.unique_id and ent.unique_id.startswith(f"{old_prefix}_"):
entity_registry.async_update_entity(
ent.entity_id,
new_unique_id=f"{new_unique_id}{ent.unique_id[len(old_prefix) :]}",
)
async def async_migrate_entry(
hass: HomeAssistant, config_entry: CookidooConfigEntry
) -> bool:
@@ -49,41 +78,37 @@ async def async_migrate_entry(
_LOGGER.debug("Migrating from version %s", config_entry.version)
if config_entry.version == 1 and config_entry.minor_version == 1:
# Add the unique uuid
# Add the unique uuid (first migration, entities used config_entry_id as prefix)
cookidoo = await cookidoo_from_config_entry(hass, config_entry)
try:
auth_data = await cookidoo.login()
await cookidoo.login()
user_info = await cookidoo.get_user_info()
except (CookidooRequestException, CookidooAuthException) as e:
_LOGGER.error(
"Could not migrate config config_entry: %s",
str(e),
)
_LOGGER.error("Could not migrate config entry: %s", e)
return False
unique_id = auth_data.sub
device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass)
device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry_id=config_entry.entry_id
)
entity_entries = er.async_entries_for_config_entry(
entity_registry, config_entry_id=config_entry.entry_id
)
for dev in device_entries:
device_registry.async_update_device(
dev.id, new_identifiers={(DOMAIN, unique_id)}
)
for ent in entity_entries:
assert ent.config_entry_id
entity_registry.async_update_entity(
ent.entity_id,
new_unique_id=ent.unique_id.replace(ent.config_entry_id, unique_id),
)
_migrate_identifiers(hass, config_entry, config_entry.entry_id, user_info.id)
hass.config_entries.async_update_entry(
config_entry, unique_id=auth_data.sub, minor_version=2
config_entry, unique_id=user_info.id, minor_version=3
)
if config_entry.version == 1 and config_entry.minor_version == 2:
# Migrate unique_id from old CIAM sub to community profile id
cookidoo = await cookidoo_from_config_entry(hass, config_entry)
try:
await cookidoo.login()
user_info = await cookidoo.get_user_info()
except (CookidooRequestException, CookidooAuthException) as e:
_LOGGER.error("Could not migrate config entry: %s", e)
return False
old_unique_id = config_entry.unique_id
if old_unique_id:
_migrate_identifiers(hass, config_entry, old_unique_id, user_info.id)
hass.config_entries.async_update_entry(
config_entry, unique_id=user_info.id, minor_version=3
)
_LOGGER.debug(
+12 -2
View File
@@ -3,7 +3,11 @@
from datetime import date, datetime, timedelta
import logging
from cookidoo_api import CookidooAuthException, CookidooException
from cookidoo_api import (
CookidooAuthException,
CookidooException,
CookidooRequestException,
)
from cookidoo_api.types import CookidooCalendarDayRecipe
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
@@ -74,7 +78,13 @@ class CookidooCalendarEntity(CookidooBaseEntity, CalendarEntity):
week_day
)
except CookidooAuthException:
await self.coordinator.cookidoo.refresh_token()
try:
await self.coordinator.cookidoo.login()
except (CookidooAuthException, CookidooRequestException) as exc:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="calendar_fetch_failed",
) from exc
return await self.coordinator.cookidoo.get_recipes_in_calendar_week(
week_day
)
@@ -54,7 +54,7 @@ class CookidooConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Cookidoo."""
VERSION = 1
MINOR_VERSION = 2
MINOR_VERSION = 3
COUNTRY_DATA_SCHEMA: dict
LANGUAGE_DATA_SCHEMA: dict
@@ -223,8 +223,9 @@ class CookidooConfigFlow(ConfigFlow, domain=DOMAIN):
cookidoo = await cookidoo_from_config_data(self.hass, data_input)
try:
auth_data = await cookidoo.login()
self.user_uuid = auth_data.sub
await cookidoo.login()
user_info = await cookidoo.get_user_info()
self.user_uuid = user_info.id
if language_input:
await cookidoo.get_additional_items()
except CookidooRequestException:
@@ -87,7 +87,7 @@ class CookidooDataUpdateCoordinator(DataUpdateCoordinator[CookidooData]):
)
except CookidooAuthException:
try:
await self.cookidoo.refresh_token()
await self.cookidoo.login()
except CookidooAuthException as exc:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
@@ -96,6 +96,11 @@ class CookidooDataUpdateCoordinator(DataUpdateCoordinator[CookidooData]):
CONF_EMAIL: self.config_entry.data[CONF_EMAIL]
},
) from exc
except CookidooRequestException as exc:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="setup_request_exception",
) from exc
_LOGGER.debug(
"Authentication failed but re-authentication"
" was successful, trying again later"
+3 -2
View File
@@ -2,11 +2,12 @@
from typing import Any
from aiohttp import CookieJar
from cookidoo_api import Cookidoo, CookidooConfig, get_localization_options
from homeassistant.const import CONF_COUNTRY, CONF_EMAIL, CONF_LANGUAGE, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .coordinator import CookidooConfigEntry
@@ -21,7 +22,7 @@ async def cookidoo_from_config_data(
)
return Cookidoo(
async_get_clientsession(hass),
async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)),
CookidooConfig(
email=data[CONF_EMAIL],
password=data[CONF_PASSWORD],
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["cookidoo_api"],
"quality_scale": "silver",
"requirements": ["cookidoo-api==0.14.0"]
"requirements": ["cookidoo-api==0.17.2"]
}
+1 -1
View File
@@ -769,7 +769,7 @@ connect-box==0.3.1
construct==2.10.68
# homeassistant.components.cookidoo
cookidoo-api==0.14.0
cookidoo-api==0.17.2
# homeassistant.components.backup
# homeassistant.components.utility_meter
+5 -7
View File
@@ -1,12 +1,10 @@
"""Common fixtures for the Cookidoo tests."""
from collections.abc import Generator
from typing import cast
from unittest.mock import AsyncMock, patch
from cookidoo_api import (
CookidooAdditionalItem,
CookidooAuthResponse,
CookidooIngredientItem,
CookidooSubscription,
CookidooUserInfo,
@@ -44,7 +42,7 @@ def mock_cookidoo_client() -> Generator[AsyncMock]:
autospec=True,
) as mock_client:
client = mock_client.return_value
client.login.return_value = cast(CookidooAuthResponse, {"name": "Cookidoo"})
client.login.return_value = None
client.get_ingredient_items.return_value = [
CookidooIngredientItem(**item)
for item in load_json_object_fixture("ingredient_items.json", DOMAIN)[
@@ -63,9 +61,6 @@ def mock_cookidoo_client() -> Generator[AsyncMock]:
client.get_user_info.return_value = CookidooUserInfo(
**load_json_object_fixture("user_info.json", DOMAIN)["data"]
)
client.login.return_value = CookidooAuthResponse(
**load_json_object_fixture("login.json", DOMAIN)
)
client.get_recipes_in_calendar_week.return_value = [
CookidooCalendarDay(
id=day["id"],
@@ -75,6 +70,9 @@ def mock_cookidoo_client() -> Generator[AsyncMock]:
id=recipe["id"],
name=recipe["name"],
total_time=recipe["total_time"],
thumbnail=recipe["thumbnail"],
image=recipe["image"],
url=recipe["url"],
)
for recipe in day["recipes"]
],
@@ -90,7 +88,7 @@ def mock_cookidoo_config_entry() -> MockConfigEntry:
return MockConfigEntry(
domain=DOMAIN,
version=1,
minor_version=2,
minor_version=3,
data={
CONF_EMAIL: EMAIL,
CONF_PASSWORD: PASSWORD,
@@ -7,7 +7,10 @@
{
"id": "r1",
"name": "Waffles",
"total_time": 1500
"total_time": 1500,
"thumbnail": "https://assets.tmecosys.com/image/upload/t_web_thumbnail/r1.jpg",
"image": "https://assets.tmecosys.com/image/upload/t_web_large/r1.jpg",
"url": "https://cookidoo.ch/recipes/recipe/de-CH/r1"
}
]
},
@@ -18,7 +21,10 @@
{
"id": "r2",
"name": "Mint Tea",
"total_time": 1500
"total_time": 1500,
"thumbnail": "https://assets.tmecosys.com/image/upload/t_web_thumbnail/r2.jpg",
"image": "https://assets.tmecosys.com/image/upload/t_web_large/r2.jpg",
"url": "https://cookidoo.ch/recipes/recipe/de-CH/r2"
}
]
}
@@ -1,7 +0,0 @@
{
"access_token": "eyJhbGci<redacted>",
"expires_in": 43199,
"refresh_token": "eyJhbGciOiJSUzI1NiI<redacted>",
"token_type": "bearer",
"sub": "sub_uuid"
}
@@ -1,5 +1,6 @@
{
"data": {
"id": "sub_uuid",
"username": "username_1234",
"description": null,
"picture": null
@@ -29,23 +29,33 @@
}),
'week_plan': list([
dict({
'customer_recipe_ids': list([
]),
'id': '2025-03-04',
'recipes': list([
dict({
'id': 'r1',
'image': 'https://assets.tmecosys.com/image/upload/t_web_large/r1.jpg',
'name': 'Waffles',
'thumbnail': 'https://assets.tmecosys.com/image/upload/t_web_thumbnail/r1.jpg',
'total_time': 1500,
'url': 'https://cookidoo.ch/recipes/recipe/de-CH/r1',
}),
]),
'title': '2025-03-04',
}),
dict({
'customer_recipe_ids': list([
]),
'id': '2025-03-05',
'recipes': list([
dict({
'id': 'r2',
'image': 'https://assets.tmecosys.com/image/upload/t_web_large/r2.jpg',
'name': 'Mint Tea',
'thumbnail': 'https://assets.tmecosys.com/image/upload/t_web_thumbnail/r2.jpg',
'total_time': 1500,
'url': 'https://cookidoo.ch/recipes/recipe/de-CH/r2',
}),
]),
'title': '2025-03-05',
@@ -60,6 +70,7 @@
}),
'user': dict({
'description': None,
'id': 'sub_uuid',
'picture': None,
'username': 'username_1234',
}),
@@ -4,12 +4,14 @@ from collections.abc import Generator
from datetime import UTC, datetime
from unittest.mock import AsyncMock, patch
from cookidoo_api import CookidooAuthException, CookidooRequestException
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.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import setup_integration
@@ -81,3 +83,51 @@ async def test_get_events(
)
assert resp == snapshot
@pytest.mark.parametrize(
"login_exception",
[
CookidooAuthException(),
CookidooRequestException(),
],
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_get_events_login_failure(
hass: HomeAssistant,
cookidoo_config_entry: MockConfigEntry,
mock_cookidoo_client: AsyncMock,
entity_registry: er.EntityRegistry,
login_exception: Exception,
) -> None:
"""Test calendar handles login failures gracefully during event fetch."""
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
# First call to get_recipes_in_calendar_week raises auth, login also fails
mock_cookidoo_client.get_recipes_in_calendar_week.side_effect = (
CookidooAuthException()
)
mock_cookidoo_client.login.side_effect = login_exception
with pytest.raises(HomeAssistantError):
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,
)
+41 -13
View File
@@ -85,24 +85,36 @@ async def test_config_entry_not_ready(
@pytest.mark.parametrize(
("exception", "status"),
("login_exception", "status"),
[
(None, ConfigEntryState.LOADED),
(CookidooRequestException, ConfigEntryState.SETUP_RETRY),
(CookidooAuthException, ConfigEntryState.SETUP_ERROR),
(CookidooRequestException(), ConfigEntryState.SETUP_RETRY),
(CookidooAuthException(), ConfigEntryState.SETUP_ERROR),
],
)
async def test_config_entry_not_ready_auth_error(
hass: HomeAssistant,
cookidoo_config_entry: MockConfigEntry,
mock_cookidoo_client: AsyncMock,
exception: Exception | None,
login_exception: Exception | None,
status: ConfigEntryState,
) -> None:
"""Test config entry not ready from authentication error."""
"""Test config entry recovery when data fetch hits an auth error.
mock_cookidoo_client.get_ingredient_items.side_effect = CookidooAuthException
mock_cookidoo_client.refresh_token.side_effect = exception
Simulates: initial login succeeds (_async_setup), first data fetch
raises CookidooAuthException (expired session), then re-login either
succeeds or fails. On success, the next data fetch returns valid data.
"""
# get_ingredient_items raises auth error once, then returns valid data
default_return = mock_cookidoo_client.get_ingredient_items.return_value
mock_cookidoo_client.get_ingredient_items.side_effect = [
CookidooAuthException(),
default_return,
]
# First login() is _async_setup (succeeds), second is re-login attempt
mock_cookidoo_client.login.side_effect = (
[None, login_exception] if login_exception else None
)
cookidoo_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(cookidoo_config_entry.entry_id)
@@ -135,7 +147,8 @@ OLD_ENTRY_ID = "OLD_OLD_ENTRY_ID"
MOCK_CONFIG_ENTRY_MIGRATION,
None,
),
(1, 2, MOCK_CONFIG_ENTRY_MIGRATION, TEST_UUID),
(1, 2, MOCK_CONFIG_ENTRY_MIGRATION, "old_ciam_sub_uuid"),
(1, 3, MOCK_CONFIG_ENTRY_MIGRATION, TEST_UUID),
],
)
async def test_migration_from(
@@ -160,31 +173,32 @@ async def test_migration_from(
entry_id=OLD_ENTRY_ID,
)
config_entry.add_to_hass(hass)
entity_prefix = unique_id or OLD_ENTRY_ID
device = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, OLD_ENTRY_ID)},
identifiers={(DOMAIN, entity_prefix)},
entry_type=dr.DeviceEntryType.SERVICE,
)
entity_registry.async_get_or_create(
config_entry=config_entry,
platform=DOMAIN,
domain="todo",
unique_id=f"{OLD_ENTRY_ID}_ingredients",
unique_id=f"{entity_prefix}_ingredients",
device_id=device.id,
)
entity_registry.async_get_or_create(
config_entry=config_entry,
platform=DOMAIN,
domain="todo",
unique_id=f"{OLD_ENTRY_ID}_additional_items",
unique_id=f"{entity_prefix}_additional_items",
device_id=device.id,
)
entity_registry.async_get_or_create(
config_entry=config_entry,
platform=DOMAIN,
domain="button",
unique_id=f"{OLD_ENTRY_ID}_todo_clear",
unique_id=f"{entity_prefix}_todo_clear",
device_id=device.id,
)
@@ -194,7 +208,7 @@ async def test_migration_from(
# Check change in config entry and verify most recent version
assert config_entry.version == 1
assert config_entry.minor_version == 2
assert config_entry.minor_version == 3
assert config_entry.unique_id == TEST_UUID
assert entity_registry.async_is_registered(
@@ -249,6 +263,20 @@ async def test_migration_from(
None,
CookidooAuthException,
),
(
1,
2,
MOCK_CONFIG_ENTRY_MIGRATION,
"old_ciam_sub_uuid",
CookidooRequestException,
),
(
1,
2,
MOCK_CONFIG_ENTRY_MIGRATION,
"old_ciam_sub_uuid",
CookidooAuthException,
),
],
)
async def test_migration_from_with_error(