Home Assistant Unofficial Reference 2024.12.1
calendar.py
Go to the documentation of this file.
1 """Support for Todoist task management (https://todoist.com)."""
2 
3 from __future__ import annotations
4 
5 from datetime import date, datetime, timedelta
6 import logging
7 from typing import Any
8 import uuid
9 
10 from todoist_api_python.api_async import TodoistAPIAsync
11 from todoist_api_python.endpoints import get_sync_url
12 from todoist_api_python.headers import create_headers
13 from todoist_api_python.models import Due, Label, Task
14 import voluptuous as vol
15 
17  PLATFORM_SCHEMA as CALENDAR_PLATFORM_SCHEMA,
18  CalendarEntity,
19  CalendarEvent,
20 )
21 from homeassistant.config_entries import ConfigEntry
22 from homeassistant.const import CONF_ID, CONF_NAME, CONF_TOKEN, EVENT_HOMEASSISTANT_STOP
23 from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
24 from homeassistant.exceptions import ServiceValidationError
25 from homeassistant.helpers.aiohttp_client import async_get_clientsession
27 from homeassistant.helpers.entity_platform import AddEntitiesCallback
28 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
29 from homeassistant.helpers.update_coordinator import CoordinatorEntity
30 from homeassistant.util import dt as dt_util
31 
32 from .const import (
33  ALL_DAY,
34  ALL_TASKS,
35  ASSIGNEE,
36  COMPLETED,
37  CONF_EXTRA_PROJECTS,
38  CONF_PROJECT_DUE_DATE,
39  CONF_PROJECT_LABEL_WHITELIST,
40  CONF_PROJECT_WHITELIST,
41  CONTENT,
42  DESCRIPTION,
43  DOMAIN,
44  DUE_DATE,
45  DUE_DATE_LANG,
46  DUE_DATE_STRING,
47  DUE_DATE_VALID_LANGS,
48  DUE_TODAY,
49  END,
50  LABELS,
51  OVERDUE,
52  PRIORITY,
53  PROJECT_NAME,
54  REMINDER_DATE,
55  REMINDER_DATE_LANG,
56  REMINDER_DATE_STRING,
57  SECTION_NAME,
58  SERVICE_NEW_TASK,
59  START,
60  SUMMARY,
61 )
62 from .coordinator import TodoistCoordinator
63 from .types import CalData, CustomProject, ProjectData, TodoistEvent
64 
65 _LOGGER = logging.getLogger(__name__)
66 
67 NEW_TASK_SERVICE_SCHEMA = vol.Schema(
68  {
69  vol.Required(CONTENT): cv.string,
70  vol.Optional(DESCRIPTION): cv.string,
71  vol.Optional(PROJECT_NAME, default="inbox"): vol.All(cv.string, vol.Lower),
72  vol.Optional(SECTION_NAME): vol.All(cv.string, vol.Lower),
73  vol.Optional(LABELS): cv.ensure_list_csv,
74  vol.Optional(ASSIGNEE): cv.string,
75  vol.Optional(PRIORITY): vol.All(vol.Coerce(int), vol.Range(min=1, max=4)),
76  vol.Exclusive(DUE_DATE_STRING, "due_date"): cv.string,
77  vol.Optional(DUE_DATE_LANG): vol.All(cv.string, vol.In(DUE_DATE_VALID_LANGS)),
78  vol.Exclusive(DUE_DATE, "due_date"): cv.string,
79  vol.Exclusive(REMINDER_DATE_STRING, "reminder_date"): cv.string,
80  vol.Optional(REMINDER_DATE_LANG): vol.All(
81  cv.string, vol.In(DUE_DATE_VALID_LANGS)
82  ),
83  vol.Exclusive(REMINDER_DATE, "reminder_date"): cv.string,
84  }
85 )
86 
87 PLATFORM_SCHEMA = CALENDAR_PLATFORM_SCHEMA.extend(
88  {
89  vol.Required(CONF_TOKEN): cv.string,
90  vol.Optional(CONF_EXTRA_PROJECTS, default=[]): vol.All(
91  cv.ensure_list,
92  vol.Schema(
93  [
94  vol.Schema(
95  {
96  vol.Required(CONF_NAME): cv.string,
97  vol.Optional(CONF_PROJECT_DUE_DATE): vol.Coerce(int),
98  vol.Optional(CONF_PROJECT_WHITELIST, default=[]): vol.All(
99  cv.ensure_list, [vol.All(cv.string, vol.Lower)]
100  ),
101  vol.Optional(
102  CONF_PROJECT_LABEL_WHITELIST, default=[]
103  ): vol.All(cv.ensure_list, [vol.All(cv.string)]),
104  }
105  )
106  ]
107  ),
108  ),
109  }
110 )
111 
112 SCAN_INTERVAL = timedelta(minutes=1)
113 
114 
116  hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
117 ) -> None:
118  """Set up the Todoist calendar platform config entry."""
119  coordinator = hass.data[DOMAIN][entry.entry_id]
120  projects = await coordinator.async_get_projects()
121  labels = await coordinator.async_get_labels()
122 
123  entities = []
124  for project in projects:
125  project_data: ProjectData = {CONF_NAME: project.name, CONF_ID: project.id}
126  entities.append(TodoistProjectEntity(coordinator, project_data, labels))
127 
128  async_add_entities(entities)
129  async_register_services(hass, coordinator)
130 
131 
133  hass: HomeAssistant,
134  config: ConfigType,
135  async_add_entities: AddEntitiesCallback,
136  discovery_info: DiscoveryInfoType | None = None,
137 ) -> None:
138  """Set up the Todoist platform."""
139  token = config[CONF_TOKEN]
140 
141  # Look up IDs based on (lowercase) names.
142  project_id_lookup = {}
143 
144  api = TodoistAPIAsync(token)
145  coordinator = TodoistCoordinator(hass, _LOGGER, None, SCAN_INTERVAL, api, token)
146  await coordinator.async_refresh()
147 
148  async def _shutdown_coordinator(_: Event) -> None:
149  await coordinator.async_shutdown()
150 
151  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown_coordinator)
152 
153  # Setup devices:
154  # Grab all projects.
155  projects = await api.get_projects()
156 
157  # Grab all labels
158  labels = await api.get_labels()
159 
160  # Add all Todoist-defined projects.
161  project_devices = []
162  for project in projects:
163  # Project is an object, not a dict!
164  # Because of that, we convert what we need to a dict.
165  project_data: ProjectData = {CONF_NAME: project.name, CONF_ID: project.id}
166  project_devices.append(TodoistProjectEntity(coordinator, project_data, labels))
167  # Cache the names so we can easily look up name->ID.
168  project_id_lookup[project.name.lower()] = project.id
169 
170  # Check config for more projects.
171  extra_projects: list[CustomProject] = config[CONF_EXTRA_PROJECTS]
172  for extra_project in extra_projects:
173  # Special filter: By date
174  project_due_date = extra_project.get(CONF_PROJECT_DUE_DATE)
175 
176  # Special filter: By label
177  project_label_filter = extra_project[CONF_PROJECT_LABEL_WHITELIST]
178 
179  # Special filter: By name
180  # Names must be converted into IDs.
181  project_name_filter = extra_project[CONF_PROJECT_WHITELIST]
182  project_id_filter: list[str] | None = None
183  if project_name_filter is not None:
184  project_id_filter = [
185  project_id_lookup[project_name.lower()]
186  for project_name in project_name_filter
187  ]
188 
189  # Create the custom project and add it to the devices array.
190  project_devices.append(
192  coordinator,
193  {"id": None, "name": extra_project["name"]},
194  labels,
195  due_date_days=project_due_date,
196  whitelisted_labels=project_label_filter,
197  whitelisted_projects=project_id_filter,
198  )
199  )
200 
201  async_add_entities(project_devices, update_before_add=True)
202 
203  async_register_services(hass, coordinator)
204 
205 
206 def async_register_services( # noqa: C901
207  hass: HomeAssistant, coordinator: TodoistCoordinator
208 ) -> None:
209  """Register services."""
210 
211  if hass.services.has_service(DOMAIN, SERVICE_NEW_TASK):
212  return
213 
214  session = async_get_clientsession(hass)
215 
216  async def handle_new_task(call: ServiceCall) -> None: # noqa: C901
217  """Call when a user creates a new Todoist Task from Home Assistant."""
218  project_name = call.data[PROJECT_NAME]
219  projects = await coordinator.async_get_projects()
220  project_id: str | None = None
221  for project in projects:
222  if project_name == project.name.lower():
223  project_id = project.id
224  break
225  if project_id is None:
227  translation_domain=DOMAIN,
228  translation_key="project_invalid",
229  translation_placeholders={
230  "project": project_name,
231  },
232  )
233 
234  # Optional section within project
235  section_id: str | None = None
236  if SECTION_NAME in call.data:
237  section_name = call.data[SECTION_NAME]
238  sections = await coordinator.async_get_sections(project_id)
239  for section in sections:
240  if section_name == section.name.lower():
241  section_id = section.id
242  break
243  if section_id is None:
245  translation_domain=DOMAIN,
246  translation_key="section_invalid",
247  translation_placeholders={
248  "section": section_name,
249  "project": project_name,
250  },
251  )
252 
253  # Create the task
254  content = call.data[CONTENT]
255  data: dict[str, Any] = {"project_id": project_id}
256 
257  if description := call.data.get(DESCRIPTION):
258  data["description"] = description
259 
260  if section_id is not None:
261  data["section_id"] = section_id
262 
263  if task_labels := call.data.get(LABELS):
264  data["labels"] = task_labels
265 
266  if ASSIGNEE in call.data:
267  collaborators = await coordinator.api.get_collaborators(project_id)
268  collaborator_id_lookup = {
269  collab.name.lower(): collab.id for collab in collaborators
270  }
271  task_assignee = call.data[ASSIGNEE].lower()
272  if task_assignee in collaborator_id_lookup:
273  data["assignee_id"] = collaborator_id_lookup[task_assignee]
274  else:
275  raise ValueError(
276  f"User is not part of the shared project. user: {task_assignee}"
277  )
278 
279  if PRIORITY in call.data:
280  data["priority"] = call.data[PRIORITY]
281 
282  if DUE_DATE_STRING in call.data:
283  data["due_string"] = call.data[DUE_DATE_STRING]
284 
285  if DUE_DATE_LANG in call.data:
286  data["due_lang"] = call.data[DUE_DATE_LANG]
287 
288  if DUE_DATE in call.data:
289  due_date = dt_util.parse_datetime(call.data[DUE_DATE])
290  if due_date is None:
291  due = dt_util.parse_date(call.data[DUE_DATE])
292  if due is None:
293  raise ValueError(f"Invalid due_date: {call.data[DUE_DATE]}")
294  due_date = datetime(due.year, due.month, due.day)
295  # Format it in the manner Todoist expects
296  due_date = dt_util.as_utc(due_date)
297  date_format = "%Y-%m-%dT%H:%M:%S"
298  data["due_datetime"] = datetime.strftime(due_date, date_format)
299 
300  api_task = await coordinator.api.add_task(content, **data)
301 
302  # @NOTE: The rest-api doesn't support reminders, this works manually using
303  # the sync api, in order to keep functional parity with the component.
304  # https://developer.todoist.com/sync/v9/#reminders
305  sync_url = get_sync_url("sync")
306  _reminder_due: dict = {}
307  if REMINDER_DATE_STRING in call.data:
308  _reminder_due["string"] = call.data[REMINDER_DATE_STRING]
309 
310  if REMINDER_DATE_LANG in call.data:
311  _reminder_due["lang"] = call.data[REMINDER_DATE_LANG]
312 
313  if REMINDER_DATE in call.data:
314  due_date = dt_util.parse_datetime(call.data[REMINDER_DATE])
315  if due_date is None:
316  due = dt_util.parse_date(call.data[REMINDER_DATE])
317  if due is None:
318  raise ValueError(
319  f"Invalid reminder_date: {call.data[REMINDER_DATE]}"
320  )
321  due_date = datetime(due.year, due.month, due.day)
322  # Format it in the manner Todoist expects
323  due_date = dt_util.as_utc(due_date)
324  date_format = "%Y-%m-%dT%H:%M:%S"
325  _reminder_due["date"] = datetime.strftime(due_date, date_format)
326 
327  async def add_reminder(reminder_due: dict):
328  reminder_data = {
329  "commands": [
330  {
331  "type": "reminder_add",
332  "temp_id": str(uuid.uuid1()),
333  "uuid": str(uuid.uuid1()),
334  "args": {
335  "item_id": api_task.id,
336  "type": "absolute",
337  "due": reminder_due,
338  },
339  }
340  ]
341  }
342  headers = create_headers(token=coordinator.token, with_content=True)
343  return await session.post(sync_url, headers=headers, json=reminder_data)
344 
345  if _reminder_due:
346  await add_reminder(_reminder_due)
347 
348  _LOGGER.debug("Created Todoist task: %s", call.data[CONTENT])
349 
350  hass.services.async_register(
351  DOMAIN, SERVICE_NEW_TASK, handle_new_task, schema=NEW_TASK_SERVICE_SCHEMA
352  )
353 
354 
355 class TodoistProjectEntity(CoordinatorEntity[TodoistCoordinator], CalendarEntity):
356  """A device for getting the next Task from a Todoist Project."""
357 
358  def __init__(
359  self,
360  coordinator: TodoistCoordinator,
361  data: ProjectData,
362  labels: list[Label],
363  due_date_days: int | None = None,
364  whitelisted_labels: list[str] | None = None,
365  whitelisted_projects: list[str] | None = None,
366  ) -> None:
367  """Create the Todoist Calendar Entity."""
368  super().__init__(coordinator=coordinator)
370  data,
371  labels,
372  coordinator,
373  due_date_days=due_date_days,
374  whitelisted_labels=whitelisted_labels,
375  whitelisted_projects=whitelisted_projects,
376  )
377  self._cal_data: CalData = {}
378  self._name_name = data[CONF_NAME]
379  self._attr_unique_id_attr_unique_id = (
380  str(data[CONF_ID]) if data.get(CONF_ID) is not None else None
381  )
382 
383  @callback
384  def _handle_coordinator_update(self) -> None:
385  """Handle updated data from the coordinator."""
386  self.datadatadata.update()
388 
389  @property
390  def event(self) -> CalendarEvent | None:
391  """Return the next upcoming event."""
392  return self.datadatadata.calendar_event
393 
394  @property
395  def name(self) -> str:
396  """Return the name of the entity."""
397  return self._name_name
398 
399  async def async_update(self) -> None:
400  """Update all Todoist Calendars."""
401  await super().async_update()
402  self.datadatadata.update()
403 
404  async def async_get_events(
405  self,
406  hass: HomeAssistant,
407  start_date: datetime,
408  end_date: datetime,
409  ) -> list[CalendarEvent]:
410  """Get all events in a specific time frame."""
411  return await self.datadatadata.async_get_events(start_date, end_date)
412 
413  @property
414  def extra_state_attributes(self) -> dict[str, Any] | None:
415  """Return the device state attributes."""
416  if self.datadatadata.event is None:
417  # No tasks, we don't REALLY need to show anything.
418  return None
419 
420  return {
421  DUE_TODAY: self.datadatadata.event[DUE_TODAY],
422  OVERDUE: self.datadatadata.event[OVERDUE],
423  ALL_TASKS: [task[SUMMARY] for task in self.datadatadata.all_project_tasks],
424  PRIORITY: self.datadatadata.event[PRIORITY],
425  LABELS: self.datadatadata.event[LABELS],
426  }
427 
428 
430  """Class used by the Task Entity service object to hold all Todoist Tasks.
431 
432  This is analogous to the GoogleCalendarData found in the Google Calendar
433  component.
434 
435  Takes an object with a 'name' field and optionally an 'id' field (either
436  user-defined or from the Todoist API), a Todoist API token, and an optional
437  integer specifying the latest number of days from now a task can be due (7
438  means everything due in the next week, 0 means today, etc.).
439 
440  This object has an exposed 'event' property (used by the Calendar platform
441  to determine the next calendar event) and an exposed 'update' method (used
442  by the Calendar platform to poll for new calendar events).
443 
444  The 'event' is a representation of a Todoist Task, with defined parameters
445  of 'due_today' (is the task due today?), 'all_day' (does the task have a
446  due date?), 'task_labels' (all labels assigned to the task), 'message'
447  (the content of the task, e.g. 'Fetch Mail'), 'description' (a URL pointing
448  to the task on the Todoist website), 'end_time' (what time the event is
449  due), 'start_time' (what time this event was last updated), 'overdue' (is
450  the task past its due date?), 'priority' (1-4, how important the task is,
451  with 4 being the most important), and 'all_tasks' (all tasks in this
452  project, sorted by how important they are).
453 
454  'offset_reached', 'location', and 'friendly_name' are defined by the
455  platform itself, but are not used by this component at all.
456 
457  The 'update' method polls the Todoist API for new projects/tasks, as well
458  as any updates to current projects/tasks. This occurs every SCAN_INTERVAL minutes.
459  """
460 
461  def __init__(
462  self,
463  project_data: ProjectData,
464  labels: list[Label],
465  coordinator: TodoistCoordinator,
466  due_date_days: int | None = None,
467  whitelisted_labels: list[str] | None = None,
468  whitelisted_projects: list[str] | None = None,
469  ) -> None:
470  """Initialize a Todoist Project."""
471  self.eventevent: TodoistEvent | None = None
472 
473  self._coordinator_coordinator = coordinator
474  self._name_name = project_data[CONF_NAME]
475  # If no ID is defined, this is a custom project.
476  self._id_id = project_data.get(CONF_ID)
477 
478  # All labels the user has defined, for easy lookup.
479  self._labels_labels = labels
480  # Not tracked: order, indent, comment_count.
481 
482  self.all_project_tasks: list[TodoistEvent] = []
483 
484  # The days a task can be due (for making lists of everything
485  # due today, or everything due in the next week, for example).
486  self._due_date_days_due_date_days: timedelta | None = None
487  if due_date_days is not None:
488  self._due_date_days_due_date_days = timedelta(days=due_date_days)
489 
490  # Only tasks with one of these labels will be included.
491  self._label_whitelist_label_whitelist: list[str] = []
492  if whitelisted_labels is not None:
493  self._label_whitelist_label_whitelist = whitelisted_labels
494 
495  # This project includes only projects with these names.
496  self._project_id_whitelist_project_id_whitelist: list[str] = []
497  if whitelisted_projects is not None:
498  self._project_id_whitelist_project_id_whitelist = whitelisted_projects
499 
500  @property
501  def calendar_event(self) -> CalendarEvent | None:
502  """Return the next upcoming calendar event."""
503  if not self.eventevent:
504  return None
505 
506  start = self.eventevent[START]
507  if self.eventevent.get(ALL_DAY) or self.eventevent[END] is None:
508  return CalendarEvent(
509  summary=self.eventevent[SUMMARY],
510  start=start.date(),
511  end=start.date() + timedelta(days=1),
512  )
513 
514  return CalendarEvent(
515  summary=self.eventevent[SUMMARY], start=start, end=self.eventevent[END]
516  )
517 
518  def create_todoist_task(self, data: Task):
519  """Create a dictionary based on a Task passed from the Todoist API.
520 
521  Will return 'None' if the task is to be filtered out.
522  """
523  task: TodoistEvent = {
524  ALL_DAY: False,
525  COMPLETED: data.is_completed,
526  DESCRIPTION: f"https://todoist.com/showTask?id={data.id}",
527  DUE_TODAY: False,
528  END: None,
529  LABELS: [],
530  OVERDUE: False,
531  PRIORITY: data.priority,
532  START: dt_util.now(),
533  SUMMARY: data.content,
534  }
535 
536  if (
537  self._project_id_whitelist_project_id_whitelist
538  and data.project_id not in self._project_id_whitelist_project_id_whitelist
539  ):
540  # Project isn't in `include_projects` filter.
541  return None
542 
543  # All task Labels (optional parameter).
544  task[LABELS] = [
545  label.name for label in self._labels_labels if label.name in data.labels
546  ]
547  if self._label_whitelist_label_whitelist and (
548  not any(label in task[LABELS] for label in self._label_whitelist_label_whitelist)
549  ):
550  # We're not on the whitelist, return invalid task.
551  return None
552 
553  # Due dates (optional parameter).
554  # The due date is the END date -- the task cannot be completed
555  # past this time.
556  # That means that the START date is the earliest time one can
557  # complete the task.
558  # Generally speaking, that means right now.
559  if data.due is not None:
560  end = dt_util.parse_datetime(
561  data.due.datetime if data.due.datetime else data.due.date
562  )
563  task[END] = dt_util.as_local(end) if end is not None else end
564  if task[END] is not None:
565  if self._due_date_days_due_date_days is not None and (
566  task[END] > dt_util.now() + self._due_date_days_due_date_days
567  ):
568  # This task is out of range of our due date;
569  # it shouldn't be counted.
570  return None
571 
572  task[DUE_TODAY] = task[END].date() == dt_util.now().date()
573 
574  # Special case: Task is overdue.
575  if task[END] <= task[START]:
576  task[OVERDUE] = True
577  # Set end time to the current time plus 1 hour.
578  # We're pretty much guaranteed to update within that 1 hour,
579  # so it should be fine.
580  task[END] = task[START] + timedelta(hours=1)
581  else:
582  task[OVERDUE] = False
583  else:
584  # If we ask for everything due before a certain date, don't count
585  # things which have no due dates.
586  if self._due_date_days_due_date_days is not None:
587  return None
588 
589  # Define values for tasks without due dates
590  task[END] = None
591  task[ALL_DAY] = True
592  task[DUE_TODAY] = False
593  task[OVERDUE] = False
594 
595  # Not tracked: id, comments, project_id order, indent, recurring.
596  return task
597 
598  @staticmethod
599  def select_best_task(project_tasks: list[TodoistEvent]) -> TodoistEvent:
600  """Search through a list of events for the "best" event to select.
601 
602  The "best" event is determined by the following criteria:
603  * A proposed event must not be completed
604  * A proposed event must have an end date (otherwise we go with
605  the event at index 0, selected above)
606  * A proposed event must be on the same day or earlier as our
607  current event
608  * If a proposed event is an earlier day than what we have so
609  far, select it
610  * If a proposed event is on the same day as our current event
611  and the proposed event has a higher priority than our current
612  event, select it
613  * If a proposed event is on the same day as our current event,
614  has the same priority as our current event, but is due earlier
615  in the day, select it
616  """
617  # Start at the end of the list, so if tasks don't have a due date
618  # the newest ones are the most important.
619 
620  event = project_tasks[-1]
621 
622  for proposed_event in project_tasks:
623  if event == proposed_event:
624  continue
625 
626  if proposed_event[COMPLETED]:
627  # Event is complete!
628  continue
629 
630  if proposed_event[END] is None:
631  # No end time:
632  if event[END] is None and (proposed_event[PRIORITY] < event[PRIORITY]):
633  # They also have no end time,
634  # but we have a higher priority.
635  event = proposed_event
636  continue
637 
638  if event[END] is None:
639  # We have an end time, they do not.
640  event = proposed_event
641  continue
642 
643  if proposed_event[END].date() > event[END].date():
644  # Event is too late.
645  continue
646 
647  if proposed_event[END].date() < event[END].date():
648  # Event is earlier than current, select it.
649  event = proposed_event
650  continue
651 
652  if proposed_event[PRIORITY] > event[PRIORITY]:
653  # Proposed event has a higher priority.
654  event = proposed_event
655  continue
656 
657  if proposed_event[PRIORITY] == event[PRIORITY] and (
658  event[END] is not None and proposed_event[END] < event[END]
659  ):
660  event = proposed_event
661  continue
662  return event
663 
664  async def async_get_events(
665  self, start_date: datetime, end_date: datetime
666  ) -> list[CalendarEvent]:
667  """Get all tasks in a specific time frame."""
668  tasks = self._coordinator_coordinator.data
669  if self._id_id is None:
670  project_task_data = [
671  task for task in tasks if self.create_todoist_taskcreate_todoist_task(task) is not None
672  ]
673  else:
674  project_task_data = [task for task in tasks if task.project_id == self._id_id]
675 
676  events = []
677  for task in project_task_data:
678  if task.due is None:
679  continue
680  start = get_start(task.due)
681  if start is None:
682  continue
683  event = CalendarEvent(
684  summary=task.content,
685  start=start,
686  end=start + timedelta(days=1),
687  )
688  if event.start_datetime_local >= end_date:
689  continue
690  if event.end_datetime_local < start_date:
691  continue
692  events.append(event)
693  return events
694 
695  def update(self) -> None:
696  """Get the latest data."""
697  tasks = self._coordinator_coordinator.data
698  if self._id_id is None:
699  project_task_data = [
700  task
701  for task in tasks
702  if not self._project_id_whitelist_project_id_whitelist
703  or task.project_id in self._project_id_whitelist_project_id_whitelist
704  ]
705  else:
706  project_task_data = [task for task in tasks if task.project_id == self._id_id]
707 
708  # If we have no data, we can just return right away.
709  if not project_task_data:
710  _LOGGER.debug("No data for %s", self._name_name)
711  self.eventevent = None
712  return
713 
714  # Keep an updated list of all tasks in this project.
715  project_tasks = []
716  for task in project_task_data:
717  todoist_task = self.create_todoist_taskcreate_todoist_task(task)
718  if todoist_task is not None:
719  # A None task means it is invalid for this project
720  project_tasks.append(todoist_task)
721 
722  if not project_tasks:
723  # We had no valid tasks
724  _LOGGER.debug("No valid tasks for %s", self._name_name)
725  self.eventevent = None
726  return
727 
728  # Make sure the task collection is reset to prevent an
729  # infinite collection repeating the same tasks
730  self.all_project_tasks.clear()
731 
732  # Organize the best tasks (so users can see all the tasks
733  # they have, organized)
734  while project_tasks:
735  best_task = self.select_best_taskselect_best_task(project_tasks)
736  _LOGGER.debug("Found Todoist Task: %s", best_task[SUMMARY])
737  project_tasks.remove(best_task)
738  self.all_project_tasks.append(best_task)
739 
740  event = self.all_project_tasks[0]
741  if event is None or event[START] is None:
742  _LOGGER.debug("No valid event or event start for %s", self._name_name)
743  self.eventevent = None
744  return
745  self.eventevent = event
746  _LOGGER.debug("Updated %s", self._name_name)
747 
748 
749 def get_start(due: Due) -> datetime | date | None:
750  """Return the task due date as a start date or date time."""
751  if due.datetime:
752  start = dt_util.parse_datetime(due.datetime)
753  if not start:
754  return None
755  return dt_util.as_local(start)
756  if due.date:
757  return dt_util.parse_date(due.date)
758  return None
None __init__(self, ProjectData project_data, list[Label] labels, TodoistCoordinator coordinator, int|None due_date_days=None, list[str]|None whitelisted_labels=None, list[str]|None whitelisted_projects=None)
Definition: calendar.py:469
TodoistEvent select_best_task(list[TodoistEvent] project_tasks)
Definition: calendar.py:599
list[CalendarEvent] async_get_events(self, datetime start_date, datetime end_date)
Definition: calendar.py:666
list[CalendarEvent] async_get_events(self, HomeAssistant hass, datetime start_date, datetime end_date)
Definition: calendar.py:409
None __init__(self, TodoistCoordinator coordinator, ProjectData data, list[Label] labels, int|None due_date_days=None, list[str]|None whitelisted_labels=None, list[str]|None whitelisted_projects=None)
Definition: calendar.py:366
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
datetime|date|None get_start(Due due)
Definition: calendar.py:749
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: calendar.py:137
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: calendar.py:117
None async_register_services(HomeAssistant hass, TodoistCoordinator coordinator)
Definition: calendar.py:208
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)