Add calendars for to-do and daily reminders to Habitica integration (#130789)

* Add calendars for to-do and daily reminders

* revert order

* changes
This commit is contained in:
Manu 2024-11-20 03:37:23 +01:00 committed by GitHub
parent 397a299f15
commit 5daf95ec8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 562 additions and 1 deletions

View File

@ -28,6 +28,8 @@ class HabiticaCalendar(StrEnum):
DAILIES = "dailys"
TODOS = "todos"
TODO_REMINDERS = "todo_reminders"
DAILY_REMINDERS = "daily_reminders"
async def async_setup_entry(
@ -42,6 +44,8 @@ async def async_setup_entry(
[
HabiticaTodosCalendarEntity(coordinator),
HabiticaDailiesCalendarEntity(coordinator),
HabiticaTodoRemindersCalendarEntity(coordinator),
HabiticaDailyRemindersCalendarEntity(coordinator),
]
)
@ -225,3 +229,177 @@ class HabiticaDailiesCalendarEntity(HabiticaCalendarEntity):
return {
"yesterdaily": self.event.start < self.today.date() if self.event else None
}
class HabiticaTodoRemindersCalendarEntity(HabiticaCalendarEntity):
"""Habitica to-do reminders calendar entity."""
entity_description = CalendarEntityDescription(
key=HabiticaCalendar.TODO_REMINDERS,
translation_key=HabiticaCalendar.TODO_REMINDERS,
)
def reminders(
self, start_date: datetime, end_date: datetime | None = None
) -> list[CalendarEvent]:
"""Reminders for todos."""
events = []
for task in self.coordinator.data.tasks:
if task["type"] != HabiticaTaskType.TODO or task["completed"]:
continue
for reminder in task.get("reminders", []):
# reminders are returned by the API in local time but with wrong
# timezone (UTC) and arbitrary added seconds/microseconds. When
# creating reminders in Habitica only hours and minutes can be defined.
start = datetime.fromisoformat(reminder["time"]).replace(
tzinfo=dt_util.DEFAULT_TIME_ZONE, second=0, microsecond=0
)
end = start + timedelta(hours=1)
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,
end=end,
summary=task["text"],
description=task["notes"],
uid=f"{task["id"]}_{reminder["id"]}",
)
)
return sorted(
events,
key=lambda event: event.start,
)
@property
def event(self) -> CalendarEvent | None:
"""Return the next upcoming event."""
return next(iter(self.reminders(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.reminders(start_date, end_date)
class HabiticaDailyRemindersCalendarEntity(HabiticaCalendarEntity):
"""Habitica daily reminders calendar entity."""
entity_description = CalendarEntityDescription(
key=HabiticaCalendar.DAILY_REMINDERS,
translation_key=HabiticaCalendar.DAILY_REMINDERS,
)
def start(self, reminder_time: str, reminder_date: date) -> datetime:
"""Generate reminder times for dailies.
Reminders for dailies have a datetime but the date part is arbitrary,
only the time part is evaluated. The dates for the reminders are the
dailies' due dates.
"""
return datetime.combine(
reminder_date,
datetime.fromisoformat(reminder_time)
.replace(
second=0,
microsecond=0,
)
.time(),
tzinfo=dt_util.DEFAULT_TIME_ZONE,
)
@property
def today(self) -> datetime:
"""Habitica daystart."""
return dt_util.start_of_local_day(
datetime.fromisoformat(self.coordinator.data.user["lastCron"])
)
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 reminders(
self, start_date: datetime, end_date: datetime | None = None
) -> list[CalendarEvent]:
"""Reminders for dailies."""
events = []
if end_date and end_date < self.today:
return []
start_date = max(start_date, self.today)
for task in self.coordinator.data.tasks:
if not (task["type"] == HabiticaTaskType.DAILY and task["everyX"]):
continue
recurrences = build_rrule(task)
recurrences_start = self.today
recurrence_dates = self.get_recurrence_dates(
recurrences, recurrences_start, 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 and not is_current_event:
continue
for reminder in task.get("reminders", []):
start = self.start(reminder["time"], recurrence)
end = start + timedelta(hours=1)
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,
end=end,
summary=task["text"],
description=task["notes"],
uid=f"{task["id"]}_{reminder["id"]}",
)
)
return sorted(
events,
key=lambda event: event.start,
)
@property
def event(self) -> CalendarEvent | None:
"""Return the next upcoming event."""
return next(iter(self.reminders(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.reminders(start_date, end_date)

View File

@ -64,6 +64,12 @@
},
"dailys": {
"default": "mdi:calendar-multiple"
},
"todo_reminders": {
"default": "mdi:reminder"
},
"daily_reminders": {
"default": "mdi:reminder"
}
},
"sensor": {

View File

@ -109,6 +109,12 @@
}
}
}
},
"todo_reminders": {
"name": "To-do reminders"
},
"daily_reminders": {
"name": "Daily reminders"
}
},
"sensor": {

View File

@ -345,7 +345,12 @@
"daysOfMonth": [],
"weeksOfMonth": [],
"checklist": [],
"reminders": [],
"reminders": [
{
"id": "1491d640-6b21-4d0c-8940-0b7aa61c8836",
"time": "2024-09-22T20:00:00.0000Z"
}
],
"createdAt": "2024-07-07T17:51:53.266Z",
"updatedAt": "2024-09-21T22:51:41.756Z",
"userId": "5f359083-ef78-4af0-985a-0b2c6d05797c",

View File

@ -577,6 +577,266 @@
}),
])
# ---
# name: test_api_events[calendar.test_user_daily_reminders]
list([
dict({
'description': 'Klicke um Deinen Terminplan festzulegen!',
'end': dict({
'dateTime': '2024-09-21T21:00:00+02:00',
}),
'location': None,
'recurrence_id': None,
'rrule': None,
'start': dict({
'dateTime': '2024-09-21T20:00:00+02:00',
}),
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
dict({
'description': 'Klicke um Deinen Terminplan festzulegen!',
'end': dict({
'dateTime': '2024-09-22T21:00:00+02:00',
}),
'location': None,
'recurrence_id': None,
'rrule': None,
'start': dict({
'dateTime': '2024-09-22T20:00:00+02:00',
}),
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
dict({
'description': 'Klicke um Deinen Terminplan festzulegen!',
'end': dict({
'dateTime': '2024-09-23T21:00:00+02:00',
}),
'location': None,
'recurrence_id': None,
'rrule': None,
'start': dict({
'dateTime': '2024-09-23T20:00:00+02:00',
}),
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
dict({
'description': 'Klicke um Deinen Terminplan festzulegen!',
'end': dict({
'dateTime': '2024-09-24T21:00:00+02:00',
}),
'location': None,
'recurrence_id': None,
'rrule': None,
'start': dict({
'dateTime': '2024-09-24T20:00:00+02:00',
}),
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
dict({
'description': 'Klicke um Deinen Terminplan festzulegen!',
'end': dict({
'dateTime': '2024-09-25T21:00:00+02:00',
}),
'location': None,
'recurrence_id': None,
'rrule': None,
'start': dict({
'dateTime': '2024-09-25T20:00:00+02:00',
}),
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
dict({
'description': 'Klicke um Deinen Terminplan festzulegen!',
'end': dict({
'dateTime': '2024-09-26T21:00:00+02:00',
}),
'location': None,
'recurrence_id': None,
'rrule': None,
'start': dict({
'dateTime': '2024-09-26T20:00:00+02:00',
}),
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
dict({
'description': 'Klicke um Deinen Terminplan festzulegen!',
'end': dict({
'dateTime': '2024-09-27T21:00:00+02:00',
}),
'location': None,
'recurrence_id': None,
'rrule': None,
'start': dict({
'dateTime': '2024-09-27T20:00:00+02:00',
}),
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
dict({
'description': 'Klicke um Deinen Terminplan festzulegen!',
'end': dict({
'dateTime': '2024-09-28T21:00:00+02:00',
}),
'location': None,
'recurrence_id': None,
'rrule': None,
'start': dict({
'dateTime': '2024-09-28T20:00:00+02:00',
}),
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
dict({
'description': 'Klicke um Deinen Terminplan festzulegen!',
'end': dict({
'dateTime': '2024-09-29T21:00:00+02:00',
}),
'location': None,
'recurrence_id': None,
'rrule': None,
'start': dict({
'dateTime': '2024-09-29T20:00:00+02:00',
}),
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
dict({
'description': 'Klicke um Deinen Terminplan festzulegen!',
'end': dict({
'dateTime': '2024-09-30T21:00:00+02:00',
}),
'location': None,
'recurrence_id': None,
'rrule': None,
'start': dict({
'dateTime': '2024-09-30T20:00:00+02:00',
}),
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
dict({
'description': 'Klicke um Deinen Terminplan festzulegen!',
'end': dict({
'dateTime': '2024-10-01T21:00:00+02:00',
}),
'location': None,
'recurrence_id': None,
'rrule': None,
'start': dict({
'dateTime': '2024-10-01T20:00:00+02:00',
}),
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
dict({
'description': 'Klicke um Deinen Terminplan festzulegen!',
'end': dict({
'dateTime': '2024-10-02T21:00:00+02:00',
}),
'location': None,
'recurrence_id': None,
'rrule': None,
'start': dict({
'dateTime': '2024-10-02T20:00:00+02:00',
}),
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
dict({
'description': 'Klicke um Deinen Terminplan festzulegen!',
'end': dict({
'dateTime': '2024-10-03T21:00:00+02:00',
}),
'location': None,
'recurrence_id': None,
'rrule': None,
'start': dict({
'dateTime': '2024-10-03T20:00:00+02:00',
}),
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
dict({
'description': 'Klicke um Deinen Terminplan festzulegen!',
'end': dict({
'dateTime': '2024-10-04T21:00:00+02:00',
}),
'location': None,
'recurrence_id': None,
'rrule': None,
'start': dict({
'dateTime': '2024-10-04T20:00:00+02:00',
}),
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
dict({
'description': 'Klicke um Deinen Terminplan festzulegen!',
'end': dict({
'dateTime': '2024-10-05T21:00:00+02:00',
}),
'location': None,
'recurrence_id': None,
'rrule': None,
'start': dict({
'dateTime': '2024-10-05T20:00:00+02:00',
}),
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
dict({
'description': 'Klicke um Deinen Terminplan festzulegen!',
'end': dict({
'dateTime': '2024-10-06T21:00:00+02:00',
}),
'location': None,
'recurrence_id': None,
'rrule': None,
'start': dict({
'dateTime': '2024-10-06T20:00:00+02:00',
}),
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
dict({
'description': 'Klicke um Deinen Terminplan festzulegen!',
'end': dict({
'dateTime': '2024-10-07T21:00:00+02:00',
}),
'location': None,
'recurrence_id': None,
'rrule': None,
'start': dict({
'dateTime': '2024-10-07T20:00:00+02:00',
}),
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f_1491d640-6b21-4d0c-8940-0b7aa61c8836',
}),
])
# ---
# name: test_api_events[calendar.test_user_to_do_reminders]
list([
dict({
'description': 'Strom- und Internetrechnungen rechtzeitig überweisen.',
'end': dict({
'dateTime': '2024-09-22T03:00:00+02:00',
}),
'location': None,
'recurrence_id': None,
'rrule': None,
'start': dict({
'dateTime': '2024-09-22T02:00:00+02:00',
}),
'summary': 'Rechnungen bezahlen',
'uid': '2f6fcabc-f670-4ec3-ba65-817e8deea490_91c09432-10ac-4a49-bd20-823081ec29ed',
}),
])
# ---
# name: test_api_events[calendar.test_user_to_do_s]
list([
dict({
@ -676,6 +936,110 @@
'state': 'on',
})
# ---
# name: test_calendar_platform[calendar.test_user_daily_reminders-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_daily_reminders',
'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': 'Daily reminders',
'platform': 'habitica',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': <HabiticaCalendar.DAILY_REMINDERS: 'daily_reminders'>,
'unique_id': '00000000-0000-0000-0000-000000000000_daily_reminders',
'unit_of_measurement': None,
})
# ---
# name: test_calendar_platform[calendar.test_user_daily_reminders-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'all_day': False,
'description': 'Klicke um Deinen Terminplan festzulegen!',
'end_time': '2024-09-21 21:00:00',
'friendly_name': 'test-user Daily reminders',
'location': '',
'message': '5 Minuten ruhig durchatmen',
'start_time': '2024-09-21 20:00:00',
}),
'context': <ANY>,
'entity_id': 'calendar.test_user_daily_reminders',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_calendar_platform[calendar.test_user_to_do_reminders-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_reminders',
'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 reminders',
'platform': 'habitica',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': <HabiticaCalendar.TODO_REMINDERS: 'todo_reminders'>,
'unique_id': '00000000-0000-0000-0000-000000000000_todo_reminders',
'unit_of_measurement': None,
})
# ---
# name: test_calendar_platform[calendar.test_user_to_do_reminders-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'all_day': False,
'description': 'Strom- und Internetrechnungen rechtzeitig überweisen.',
'end_time': '2024-09-22 03:00:00',
'friendly_name': 'test-user To-do reminders',
'location': '',
'message': 'Rechnungen bezahlen',
'start_time': '2024-09-22 02:00:00',
}),
'context': <ANY>,
'entity_id': 'calendar.test_user_to_do_reminders',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_calendar_platform[calendar.test_user_to_do_s-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -55,6 +55,8 @@ async def test_calendar_platform(
[
"calendar.test_user_to_do_s",
"calendar.test_user_dailies",
"calendar.test_user_daily_reminders",
"calendar.test_user_to_do_reminders",
],
)
@pytest.mark.freeze_time("2024-09-20T22:00:00.000Z")