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 loops
pull/129479/head
Manu 2024-10-30 04:53:49 +01:00 committed by GitHub
parent db5cb6233c
commit 6887a4419e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1184 additions and 14 deletions

View File

@ -29,7 +29,13 @@ from .types import HabiticaConfigEntry
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:

View File

@ -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
}

View File

@ -58,6 +58,14 @@
"default": "mdi:hand-heart-outline"
}
},
"calendar": {
"todos": {
"default": "mdi:calendar-check"
},
"dailys": {
"default": "mdi:calendar-multiple"
}
},
"sensor": {
"display_name": {
"default": "mdi:account-circle"

View File

@ -84,6 +84,23 @@
"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": {
"display_name": {
"name": "Display name"

View File

@ -24,7 +24,7 @@ from homeassistant.util import dt as dt_util
from .const import ASSETS_URL, DOMAIN
from .coordinator import HabiticaDataUpdateCoordinator
from .entity import HabiticaBase
from .types import HabiticaConfigEntry
from .types import HabiticaConfigEntry, HabiticaTaskType
from .util import next_due_date
@ -37,15 +37,6 @@ class HabiticaTodoList(StrEnum):
REWARDS = "rewards"
class HabiticaTaskType(StrEnum):
"""Habitica Entities."""
HABIT = "habit"
DAILY = "daily"
TODO = "todo"
REWARD = "reward"
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HabiticaConfigEntry,

View File

@ -1,7 +1,18 @@
"""Types for Habitica integration."""
from enum import StrEnum
from homeassistant.config_entries import ConfigEntry
from .coordinator import HabiticaDataUpdateCoordinator
type HabiticaConfigEntry = ConfigEntry[HabiticaDataUpdateCoordinator]
class HabiticaTaskType(StrEnum):
"""Habitica Entities."""
HABIT = "habit"
DAILY = "daily"
TODO = "todo"
REWARD = "reward"

View File

@ -5,6 +5,21 @@ from __future__ import annotations
import datetime
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.script import scripts_with_entity
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 += scripts_with_entity(hass, entity_id)
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]

View File

@ -444,7 +444,12 @@
"completedBy": {},
"assignedUsers": []
},
"reminders": [],
"reminders": [
{
"id": "91c09432-10ac-4a49-bd20-823081ec29ed",
"time": "2024-09-22T02:00:00.0000Z"
}
],
"byHabitica": false,
"createdAt": "2024-09-21T22:17:19.513Z",
"updatedAt": "2024-09-21T22:19:35.576Z",
@ -477,7 +482,7 @@
},
{
"_id": "86ea2475-d1b5-4020-bdcc-c188c7996afa",
"date": "2024-09-26T22:15:00.000Z",
"date": "2024-09-21T22:00:00.000Z",
"completed": false,
"collapseChecklist": false,
"checklist": [],

View File

@ -34,6 +34,24 @@
"flags": {
"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,
"lastCron": "2024-09-21T22:01:55.586Z"
}

View File

@ -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',
})
# ---

View File

@ -72,7 +72,7 @@
}),
dict({
'description': 'Den Ausflug für das kommende Wochenende organisieren.',
'due': '2024-09-26',
'due': '2024-09-21',
'status': 'needs_action',
'summary': 'Wochenendausflug planen',
'uid': '86ea2475-d1b5-4020-bdcc-c188c7996afa',

View File

@ -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