Home Assistant Unofficial Reference 2024.12.1
todo.py
Go to the documentation of this file.
1 """A todo platform for Todoist."""
2 
3 import asyncio
4 import datetime
5 from typing import Any, cast
6 
7 from todoist_api_python.models import Task
8 
10  TodoItem,
11  TodoItemStatus,
12  TodoListEntity,
13  TodoListEntityFeature,
14 )
15 from homeassistant.config_entries import ConfigEntry
16 from homeassistant.core import HomeAssistant, callback
17 from homeassistant.helpers.entity_platform import AddEntitiesCallback
18 from homeassistant.helpers.update_coordinator import CoordinatorEntity
19 from homeassistant.util import dt as dt_util
20 
21 from .const import DOMAIN
22 from .coordinator import TodoistCoordinator
23 
24 
26  hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
27 ) -> None:
28  """Set up the Todoist todo platform config entry."""
29  coordinator: TodoistCoordinator = hass.data[DOMAIN][entry.entry_id]
30  projects = await coordinator.async_get_projects()
32  TodoistTodoListEntity(coordinator, entry.entry_id, project.id, project.name)
33  for project in projects
34  )
35 
36 
37 def _task_api_data(item: TodoItem, api_data: Task | None = None) -> dict[str, Any]:
38  """Convert a TodoItem to the set of add or update arguments."""
39  item_data: dict[str, Any] = {
40  "content": item.summary,
41  # Description needs to be empty string to be cleared
42  "description": item.description or "",
43  }
44  if due := item.due:
45  if isinstance(due, datetime.datetime):
46  item_data["due_datetime"] = due.isoformat()
47  else:
48  item_data["due_date"] = due.isoformat()
49  # In order to not lose any recurrence metadata for the task, we need to
50  # ensure that we send the `due_string` param if the task has it set.
51  # NOTE: It's ok to send stale data for non-recurring tasks. Any provided
52  # date/datetime will override this string.
53  if api_data and api_data.due:
54  item_data["due_string"] = api_data.due.string
55  else:
56  # Special flag "no date" clears the due date/datetime.
57  # See https://developer.todoist.com/rest/v2/#update-a-task for more.
58  item_data["due_string"] = "no date"
59  return item_data
60 
61 
62 class TodoistTodoListEntity(CoordinatorEntity[TodoistCoordinator], TodoListEntity):
63  """A Todoist TodoListEntity."""
64 
65  _attr_supported_features = (
66  TodoListEntityFeature.CREATE_TODO_ITEM
67  | TodoListEntityFeature.UPDATE_TODO_ITEM
68  | TodoListEntityFeature.DELETE_TODO_ITEM
69  | TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
70  | TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM
71  | TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
72  )
73 
74  def __init__(
75  self,
76  coordinator: TodoistCoordinator,
77  config_entry_id: str,
78  project_id: str,
79  project_name: str,
80  ) -> None:
81  """Initialize TodoistTodoListEntity."""
82  super().__init__(coordinator=coordinator)
83  self._project_id_project_id = project_id
84  self._attr_unique_id_attr_unique_id = f"{config_entry_id}-{project_id}"
85  self._attr_name_attr_name = project_name
86 
87  @callback
88  def _handle_coordinator_update(self) -> None:
89  """Handle updated data from the coordinator."""
90  if self.coordinator.data is None:
91  self._attr_todo_items_attr_todo_items = None
92  else:
93  items = []
94  for task in self.coordinator.data:
95  if task.project_id != self._project_id_project_id:
96  continue
97  if task.parent_id is not None:
98  # Filter out sub-tasks until they are supported by the UI.
99  continue
100  if task.is_completed:
101  status = TodoItemStatus.COMPLETED
102  else:
103  status = TodoItemStatus.NEEDS_ACTION
104  due: datetime.date | datetime.datetime | None = None
105  if task_due := task.due:
106  if task_due.datetime:
107  due = dt_util.as_local(
108  datetime.datetime.fromisoformat(task_due.datetime)
109  )
110  elif task_due.date:
111  due = datetime.date.fromisoformat(task_due.date)
112  items.append(
113  TodoItem(
114  summary=task.content,
115  uid=task.id,
116  status=status,
117  due=due,
118  description=task.description or None, # Don't use empty string
119  )
120  )
121  self._attr_todo_items_attr_todo_items = items
123 
124  async def async_create_todo_item(self, item: TodoItem) -> None:
125  """Create a To-do item."""
126  if item.status != TodoItemStatus.NEEDS_ACTION:
127  raise ValueError("Only active tasks may be created.")
128  await self.coordinator.api.add_task(
129  **_task_api_data(item),
130  project_id=self._project_id_project_id,
131  )
132  await self.coordinator.async_refresh()
133 
134  async def async_update_todo_item(self, item: TodoItem) -> None:
135  """Update a To-do item."""
136  uid: str = cast(str, item.uid)
137  api_data = next((d for d in self.coordinator.data if d.id == uid), None)
138  if update_data := _task_api_data(item, api_data):
139  await self.coordinator.api.update_task(task_id=uid, **update_data)
140  if item.status is not None:
141  # Only update status if changed
142  for existing_item in self._attr_todo_items_attr_todo_items or ():
143  if existing_item.uid != item.uid:
144  continue
145 
146  if item.status != existing_item.status:
147  if item.status == TodoItemStatus.COMPLETED:
148  await self.coordinator.api.close_task(task_id=uid)
149  else:
150  await self.coordinator.api.reopen_task(task_id=uid)
151  await self.coordinator.async_refresh()
152 
153  async def async_delete_todo_items(self, uids: list[str]) -> None:
154  """Delete a To-do item."""
155  await asyncio.gather(
156  *[self.coordinator.api.delete_task(task_id=uid) for uid in uids]
157  )
158  await self.coordinator.async_refresh()
159 
160  async def async_added_to_hass(self) -> None:
161  """When entity is added to hass update state from existing coordinator data."""
162  await super().async_added_to_hass()
163  self._handle_coordinator_update_handle_coordinator_update()
None __init__(self, TodoistCoordinator coordinator, str config_entry_id, str project_id, str project_name)
Definition: todo.py:80
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: todo.py:27
dict[str, Any] _task_api_data(TodoItem item, Task|None api_data=None)
Definition: todo.py:37