Add calendar platform to Habitica integration (#128248)
* Add calendar platform * Add tests * add missing reminders filter by date * Add +1 day to todo end * add 1 day to dailies, remove unused line of code * Removing reminders calendar to a separate PR * fix upcoming event for dailies * util function for rrule string * Add test for get_recurrence_rule * use habitica daystart and account for isDue flag * yesterdaily is still an active event * Fix yesterdailies and add attribute * Update snapshot * Use iter, return attribute with None value * various changes * update snapshot * fix merge error * update snapshot * change date range filtering for todos * use datetimes instead of date in async_get_events * Sort events * Update snapshot * add method for todos * filter for upcoming events * dailies * refactor todos * update dailies logic * dedent loopspull/129479/head
parent
db5cb6233c
commit
6887a4419e
|
@ -29,7 +29,13 @@ from .types import HabiticaConfigEntry
|
||||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
PLATFORMS = [Platform.BUTTON, Platform.SENSOR, Platform.SWITCH, Platform.TODO]
|
PLATFORMS = [
|
||||||
|
Platform.BUTTON,
|
||||||
|
Platform.CALENDAR,
|
||||||
|
Platform.SENSOR,
|
||||||
|
Platform.SWITCH,
|
||||||
|
Platform.TODO,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
|
|
@ -0,0 +1,227 @@
|
||||||
|
"""Calendar platform for Habitica integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
from dateutil.rrule import rrule
|
||||||
|
|
||||||
|
from homeassistant.components.calendar import (
|
||||||
|
CalendarEntity,
|
||||||
|
CalendarEntityDescription,
|
||||||
|
CalendarEvent,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
from . import HabiticaConfigEntry
|
||||||
|
from .coordinator import HabiticaDataUpdateCoordinator
|
||||||
|
from .entity import HabiticaBase
|
||||||
|
from .types import HabiticaTaskType
|
||||||
|
from .util import build_rrule, get_recurrence_rule
|
||||||
|
|
||||||
|
|
||||||
|
class HabiticaCalendar(StrEnum):
|
||||||
|
"""Habitica calendars."""
|
||||||
|
|
||||||
|
DAILIES = "dailys"
|
||||||
|
TODOS = "todos"
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: HabiticaConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the calendar platform."""
|
||||||
|
coordinator = config_entry.runtime_data
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
[
|
||||||
|
HabiticaTodosCalendarEntity(coordinator),
|
||||||
|
HabiticaDailiesCalendarEntity(coordinator),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HabiticaCalendarEntity(HabiticaBase, CalendarEntity):
|
||||||
|
"""Base Habitica calendar entity."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: HabiticaDataUpdateCoordinator,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize calendar entity."""
|
||||||
|
super().__init__(coordinator, self.entity_description)
|
||||||
|
|
||||||
|
|
||||||
|
class HabiticaTodosCalendarEntity(HabiticaCalendarEntity):
|
||||||
|
"""Habitica todos calendar entity."""
|
||||||
|
|
||||||
|
entity_description = CalendarEntityDescription(
|
||||||
|
key=HabiticaCalendar.TODOS,
|
||||||
|
translation_key=HabiticaCalendar.TODOS,
|
||||||
|
)
|
||||||
|
|
||||||
|
def dated_todos(
|
||||||
|
self, start_date: datetime, end_date: datetime | None = None
|
||||||
|
) -> list[CalendarEvent]:
|
||||||
|
"""Get all dated todos."""
|
||||||
|
|
||||||
|
events = []
|
||||||
|
for task in self.coordinator.data.tasks:
|
||||||
|
if not (
|
||||||
|
task["type"] == HabiticaTaskType.TODO
|
||||||
|
and not task["completed"]
|
||||||
|
and task.get("date") # only if has due date
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
start = dt_util.start_of_local_day(datetime.fromisoformat(task["date"]))
|
||||||
|
end = start + timedelta(days=1)
|
||||||
|
# return current and upcoming events or events within the requested range
|
||||||
|
|
||||||
|
if end < start_date:
|
||||||
|
# Event ends before date range
|
||||||
|
continue
|
||||||
|
|
||||||
|
if end_date and start > end_date:
|
||||||
|
# Event starts after date range
|
||||||
|
continue
|
||||||
|
|
||||||
|
events.append(
|
||||||
|
CalendarEvent(
|
||||||
|
start=start.date(),
|
||||||
|
end=end.date(),
|
||||||
|
summary=task["text"],
|
||||||
|
description=task["notes"],
|
||||||
|
uid=task["id"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return sorted(
|
||||||
|
events,
|
||||||
|
key=lambda event: (
|
||||||
|
event.start,
|
||||||
|
self.coordinator.data.user["tasksOrder"]["todos"].index(event.uid),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def event(self) -> CalendarEvent | None:
|
||||||
|
"""Return the current or next upcoming event."""
|
||||||
|
|
||||||
|
return next(iter(self.dated_todos(dt_util.now())), None)
|
||||||
|
|
||||||
|
async def async_get_events(
|
||||||
|
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||||
|
) -> list[CalendarEvent]:
|
||||||
|
"""Return calendar events within a datetime range."""
|
||||||
|
return self.dated_todos(start_date, end_date)
|
||||||
|
|
||||||
|
|
||||||
|
class HabiticaDailiesCalendarEntity(HabiticaCalendarEntity):
|
||||||
|
"""Habitica dailies calendar entity."""
|
||||||
|
|
||||||
|
entity_description = CalendarEntityDescription(
|
||||||
|
key=HabiticaCalendar.DAILIES,
|
||||||
|
translation_key=HabiticaCalendar.DAILIES,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def today(self) -> datetime:
|
||||||
|
"""Habitica daystart."""
|
||||||
|
return dt_util.start_of_local_day(
|
||||||
|
datetime.fromisoformat(self.coordinator.data.user["lastCron"])
|
||||||
|
)
|
||||||
|
|
||||||
|
def end_date(self, recurrence: datetime, end: datetime | None = None) -> date:
|
||||||
|
"""Calculate the end date for a yesterdaily.
|
||||||
|
|
||||||
|
The enddates of events from yesterday move forward to the end
|
||||||
|
of the current day (until the cron resets the dailies) to show them
|
||||||
|
as still active events on the calendar state entity (state: on).
|
||||||
|
|
||||||
|
Events in the calendar view will show all-day events on their due day
|
||||||
|
"""
|
||||||
|
if end:
|
||||||
|
return recurrence.date() + timedelta(days=1)
|
||||||
|
return (
|
||||||
|
dt_util.start_of_local_day() if recurrence == self.today else recurrence
|
||||||
|
).date() + timedelta(days=1)
|
||||||
|
|
||||||
|
def get_recurrence_dates(
|
||||||
|
self, recurrences: rrule, start_date: datetime, end_date: datetime | None = None
|
||||||
|
) -> list[datetime]:
|
||||||
|
"""Calculate recurrence dates based on start_date and end_date."""
|
||||||
|
if end_date:
|
||||||
|
return recurrences.between(
|
||||||
|
start_date, end_date - timedelta(days=1), inc=True
|
||||||
|
)
|
||||||
|
# if no end_date is given, return only the next recurrence
|
||||||
|
return [recurrences.after(self.today, inc=True)]
|
||||||
|
|
||||||
|
def due_dailies(
|
||||||
|
self, start_date: datetime, end_date: datetime | None = None
|
||||||
|
) -> list[CalendarEvent]:
|
||||||
|
"""Get dailies and recurrences for a given period or the next upcoming."""
|
||||||
|
|
||||||
|
# we only have dailies for today and future recurrences
|
||||||
|
if end_date and end_date < self.today:
|
||||||
|
return []
|
||||||
|
start_date = max(start_date, self.today)
|
||||||
|
|
||||||
|
events = []
|
||||||
|
for task in self.coordinator.data.tasks:
|
||||||
|
# only dailies that that are not 'grey dailies'
|
||||||
|
if not (task["type"] == HabiticaTaskType.DAILY and task["everyX"]):
|
||||||
|
continue
|
||||||
|
|
||||||
|
recurrences = build_rrule(task)
|
||||||
|
recurrence_dates = self.get_recurrence_dates(
|
||||||
|
recurrences, start_date, end_date
|
||||||
|
)
|
||||||
|
for recurrence in recurrence_dates:
|
||||||
|
is_future_event = recurrence > self.today
|
||||||
|
is_current_event = recurrence <= self.today and not task["completed"]
|
||||||
|
|
||||||
|
if not (is_future_event or is_current_event):
|
||||||
|
continue
|
||||||
|
|
||||||
|
events.append(
|
||||||
|
CalendarEvent(
|
||||||
|
start=recurrence.date(),
|
||||||
|
end=self.end_date(recurrence, end_date),
|
||||||
|
summary=task["text"],
|
||||||
|
description=task["notes"],
|
||||||
|
uid=task["id"],
|
||||||
|
rrule=get_recurrence_rule(recurrences),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return sorted(
|
||||||
|
events,
|
||||||
|
key=lambda event: (
|
||||||
|
event.start,
|
||||||
|
self.coordinator.data.user["tasksOrder"]["dailys"].index(event.uid),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def event(self) -> CalendarEvent | None:
|
||||||
|
"""Return the next upcoming event."""
|
||||||
|
return next(iter(self.due_dailies(self.today)), None)
|
||||||
|
|
||||||
|
async def async_get_events(
|
||||||
|
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||||
|
) -> list[CalendarEvent]:
|
||||||
|
"""Return calendar events within a datetime range."""
|
||||||
|
|
||||||
|
return self.due_dailies(start_date, end_date)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extra_state_attributes(self) -> dict[str, bool | None] | None:
|
||||||
|
"""Return entity specific state attributes."""
|
||||||
|
return {
|
||||||
|
"yesterdaily": self.event.start < self.today.date() if self.event else None
|
||||||
|
}
|
|
@ -58,6 +58,14 @@
|
||||||
"default": "mdi:hand-heart-outline"
|
"default": "mdi:hand-heart-outline"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"calendar": {
|
||||||
|
"todos": {
|
||||||
|
"default": "mdi:calendar-check"
|
||||||
|
},
|
||||||
|
"dailys": {
|
||||||
|
"default": "mdi:calendar-multiple"
|
||||||
|
}
|
||||||
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"display_name": {
|
"display_name": {
|
||||||
"default": "mdi:account-circle"
|
"default": "mdi:account-circle"
|
||||||
|
|
|
@ -84,6 +84,23 @@
|
||||||
"name": "Blessing"
|
"name": "Blessing"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"calendar": {
|
||||||
|
"todos": {
|
||||||
|
"name": "To-Do's"
|
||||||
|
},
|
||||||
|
"dailys": {
|
||||||
|
"name": "Dailies",
|
||||||
|
"state_attributes": {
|
||||||
|
"yesterdaily": {
|
||||||
|
"name": "Yester-Daily",
|
||||||
|
"state": {
|
||||||
|
"true": "[%key:common::state::yes%]",
|
||||||
|
"false": "[%key:common::state::no%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"display_name": {
|
"display_name": {
|
||||||
"name": "Display name"
|
"name": "Display name"
|
||||||
|
|
|
@ -24,7 +24,7 @@ from homeassistant.util import dt as dt_util
|
||||||
from .const import ASSETS_URL, DOMAIN
|
from .const import ASSETS_URL, DOMAIN
|
||||||
from .coordinator import HabiticaDataUpdateCoordinator
|
from .coordinator import HabiticaDataUpdateCoordinator
|
||||||
from .entity import HabiticaBase
|
from .entity import HabiticaBase
|
||||||
from .types import HabiticaConfigEntry
|
from .types import HabiticaConfigEntry, HabiticaTaskType
|
||||||
from .util import next_due_date
|
from .util import next_due_date
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,15 +37,6 @@ class HabiticaTodoList(StrEnum):
|
||||||
REWARDS = "rewards"
|
REWARDS = "rewards"
|
||||||
|
|
||||||
|
|
||||||
class HabiticaTaskType(StrEnum):
|
|
||||||
"""Habitica Entities."""
|
|
||||||
|
|
||||||
HABIT = "habit"
|
|
||||||
DAILY = "daily"
|
|
||||||
TODO = "todo"
|
|
||||||
REWARD = "reward"
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: HabiticaConfigEntry,
|
config_entry: HabiticaConfigEntry,
|
||||||
|
|
|
@ -1,7 +1,18 @@
|
||||||
"""Types for Habitica integration."""
|
"""Types for Habitica integration."""
|
||||||
|
|
||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
|
||||||
from .coordinator import HabiticaDataUpdateCoordinator
|
from .coordinator import HabiticaDataUpdateCoordinator
|
||||||
|
|
||||||
type HabiticaConfigEntry = ConfigEntry[HabiticaDataUpdateCoordinator]
|
type HabiticaConfigEntry = ConfigEntry[HabiticaDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
|
class HabiticaTaskType(StrEnum):
|
||||||
|
"""Habitica Entities."""
|
||||||
|
|
||||||
|
HABIT = "habit"
|
||||||
|
DAILY = "daily"
|
||||||
|
TODO = "todo"
|
||||||
|
REWARD = "reward"
|
||||||
|
|
|
@ -5,6 +5,21 @@ from __future__ import annotations
|
||||||
import datetime
|
import datetime
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
from dateutil.rrule import (
|
||||||
|
DAILY,
|
||||||
|
FR,
|
||||||
|
MO,
|
||||||
|
MONTHLY,
|
||||||
|
SA,
|
||||||
|
SU,
|
||||||
|
TH,
|
||||||
|
TU,
|
||||||
|
WE,
|
||||||
|
WEEKLY,
|
||||||
|
YEARLY,
|
||||||
|
rrule,
|
||||||
|
)
|
||||||
|
|
||||||
from homeassistant.components.automation import automations_with_entity
|
from homeassistant.components.automation import automations_with_entity
|
||||||
from homeassistant.components.script import scripts_with_entity
|
from homeassistant.components.script import scripts_with_entity
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
@ -62,3 +77,65 @@ def entity_used_in(hass: HomeAssistant, entity_id: str) -> list[str]:
|
||||||
used_in = automations_with_entity(hass, entity_id)
|
used_in = automations_with_entity(hass, entity_id)
|
||||||
used_in += scripts_with_entity(hass, entity_id)
|
used_in += scripts_with_entity(hass, entity_id)
|
||||||
return used_in
|
return used_in
|
||||||
|
|
||||||
|
|
||||||
|
FREQUENCY_MAP = {"daily": DAILY, "weekly": WEEKLY, "monthly": MONTHLY, "yearly": YEARLY}
|
||||||
|
WEEKDAY_MAP = {"m": MO, "t": TU, "w": WE, "th": TH, "f": FR, "s": SA, "su": SU}
|
||||||
|
|
||||||
|
|
||||||
|
def build_rrule(task: dict[str, Any]) -> rrule:
|
||||||
|
"""Build rrule string."""
|
||||||
|
|
||||||
|
rrule_frequency = FREQUENCY_MAP.get(task["frequency"], DAILY)
|
||||||
|
weekdays = [
|
||||||
|
WEEKDAY_MAP[day] for day, is_active in task["repeat"].items() if is_active
|
||||||
|
]
|
||||||
|
bymonthday = (
|
||||||
|
task["daysOfMonth"]
|
||||||
|
if rrule_frequency == MONTHLY and task["daysOfMonth"]
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
bysetpos = None
|
||||||
|
if rrule_frequency == MONTHLY and task["weeksOfMonth"]:
|
||||||
|
bysetpos = task["weeksOfMonth"]
|
||||||
|
weekdays = weekdays if weekdays else [MO]
|
||||||
|
|
||||||
|
return rrule(
|
||||||
|
freq=rrule_frequency,
|
||||||
|
interval=task["everyX"],
|
||||||
|
dtstart=dt_util.start_of_local_day(
|
||||||
|
datetime.datetime.fromisoformat(task["startDate"])
|
||||||
|
),
|
||||||
|
byweekday=weekdays if rrule_frequency in [WEEKLY, MONTHLY] else None,
|
||||||
|
bymonthday=bymonthday,
|
||||||
|
bysetpos=bysetpos,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_recurrence_rule(recurrence: rrule) -> str:
|
||||||
|
r"""Extract and return the recurrence rule portion of an RRULE.
|
||||||
|
|
||||||
|
This function takes an RRULE representing a task's recurrence pattern,
|
||||||
|
builds the RRULE string, and extracts the recurrence rule part.
|
||||||
|
|
||||||
|
'DTSTART:YYYYMMDDTHHMMSS\nRRULE:FREQ=YEARLY;INTERVAL=2'
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
recurrence : rrule
|
||||||
|
An RRULE object.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
The recurrence rule portion of the RRULE string, starting with 'FREQ='.
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
>>> rule = get_recurrence_rule(task)
|
||||||
|
>>> print(rule)
|
||||||
|
'FREQ=YEARLY;INTERVAL=2'
|
||||||
|
|
||||||
|
"""
|
||||||
|
return str(recurrence).split("RRULE:")[1]
|
||||||
|
|
|
@ -444,7 +444,12 @@
|
||||||
"completedBy": {},
|
"completedBy": {},
|
||||||
"assignedUsers": []
|
"assignedUsers": []
|
||||||
},
|
},
|
||||||
"reminders": [],
|
"reminders": [
|
||||||
|
{
|
||||||
|
"id": "91c09432-10ac-4a49-bd20-823081ec29ed",
|
||||||
|
"time": "2024-09-22T02:00:00.0000Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
"byHabitica": false,
|
"byHabitica": false,
|
||||||
"createdAt": "2024-09-21T22:17:19.513Z",
|
"createdAt": "2024-09-21T22:17:19.513Z",
|
||||||
"updatedAt": "2024-09-21T22:19:35.576Z",
|
"updatedAt": "2024-09-21T22:19:35.576Z",
|
||||||
|
@ -477,7 +482,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"_id": "86ea2475-d1b5-4020-bdcc-c188c7996afa",
|
"_id": "86ea2475-d1b5-4020-bdcc-c188c7996afa",
|
||||||
"date": "2024-09-26T22:15:00.000Z",
|
"date": "2024-09-21T22:00:00.000Z",
|
||||||
"completed": false,
|
"completed": false,
|
||||||
"collapseChecklist": false,
|
"collapseChecklist": false,
|
||||||
"checklist": [],
|
"checklist": [],
|
||||||
|
|
|
@ -34,6 +34,24 @@
|
||||||
"flags": {
|
"flags": {
|
||||||
"classSelected": true
|
"classSelected": true
|
||||||
},
|
},
|
||||||
|
"tasksOrder": {
|
||||||
|
"rewards": ["5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b"],
|
||||||
|
"todos": [
|
||||||
|
"88de7cd9-af2b-49ce-9afd-bf941d87336b",
|
||||||
|
"2f6fcabc-f670-4ec3-ba65-817e8deea490",
|
||||||
|
"1aa3137e-ef72-4d1f-91ee-41933602f438",
|
||||||
|
"86ea2475-d1b5-4020-bdcc-c188c7996afa"
|
||||||
|
],
|
||||||
|
"dailys": [
|
||||||
|
"f21fa608-cfc6-4413-9fc7-0eb1b48ca43a",
|
||||||
|
"bc1d1855-b2b8-4663-98ff-62e7b763dfc4",
|
||||||
|
"e97659e0-2c42-4599-a7bb-00282adc410d",
|
||||||
|
"564b9ac9-c53d-4638-9e7f-1cd96fe19baa",
|
||||||
|
"f2c85972-1a19-4426-bc6d-ce3337b9d99f",
|
||||||
|
"2c6d136c-a1c3-4bef-b7c4-fa980784b1e1"
|
||||||
|
],
|
||||||
|
"habits": ["1d147de6-5c02-4740-8e2f-71d3015a37f4"]
|
||||||
|
},
|
||||||
"needsCron": true,
|
"needsCron": true,
|
||||||
"lastCron": "2024-09-21T22:01:55.586Z"
|
"lastCron": "2024-09-21T22:01:55.586Z"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,730 @@
|
||||||
|
# serializer version: 1
|
||||||
|
# name: test_api_events[calendar.test_user_dailies]
|
||||||
|
list([
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Deinen Terminplan festzulegen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-22',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-21',
|
||||||
|
}),
|
||||||
|
'summary': '5 Minuten ruhig durchatmen',
|
||||||
|
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Ein einstündiges Workout im Fitnessstudio absolvieren.',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-22',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=WE,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-21',
|
||||||
|
}),
|
||||||
|
'summary': 'Fitnessstudio besuchen',
|
||||||
|
'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Änderungen zu machen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-23',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-22',
|
||||||
|
}),
|
||||||
|
'summary': 'Zahnseide benutzen',
|
||||||
|
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Deinen Terminplan festzulegen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-23',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-22',
|
||||||
|
}),
|
||||||
|
'summary': '5 Minuten ruhig durchatmen',
|
||||||
|
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Ein einstündiges Workout im Fitnessstudio absolvieren.',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-23',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=WE,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-22',
|
||||||
|
}),
|
||||||
|
'summary': 'Fitnessstudio besuchen',
|
||||||
|
'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Änderungen zu machen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-24',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-23',
|
||||||
|
}),
|
||||||
|
'summary': 'Zahnseide benutzen',
|
||||||
|
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Deinen Terminplan festzulegen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-24',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-23',
|
||||||
|
}),
|
||||||
|
'summary': '5 Minuten ruhig durchatmen',
|
||||||
|
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Änderungen zu machen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-25',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-24',
|
||||||
|
}),
|
||||||
|
'summary': 'Zahnseide benutzen',
|
||||||
|
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Deinen Terminplan festzulegen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-25',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-24',
|
||||||
|
}),
|
||||||
|
'summary': '5 Minuten ruhig durchatmen',
|
||||||
|
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Änderungen zu machen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-26',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-25',
|
||||||
|
}),
|
||||||
|
'summary': 'Zahnseide benutzen',
|
||||||
|
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Deinen Terminplan festzulegen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-26',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-25',
|
||||||
|
}),
|
||||||
|
'summary': '5 Minuten ruhig durchatmen',
|
||||||
|
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Ein einstündiges Workout im Fitnessstudio absolvieren.',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-26',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=WE,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-25',
|
||||||
|
}),
|
||||||
|
'summary': 'Fitnessstudio besuchen',
|
||||||
|
'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Änderungen zu machen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-27',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-26',
|
||||||
|
}),
|
||||||
|
'summary': 'Zahnseide benutzen',
|
||||||
|
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Deinen Terminplan festzulegen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-27',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-26',
|
||||||
|
}),
|
||||||
|
'summary': '5 Minuten ruhig durchatmen',
|
||||||
|
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Änderungen zu machen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-28',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-27',
|
||||||
|
}),
|
||||||
|
'summary': 'Zahnseide benutzen',
|
||||||
|
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Deinen Terminplan festzulegen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-28',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-27',
|
||||||
|
}),
|
||||||
|
'summary': '5 Minuten ruhig durchatmen',
|
||||||
|
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Änderungen zu machen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-29',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-28',
|
||||||
|
}),
|
||||||
|
'summary': 'Zahnseide benutzen',
|
||||||
|
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Deinen Terminplan festzulegen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-29',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-28',
|
||||||
|
}),
|
||||||
|
'summary': '5 Minuten ruhig durchatmen',
|
||||||
|
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Ein einstündiges Workout im Fitnessstudio absolvieren.',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-29',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=WE,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-28',
|
||||||
|
}),
|
||||||
|
'summary': 'Fitnessstudio besuchen',
|
||||||
|
'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Änderungen zu machen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-30',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-29',
|
||||||
|
}),
|
||||||
|
'summary': 'Zahnseide benutzen',
|
||||||
|
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Deinen Terminplan festzulegen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-30',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-29',
|
||||||
|
}),
|
||||||
|
'summary': '5 Minuten ruhig durchatmen',
|
||||||
|
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Ein einstündiges Workout im Fitnessstudio absolvieren.',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-30',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=WE,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-29',
|
||||||
|
}),
|
||||||
|
'summary': 'Fitnessstudio besuchen',
|
||||||
|
'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Änderungen zu machen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-10-01',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-30',
|
||||||
|
}),
|
||||||
|
'summary': 'Zahnseide benutzen',
|
||||||
|
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Deinen Terminplan festzulegen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-10-01',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-30',
|
||||||
|
}),
|
||||||
|
'summary': '5 Minuten ruhig durchatmen',
|
||||||
|
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Änderungen zu machen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-10-02',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-10-01',
|
||||||
|
}),
|
||||||
|
'summary': 'Zahnseide benutzen',
|
||||||
|
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Deinen Terminplan festzulegen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-10-02',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-10-01',
|
||||||
|
}),
|
||||||
|
'summary': '5 Minuten ruhig durchatmen',
|
||||||
|
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Änderungen zu machen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-10-03',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-10-02',
|
||||||
|
}),
|
||||||
|
'summary': 'Zahnseide benutzen',
|
||||||
|
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Deinen Terminplan festzulegen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-10-03',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-10-02',
|
||||||
|
}),
|
||||||
|
'summary': '5 Minuten ruhig durchatmen',
|
||||||
|
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Ein einstündiges Workout im Fitnessstudio absolvieren.',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-10-03',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=WE,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-10-02',
|
||||||
|
}),
|
||||||
|
'summary': 'Fitnessstudio besuchen',
|
||||||
|
'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Änderungen zu machen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-10-04',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-10-03',
|
||||||
|
}),
|
||||||
|
'summary': 'Zahnseide benutzen',
|
||||||
|
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Deinen Terminplan festzulegen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-10-04',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-10-03',
|
||||||
|
}),
|
||||||
|
'summary': '5 Minuten ruhig durchatmen',
|
||||||
|
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Änderungen zu machen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-10-05',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-10-04',
|
||||||
|
}),
|
||||||
|
'summary': 'Zahnseide benutzen',
|
||||||
|
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Deinen Terminplan festzulegen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-10-05',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-10-04',
|
||||||
|
}),
|
||||||
|
'summary': '5 Minuten ruhig durchatmen',
|
||||||
|
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Änderungen zu machen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-10-06',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-10-05',
|
||||||
|
}),
|
||||||
|
'summary': 'Zahnseide benutzen',
|
||||||
|
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Deinen Terminplan festzulegen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-10-06',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-10-05',
|
||||||
|
}),
|
||||||
|
'summary': '5 Minuten ruhig durchatmen',
|
||||||
|
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Ein einstündiges Workout im Fitnessstudio absolvieren.',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-10-06',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=WE,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-10-05',
|
||||||
|
}),
|
||||||
|
'summary': 'Fitnessstudio besuchen',
|
||||||
|
'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Änderungen zu machen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-10-07',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-10-06',
|
||||||
|
}),
|
||||||
|
'summary': 'Zahnseide benutzen',
|
||||||
|
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Deinen Terminplan festzulegen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-10-07',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-10-06',
|
||||||
|
}),
|
||||||
|
'summary': '5 Minuten ruhig durchatmen',
|
||||||
|
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Ein einstündiges Workout im Fitnessstudio absolvieren.',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-10-07',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=WE,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-10-06',
|
||||||
|
}),
|
||||||
|
'summary': 'Fitnessstudio besuchen',
|
||||||
|
'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Änderungen zu machen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-10-08',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-10-07',
|
||||||
|
}),
|
||||||
|
'summary': 'Zahnseide benutzen',
|
||||||
|
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Klicke um Deinen Terminplan festzulegen!',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-10-08',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU',
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-10-07',
|
||||||
|
}),
|
||||||
|
'summary': '5 Minuten ruhig durchatmen',
|
||||||
|
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
# ---
|
||||||
|
# name: test_api_events[calendar.test_user_to_do_s]
|
||||||
|
list([
|
||||||
|
dict({
|
||||||
|
'description': 'Strom- und Internetrechnungen rechtzeitig überweisen.',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-01',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': None,
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-08-31',
|
||||||
|
}),
|
||||||
|
'summary': 'Rechnungen bezahlen',
|
||||||
|
'uid': '2f6fcabc-f670-4ec3-ba65-817e8deea490',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Den Ausflug für das kommende Wochenende organisieren.',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-22',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': None,
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-21',
|
||||||
|
}),
|
||||||
|
'summary': 'Wochenendausflug planen',
|
||||||
|
'uid': '86ea2475-d1b5-4020-bdcc-c188c7996afa',
|
||||||
|
}),
|
||||||
|
dict({
|
||||||
|
'description': 'Das Buch, das du angefangen hast, bis zum Wochenende fertig lesen.',
|
||||||
|
'end': dict({
|
||||||
|
'date': '2024-09-28',
|
||||||
|
}),
|
||||||
|
'location': None,
|
||||||
|
'recurrence_id': None,
|
||||||
|
'rrule': None,
|
||||||
|
'start': dict({
|
||||||
|
'date': '2024-09-27',
|
||||||
|
}),
|
||||||
|
'summary': 'Buch zu Ende lesen',
|
||||||
|
'uid': '88de7cd9-af2b-49ce-9afd-bf941d87336b',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
# ---
|
||||||
|
# name: test_calendar_platform[calendar.test_user_dailies-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': None,
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'calendar',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'calendar.test_user_dailies',
|
||||||
|
'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': 'Dailies',
|
||||||
|
'platform': 'habitica',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': <HabiticaCalendar.DAILIES: 'dailys'>,
|
||||||
|
'unique_id': '00000000-0000-0000-0000-000000000000_dailys',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_calendar_platform[calendar.test_user_dailies-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'all_day': True,
|
||||||
|
'description': 'Klicke um Deinen Terminplan festzulegen!',
|
||||||
|
'end_time': '2024-09-22 00:00:00',
|
||||||
|
'friendly_name': 'test-user Dailies',
|
||||||
|
'location': '',
|
||||||
|
'message': '5 Minuten ruhig durchatmen',
|
||||||
|
'start_time': '2024-09-21 00:00:00',
|
||||||
|
'yesterdaily': False,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'calendar.test_user_dailies',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'on',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_calendar_platform[calendar.test_user_to_do_s-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': None,
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'calendar',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'calendar.test_user_to_do_s',
|
||||||
|
'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': "To-Do's",
|
||||||
|
'platform': 'habitica',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': <HabiticaCalendar.TODOS: 'todos'>,
|
||||||
|
'unique_id': '00000000-0000-0000-0000-000000000000_todos',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_calendar_platform[calendar.test_user_to_do_s-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'all_day': True,
|
||||||
|
'description': 'Den Ausflug für das kommende Wochenende organisieren.',
|
||||||
|
'end_time': '2024-09-22 00:00:00',
|
||||||
|
'friendly_name': "test-user To-Do's",
|
||||||
|
'location': '',
|
||||||
|
'message': 'Wochenendausflug planen',
|
||||||
|
'start_time': '2024-09-21 00:00:00',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'calendar.test_user_to_do_s',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'on',
|
||||||
|
})
|
||||||
|
# ---
|
|
@ -72,7 +72,7 @@
|
||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'description': 'Den Ausflug für das kommende Wochenende organisieren.',
|
'description': 'Den Ausflug für das kommende Wochenende organisieren.',
|
||||||
'due': '2024-09-26',
|
'due': '2024-09-21',
|
||||||
'status': 'needs_action',
|
'status': 'needs_action',
|
||||||
'summary': 'Wochenendausflug planen',
|
'summary': 'Wochenendausflug planen',
|
||||||
'uid': '86ea2475-d1b5-4020-bdcc-c188c7996afa',
|
'uid': '86ea2475-d1b5-4020-bdcc-c188c7996afa',
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
"""Tests for the Habitica calendar platform."""
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
from unittest.mock import 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 tests.common import MockConfigEntry, snapshot_platform
|
||||||
|
from tests.typing import ClientSessionGenerator
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def calendar_only() -> Generator[None]:
|
||||||
|
"""Enable only the calendar platform."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.habitica.PLATFORMS",
|
||||||
|
[Platform.CALENDAR],
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
async def set_tz(hass: HomeAssistant) -> None:
|
||||||
|
"""Fixture to set timezone."""
|
||||||
|
await hass.config.async_set_time_zone("Europe/Berlin")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_habitica")
|
||||||
|
@pytest.mark.freeze_time("2024-09-20T22:00:00.000Z")
|
||||||
|
async def test_calendar_platform(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test setup of the Habitica calendar platform."""
|
||||||
|
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert config_entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("entity"),
|
||||||
|
[
|
||||||
|
"calendar.test_user_to_do_s",
|
||||||
|
"calendar.test_user_dailies",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.freeze_time("2024-09-20T22:00:00.000Z")
|
||||||
|
@pytest.mark.usefixtures("mock_habitica")
|
||||||
|
async def test_api_events(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
hass_client: ClientSessionGenerator,
|
||||||
|
entity: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test calendar event."""
|
||||||
|
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
response = await client.get(
|
||||||
|
f"/api/calendars/{entity}?start=2024-08-29&end=2024-10-08"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert await response.json() == snapshot
|
Loading…
Reference in New Issue