mirror of https://github.com/home-assistant/core
406 lines
14 KiB
Python
406 lines
14 KiB
Python
"""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"
|
|
TODO_REMINDERS = "todo_reminders"
|
|
DAILY_REMINDERS = "daily_reminders"
|
|
|
|
|
|
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),
|
|
HabiticaTodoRemindersCalendarEntity(coordinator),
|
|
HabiticaDailyRemindersCalendarEntity(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
|
|
}
|
|
|
|
|
|
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)
|