2024-10-30 03:53:49 +00:00
|
|
|
"""Calendar platform for Habitica integration."""
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
2024-11-29 02:31:38 +00:00
|
|
|
from abc import abstractmethod
|
2024-10-30 03:53:49 +00:00
|
|
|
from datetime import date, datetime, timedelta
|
|
|
|
from enum import StrEnum
|
2024-12-29 14:00:31 +00:00
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from uuid import UUID
|
2024-10-30 03:53:49 +00:00
|
|
|
|
|
|
|
from dateutil.rrule import rrule
|
2024-12-29 14:00:31 +00:00
|
|
|
from habiticalib import TaskType
|
2024-10-30 03:53:49 +00:00
|
|
|
|
|
|
|
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 .util import build_rrule, get_recurrence_rule
|
|
|
|
|
|
|
|
|
|
|
|
class HabiticaCalendar(StrEnum):
|
|
|
|
"""Habitica calendars."""
|
|
|
|
|
|
|
|
DAILIES = "dailys"
|
|
|
|
TODOS = "todos"
|
2024-11-20 02:37:23 +00:00
|
|
|
TODO_REMINDERS = "todo_reminders"
|
|
|
|
DAILY_REMINDERS = "daily_reminders"
|
2024-10-30 03:53:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
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),
|
2024-11-20 02:37:23 +00:00
|
|
|
HabiticaTodoRemindersCalendarEntity(coordinator),
|
|
|
|
HabiticaDailyRemindersCalendarEntity(coordinator),
|
2024-10-30 03:53:49 +00:00
|
|
|
]
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class HabiticaCalendarEntity(HabiticaBase, CalendarEntity):
|
|
|
|
"""Base Habitica calendar entity."""
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
coordinator: HabiticaDataUpdateCoordinator,
|
|
|
|
) -> None:
|
|
|
|
"""Initialize calendar entity."""
|
|
|
|
super().__init__(coordinator, self.entity_description)
|
|
|
|
|
2024-11-29 02:31:38 +00:00
|
|
|
@abstractmethod
|
|
|
|
def get_events(
|
|
|
|
self, start_date: datetime, end_date: datetime | None = None
|
|
|
|
) -> list[CalendarEvent]:
|
|
|
|
"""Return events."""
|
|
|
|
|
|
|
|
@property
|
|
|
|
def event(self) -> CalendarEvent | None:
|
|
|
|
"""Return the current or next upcoming event."""
|
|
|
|
|
|
|
|
return next(iter(self.get_events(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.get_events(start_date, end_date)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def start_of_today(self) -> datetime:
|
|
|
|
"""Habitica daystart."""
|
2024-12-29 14:00:31 +00:00
|
|
|
return dt_util.start_of_local_day(self.coordinator.data.user.lastCron)
|
2024-11-29 02:31:38 +00:00
|
|
|
|
|
|
|
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(start_date, inc=True)]
|
|
|
|
|
2024-10-30 03:53:49 +00:00
|
|
|
|
|
|
|
class HabiticaTodosCalendarEntity(HabiticaCalendarEntity):
|
|
|
|
"""Habitica todos calendar entity."""
|
|
|
|
|
|
|
|
entity_description = CalendarEntityDescription(
|
|
|
|
key=HabiticaCalendar.TODOS,
|
|
|
|
translation_key=HabiticaCalendar.TODOS,
|
|
|
|
)
|
|
|
|
|
2024-11-29 02:31:38 +00:00
|
|
|
def get_events(
|
2024-10-30 03:53:49 +00:00
|
|
|
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 (
|
2024-12-29 14:00:31 +00:00
|
|
|
task.Type is TaskType.TODO
|
|
|
|
and not task.completed
|
|
|
|
and task.date is not None # only if has due date
|
2024-10-30 03:53:49 +00:00
|
|
|
):
|
|
|
|
continue
|
|
|
|
|
2024-12-29 14:00:31 +00:00
|
|
|
start = dt_util.start_of_local_day(task.date)
|
2024-10-30 03:53:49 +00:00
|
|
|
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
|
2024-12-29 14:00:31 +00:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
assert task.text
|
|
|
|
assert task.id
|
2024-10-30 03:53:49 +00:00
|
|
|
events.append(
|
|
|
|
CalendarEvent(
|
|
|
|
start=start.date(),
|
|
|
|
end=end.date(),
|
2024-12-29 14:00:31 +00:00
|
|
|
summary=task.text,
|
|
|
|
description=task.notes,
|
|
|
|
uid=str(task.id),
|
2024-10-30 03:53:49 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
return sorted(
|
|
|
|
events,
|
|
|
|
key=lambda event: (
|
|
|
|
event.start,
|
2024-12-29 14:00:31 +00:00
|
|
|
self.coordinator.data.user.tasksOrder.todos.index(UUID(event.uid)),
|
2024-10-30 03:53:49 +00:00
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class HabiticaDailiesCalendarEntity(HabiticaCalendarEntity):
|
|
|
|
"""Habitica dailies calendar entity."""
|
|
|
|
|
|
|
|
entity_description = CalendarEntityDescription(
|
|
|
|
key=HabiticaCalendar.DAILIES,
|
|
|
|
translation_key=HabiticaCalendar.DAILIES,
|
|
|
|
)
|
|
|
|
|
|
|
|
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 (
|
2024-11-29 02:31:38 +00:00
|
|
|
dt_util.start_of_local_day()
|
|
|
|
if recurrence == self.start_of_today
|
|
|
|
else recurrence
|
2024-10-30 03:53:49 +00:00
|
|
|
).date() + timedelta(days=1)
|
|
|
|
|
2024-11-29 02:31:38 +00:00
|
|
|
def get_events(
|
2024-10-30 03:53:49 +00:00
|
|
|
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
|
2024-11-29 02:31:38 +00:00
|
|
|
if end_date and end_date < self.start_of_today:
|
2024-10-30 03:53:49 +00:00
|
|
|
return []
|
2024-11-29 02:31:38 +00:00
|
|
|
start_date = max(start_date, self.start_of_today)
|
2024-10-30 03:53:49 +00:00
|
|
|
|
|
|
|
events = []
|
|
|
|
for task in self.coordinator.data.tasks:
|
|
|
|
# only dailies that that are not 'grey dailies'
|
2024-12-29 14:00:31 +00:00
|
|
|
if not (task.Type is TaskType.DAILY and task.everyX):
|
2024-10-30 03:53:49 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
recurrences = build_rrule(task)
|
|
|
|
recurrence_dates = self.get_recurrence_dates(
|
|
|
|
recurrences, start_date, end_date
|
|
|
|
)
|
|
|
|
for recurrence in recurrence_dates:
|
2024-11-29 02:31:38 +00:00
|
|
|
is_future_event = recurrence > self.start_of_today
|
|
|
|
is_current_event = (
|
2024-12-29 14:00:31 +00:00
|
|
|
recurrence <= self.start_of_today and not task.completed
|
2024-11-29 02:31:38 +00:00
|
|
|
)
|
2024-10-30 03:53:49 +00:00
|
|
|
|
2024-11-29 02:31:38 +00:00
|
|
|
if not is_future_event and not is_current_event:
|
2024-10-30 03:53:49 +00:00
|
|
|
continue
|
2024-12-29 14:00:31 +00:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
assert task.text
|
|
|
|
assert task.id
|
2024-10-30 03:53:49 +00:00
|
|
|
events.append(
|
|
|
|
CalendarEvent(
|
|
|
|
start=recurrence.date(),
|
|
|
|
end=self.end_date(recurrence, end_date),
|
2024-12-29 14:00:31 +00:00
|
|
|
summary=task.text,
|
|
|
|
description=task.notes,
|
|
|
|
uid=str(task.id),
|
2024-10-30 03:53:49 +00:00
|
|
|
rrule=get_recurrence_rule(recurrences),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return sorted(
|
|
|
|
events,
|
|
|
|
key=lambda event: (
|
|
|
|
event.start,
|
2024-12-29 14:00:31 +00:00
|
|
|
self.coordinator.data.user.tasksOrder.dailys.index(UUID(event.uid)),
|
2024-10-30 03:53:49 +00:00
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def event(self) -> CalendarEvent | None:
|
|
|
|
"""Return the next upcoming event."""
|
2024-11-29 02:31:38 +00:00
|
|
|
return next(iter(self.get_events(self.start_of_today)), None)
|
2024-10-30 03:53:49 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def extra_state_attributes(self) -> dict[str, bool | None] | None:
|
|
|
|
"""Return entity specific state attributes."""
|
|
|
|
return {
|
2024-11-29 02:31:38 +00:00
|
|
|
"yesterdaily": self.event.start < self.start_of_today.date()
|
|
|
|
if self.event
|
|
|
|
else None
|
2024-10-30 03:53:49 +00:00
|
|
|
}
|
2024-11-20 02:37:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
class HabiticaTodoRemindersCalendarEntity(HabiticaCalendarEntity):
|
|
|
|
"""Habitica to-do reminders calendar entity."""
|
|
|
|
|
|
|
|
entity_description = CalendarEntityDescription(
|
|
|
|
key=HabiticaCalendar.TODO_REMINDERS,
|
|
|
|
translation_key=HabiticaCalendar.TODO_REMINDERS,
|
|
|
|
)
|
|
|
|
|
2024-11-29 02:31:38 +00:00
|
|
|
def get_events(
|
2024-11-20 02:37:23 +00:00
|
|
|
self, start_date: datetime, end_date: datetime | None = None
|
|
|
|
) -> list[CalendarEvent]:
|
|
|
|
"""Reminders for todos."""
|
|
|
|
|
|
|
|
events = []
|
|
|
|
|
|
|
|
for task in self.coordinator.data.tasks:
|
2024-12-29 14:00:31 +00:00
|
|
|
if task.Type is not TaskType.TODO or task.completed:
|
2024-11-20 02:37:23 +00:00
|
|
|
continue
|
|
|
|
|
2024-12-29 14:00:31 +00:00
|
|
|
for reminder in task.reminders:
|
2024-11-20 02:37:23 +00:00
|
|
|
# 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.
|
2024-12-29 14:00:31 +00:00
|
|
|
start = reminder.time.replace(
|
2024-11-20 02:37:23 +00:00
|
|
|
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
|
2024-12-29 14:00:31 +00:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
assert task.text
|
|
|
|
assert task.id
|
2024-11-20 02:37:23 +00:00
|
|
|
events.append(
|
|
|
|
CalendarEvent(
|
|
|
|
start=start,
|
|
|
|
end=end,
|
2024-12-29 14:00:31 +00:00
|
|
|
summary=task.text,
|
|
|
|
description=task.notes,
|
|
|
|
uid=f"{task.id}_{reminder.id}",
|
2024-11-20 02:37:23 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
return sorted(
|
|
|
|
events,
|
|
|
|
key=lambda event: event.start,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class HabiticaDailyRemindersCalendarEntity(HabiticaCalendarEntity):
|
|
|
|
"""Habitica daily reminders calendar entity."""
|
|
|
|
|
|
|
|
entity_description = CalendarEntityDescription(
|
|
|
|
key=HabiticaCalendar.DAILY_REMINDERS,
|
|
|
|
translation_key=HabiticaCalendar.DAILY_REMINDERS,
|
|
|
|
)
|
|
|
|
|
2024-12-29 14:00:31 +00:00
|
|
|
def start(self, reminder_time: datetime, reminder_date: date) -> datetime:
|
2024-11-20 02:37:23 +00:00
|
|
|
"""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,
|
2024-12-29 14:00:31 +00:00
|
|
|
reminder_time.replace(
|
2024-11-20 02:37:23 +00:00
|
|
|
second=0,
|
|
|
|
microsecond=0,
|
2024-12-29 14:00:31 +00:00
|
|
|
).time(),
|
2024-11-20 02:37:23 +00:00
|
|
|
tzinfo=dt_util.DEFAULT_TIME_ZONE,
|
|
|
|
)
|
|
|
|
|
2024-11-29 02:31:38 +00:00
|
|
|
def get_events(
|
2024-11-20 02:37:23 +00:00
|
|
|
self, start_date: datetime, end_date: datetime | None = None
|
|
|
|
) -> list[CalendarEvent]:
|
|
|
|
"""Reminders for dailies."""
|
|
|
|
|
|
|
|
events = []
|
2024-11-29 02:31:38 +00:00
|
|
|
if end_date and end_date < self.start_of_today:
|
2024-11-20 02:37:23 +00:00
|
|
|
return []
|
2024-11-29 02:31:38 +00:00
|
|
|
start_date = max(start_date, self.start_of_today)
|
2024-11-20 02:37:23 +00:00
|
|
|
|
|
|
|
for task in self.coordinator.data.tasks:
|
2024-12-29 14:00:31 +00:00
|
|
|
if not (task.Type is TaskType.DAILY and task.everyX):
|
2024-11-20 02:37:23 +00:00
|
|
|
continue
|
|
|
|
|
|
|
|
recurrences = build_rrule(task)
|
2024-11-29 02:31:38 +00:00
|
|
|
recurrences_start = self.start_of_today
|
2024-11-20 02:37:23 +00:00
|
|
|
|
|
|
|
recurrence_dates = self.get_recurrence_dates(
|
|
|
|
recurrences, recurrences_start, end_date
|
|
|
|
)
|
|
|
|
for recurrence in recurrence_dates:
|
2024-11-29 02:31:38 +00:00
|
|
|
is_future_event = recurrence > self.start_of_today
|
|
|
|
is_current_event = (
|
2024-12-29 14:00:31 +00:00
|
|
|
recurrence <= self.start_of_today and not task.completed
|
2024-11-29 02:31:38 +00:00
|
|
|
)
|
2024-11-20 02:37:23 +00:00
|
|
|
|
|
|
|
if not is_future_event and not is_current_event:
|
|
|
|
continue
|
|
|
|
|
2024-12-29 14:00:31 +00:00
|
|
|
for reminder in task.reminders:
|
|
|
|
start = self.start(reminder.time, recurrence)
|
2024-11-20 02:37:23 +00:00
|
|
|
end = start + timedelta(hours=1)
|
|
|
|
|
|
|
|
if end < start_date:
|
|
|
|
# Event ends before date range
|
|
|
|
continue
|
|
|
|
|
2024-12-29 14:00:31 +00:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
assert task.id
|
|
|
|
assert task.text
|
2024-11-20 02:37:23 +00:00
|
|
|
events.append(
|
|
|
|
CalendarEvent(
|
|
|
|
start=start,
|
|
|
|
end=end,
|
2024-12-29 14:00:31 +00:00
|
|
|
summary=task.text,
|
|
|
|
description=task.notes,
|
|
|
|
uid=f"{task.id}_{reminder.id}",
|
2024-11-20 02:37:23 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
return sorted(
|
|
|
|
events,
|
|
|
|
key=lambda event: event.start,
|
|
|
|
)
|