Home Assistant Unofficial Reference 2024.12.1
calendar.py
Go to the documentation of this file.
1 """Calendar platform for Habitica integration."""
2 
3 from __future__ import annotations
4 
5 from datetime import date, datetime, timedelta
6 from enum import StrEnum
7 
8 from dateutil.rrule import rrule
9 
11  CalendarEntity,
12  CalendarEntityDescription,
13  CalendarEvent,
14 )
15 from homeassistant.core import HomeAssistant
16 from homeassistant.helpers.entity_platform import AddEntitiesCallback
17 from homeassistant.util import dt as dt_util
18 
19 from . import HabiticaConfigEntry
20 from .coordinator import HabiticaDataUpdateCoordinator
21 from .entity import HabiticaBase
22 from .types import HabiticaTaskType
23 from .util import build_rrule, get_recurrence_rule
24 
25 
26 class HabiticaCalendar(StrEnum):
27  """Habitica calendars."""
28 
29  DAILIES = "dailys"
30  TODOS = "todos"
31  TODO_REMINDERS = "todo_reminders"
32  DAILY_REMINDERS = "daily_reminders"
33 
34 
36  hass: HomeAssistant,
37  config_entry: HabiticaConfigEntry,
38  async_add_entities: AddEntitiesCallback,
39 ) -> None:
40  """Set up the calendar platform."""
41  coordinator = config_entry.runtime_data
42 
44  [
45  HabiticaTodosCalendarEntity(coordinator),
46  HabiticaDailiesCalendarEntity(coordinator),
49  ]
50  )
51 
52 
54  """Base Habitica calendar entity."""
55 
56  def __init__(
57  self,
58  coordinator: HabiticaDataUpdateCoordinator,
59  ) -> None:
60  """Initialize calendar entity."""
61  super().__init__(coordinator, self.entity_descriptionentity_description)
62 
63 
65  """Habitica todos calendar entity."""
66 
67  entity_description = CalendarEntityDescription(
68  key=HabiticaCalendar.TODOS,
69  translation_key=HabiticaCalendar.TODOS,
70  )
71 
73  self, start_date: datetime, end_date: datetime | None = None
74  ) -> list[CalendarEvent]:
75  """Get all dated todos."""
76 
77  events = []
78  for task in self.coordinator.data.tasks:
79  if not (
80  task["type"] == HabiticaTaskType.TODO
81  and not task["completed"]
82  and task.get("date") # only if has due date
83  ):
84  continue
85 
86  start = dt_util.start_of_local_day(datetime.fromisoformat(task["date"]))
87  end = start + timedelta(days=1)
88  # return current and upcoming events or events within the requested range
89 
90  if end < start_date:
91  # Event ends before date range
92  continue
93 
94  if end_date and start > end_date:
95  # Event starts after date range
96  continue
97 
98  events.append(
100  start=start.date(),
101  end=end.date(),
102  summary=task["text"],
103  description=task["notes"],
104  uid=task["id"],
105  )
106  )
107  return sorted(
108  events,
109  key=lambda event: (
110  event.start,
111  self.coordinator.data.user["tasksOrder"]["todos"].index(event.uid),
112  ),
113  )
114 
115  @property
116  def event(self) -> CalendarEvent | None:
117  """Return the current or next upcoming event."""
118 
119  return next(iter(self.dated_todosdated_todos(dt_util.now())), None)
120 
121  async def async_get_events(
122  self, hass: HomeAssistant, start_date: datetime, end_date: datetime
123  ) -> list[CalendarEvent]:
124  """Return calendar events within a datetime range."""
125  return self.dated_todosdated_todos(start_date, end_date)
126 
127 
129  """Habitica dailies calendar entity."""
130 
131  entity_description = CalendarEntityDescription(
132  key=HabiticaCalendar.DAILIES,
133  translation_key=HabiticaCalendar.DAILIES,
134  )
135 
136  @property
137  def today(self) -> datetime:
138  """Habitica daystart."""
139  return dt_util.start_of_local_day(
140  datetime.fromisoformat(self.coordinator.data.user["lastCron"])
141  )
142 
143  def end_date(self, recurrence: datetime, end: datetime | None = None) -> date:
144  """Calculate the end date for a yesterdaily.
145 
146  The enddates of events from yesterday move forward to the end
147  of the current day (until the cron resets the dailies) to show them
148  as still active events on the calendar state entity (state: on).
149 
150  Events in the calendar view will show all-day events on their due day
151  """
152  if end:
153  return recurrence.date() + timedelta(days=1)
154  return (
155  dt_util.start_of_local_day() if recurrence == self.todaytoday else recurrence
156  ).date() + timedelta(days=1)
157 
159  self, recurrences: rrule, start_date: datetime, end_date: datetime | None = None
160  ) -> list[datetime]:
161  """Calculate recurrence dates based on start_date and end_date."""
162  if end_date:
163  return recurrences.between(
164  start_date, end_date - timedelta(days=1), inc=True
165  )
166  # if no end_date is given, return only the next recurrence
167  return [recurrences.after(self.todaytoday, inc=True)]
168 
170  self, start_date: datetime, end_date: datetime | None = None
171  ) -> list[CalendarEvent]:
172  """Get dailies and recurrences for a given period or the next upcoming."""
173 
174  # we only have dailies for today and future recurrences
175  if end_date and end_date < self.todaytoday:
176  return []
177  start_date = max(start_date, self.todaytoday)
178 
179  events = []
180  for task in self.coordinator.data.tasks:
181  # only dailies that that are not 'grey dailies'
182  if not (task["type"] == HabiticaTaskType.DAILY and task["everyX"]):
183  continue
184 
185  recurrences = build_rrule(task)
186  recurrence_dates = self.get_recurrence_datesget_recurrence_dates(
187  recurrences, start_date, end_date
188  )
189  for recurrence in recurrence_dates:
190  is_future_event = recurrence > self.todaytoday
191  is_current_event = recurrence <= self.todaytoday and not task["completed"]
192 
193  if not (is_future_event or is_current_event):
194  continue
195 
196  events.append(
198  start=recurrence.date(),
199  end=self.end_dateend_date(recurrence, end_date),
200  summary=task["text"],
201  description=task["notes"],
202  uid=task["id"],
203  rrule=get_recurrence_rule(recurrences),
204  )
205  )
206  return sorted(
207  events,
208  key=lambda event: (
209  event.start,
210  self.coordinator.data.user["tasksOrder"]["dailys"].index(event.uid),
211  ),
212  )
213 
214  @property
215  def event(self) -> CalendarEvent | None:
216  """Return the next upcoming event."""
217  return next(iter(self.due_dailiesdue_dailies(self.todaytoday)), None)
218 
219  async def async_get_events(
220  self, hass: HomeAssistant, start_date: datetime, end_date: datetime
221  ) -> list[CalendarEvent]:
222  """Return calendar events within a datetime range."""
223 
224  return self.due_dailiesdue_dailies(start_date, end_date)
225 
226  @property
227  def extra_state_attributes(self) -> dict[str, bool | None] | None:
228  """Return entity specific state attributes."""
229  return {
230  "yesterdaily": self.eventeventevent.start < self.todaytoday.date() if self.eventeventevent else None
231  }
232 
233 
235  """Habitica to-do reminders calendar entity."""
236 
237  entity_description = CalendarEntityDescription(
238  key=HabiticaCalendar.TODO_REMINDERS,
239  translation_key=HabiticaCalendar.TODO_REMINDERS,
240  )
241 
243  self, start_date: datetime, end_date: datetime | None = None
244  ) -> list[CalendarEvent]:
245  """Reminders for todos."""
246 
247  events = []
248 
249  for task in self.coordinator.data.tasks:
250  if task["type"] != HabiticaTaskType.TODO or task["completed"]:
251  continue
252 
253  for reminder in task.get("reminders", []):
254  # reminders are returned by the API in local time but with wrong
255  # timezone (UTC) and arbitrary added seconds/microseconds. When
256  # creating reminders in Habitica only hours and minutes can be defined.
257  start = datetime.fromisoformat(reminder["time"]).replace(
258  tzinfo=dt_util.DEFAULT_TIME_ZONE, second=0, microsecond=0
259  )
260  end = start + timedelta(hours=1)
261 
262  if end < start_date:
263  # Event ends before date range
264  continue
265 
266  if end_date and start > end_date:
267  # Event starts after date range
268  continue
269 
270  events.append(
272  start=start,
273  end=end,
274  summary=task["text"],
275  description=task["notes"],
276  uid=f"{task["id"]}_{reminder["id"]}",
277  )
278  )
279 
280  return sorted(
281  events,
282  key=lambda event: event.start,
283  )
284 
285  @property
286  def event(self) -> CalendarEvent | None:
287  """Return the next upcoming event."""
288  return next(iter(self.remindersreminders(dt_util.now())), None)
289 
290  async def async_get_events(
291  self, hass: HomeAssistant, start_date: datetime, end_date: datetime
292  ) -> list[CalendarEvent]:
293  """Return calendar events within a datetime range."""
294 
295  return self.remindersreminders(start_date, end_date)
296 
297 
299  """Habitica daily reminders calendar entity."""
300 
301  entity_description = CalendarEntityDescription(
302  key=HabiticaCalendar.DAILY_REMINDERS,
303  translation_key=HabiticaCalendar.DAILY_REMINDERS,
304  )
305 
306  def start(self, reminder_time: str, reminder_date: date) -> datetime:
307  """Generate reminder times for dailies.
308 
309  Reminders for dailies have a datetime but the date part is arbitrary,
310  only the time part is evaluated. The dates for the reminders are the
311  dailies' due dates.
312  """
313  return datetime.combine(
314  reminder_date,
315  datetime.fromisoformat(reminder_time)
316  .replace(
317  second=0,
318  microsecond=0,
319  )
320  .time(),
321  tzinfo=dt_util.DEFAULT_TIME_ZONE,
322  )
323 
324  @property
325  def today(self) -> datetime:
326  """Habitica daystart."""
327  return dt_util.start_of_local_day(
328  datetime.fromisoformat(self.coordinator.data.user["lastCron"])
329  )
330 
332  self, recurrences: rrule, start_date: datetime, end_date: datetime | None = None
333  ) -> list[datetime]:
334  """Calculate recurrence dates based on start_date and end_date."""
335  if end_date:
336  return recurrences.between(
337  start_date, end_date - timedelta(days=1), inc=True
338  )
339  # if no end_date is given, return only the next recurrence
340  return [recurrences.after(self.todaytoday, inc=True)]
341 
343  self, start_date: datetime, end_date: datetime | None = None
344  ) -> list[CalendarEvent]:
345  """Reminders for dailies."""
346 
347  events = []
348  if end_date and end_date < self.todaytoday:
349  return []
350  start_date = max(start_date, self.todaytoday)
351 
352  for task in self.coordinator.data.tasks:
353  if not (task["type"] == HabiticaTaskType.DAILY and task["everyX"]):
354  continue
355 
356  recurrences = build_rrule(task)
357  recurrences_start = self.todaytoday
358 
359  recurrence_dates = self.get_recurrence_datesget_recurrence_dates(
360  recurrences, recurrences_start, end_date
361  )
362  for recurrence in recurrence_dates:
363  is_future_event = recurrence > self.todaytoday
364  is_current_event = recurrence <= self.todaytoday and not task["completed"]
365 
366  if not is_future_event and not is_current_event:
367  continue
368 
369  for reminder in task.get("reminders", []):
370  start = self.startstart(reminder["time"], recurrence)
371  end = start + timedelta(hours=1)
372 
373  if end < start_date:
374  # Event ends before date range
375  continue
376 
377  if end_date and start > end_date:
378  # Event starts after date range
379  continue
380  events.append(
382  start=start,
383  end=end,
384  summary=task["text"],
385  description=task["notes"],
386  uid=f"{task["id"]}_{reminder["id"]}",
387  )
388  )
389 
390  return sorted(
391  events,
392  key=lambda event: event.start,
393  )
394 
395  @property
396  def event(self) -> CalendarEvent | None:
397  """Return the next upcoming event."""
398  return next(iter(self.remindersreminders(dt_util.now())), None)
399 
400  async def async_get_events(
401  self, hass: HomeAssistant, start_date: datetime, end_date: datetime
402  ) -> list[CalendarEvent]:
403  """Return calendar events within a datetime range."""
404 
405  return self.remindersreminders(start_date, end_date)
None __init__(self, HabiticaDataUpdateCoordinator coordinator)
Definition: calendar.py:59
date end_date(self, datetime recurrence, datetime|None end=None)
Definition: calendar.py:143
list[CalendarEvent] due_dailies(self, datetime start_date, datetime|None end_date=None)
Definition: calendar.py:171
list[datetime] get_recurrence_dates(self, rrule recurrences, datetime start_date, datetime|None end_date=None)
Definition: calendar.py:160
list[CalendarEvent] async_get_events(self, HomeAssistant hass, datetime start_date, datetime end_date)
Definition: calendar.py:221
list[datetime] get_recurrence_dates(self, rrule recurrences, datetime start_date, datetime|None end_date=None)
Definition: calendar.py:333
list[CalendarEvent] reminders(self, datetime start_date, datetime|None end_date=None)
Definition: calendar.py:344
datetime start(self, str reminder_time, date reminder_date)
Definition: calendar.py:306
list[CalendarEvent] async_get_events(self, HomeAssistant hass, datetime start_date, datetime end_date)
Definition: calendar.py:402
list[CalendarEvent] reminders(self, datetime start_date, datetime|None end_date=None)
Definition: calendar.py:244
list[CalendarEvent] async_get_events(self, HomeAssistant hass, datetime start_date, datetime end_date)
Definition: calendar.py:292
list[CalendarEvent] dated_todos(self, datetime start_date, datetime|None end_date=None)
Definition: calendar.py:74
list[CalendarEvent] async_get_events(self, HomeAssistant hass, datetime start_date, datetime end_date)
Definition: calendar.py:123
None async_setup_entry(HomeAssistant hass, HabiticaConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: calendar.py:39
str get_recurrence_rule(rrule recurrence)
Definition: util.py:117
rrule build_rrule(dict[str, Any] task)
Definition: util.py:87
bool time(HomeAssistant hass, dt_time|str|None before=None, dt_time|str|None after=None, str|Container[str]|None weekday=None)
Definition: condition.py:802