Home Assistant Unofficial Reference 2024.12.1
todo.py
Go to the documentation of this file.
1 """CalDAV todo platform."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from datetime import date, datetime, timedelta
7 from functools import partial
8 import logging
9 from typing import Any, cast
10 
11 import caldav
12 from caldav.lib.error import DAVError, NotFoundError
13 import requests
14 
16  TodoItem,
17  TodoItemStatus,
18  TodoListEntity,
19  TodoListEntityFeature,
20 )
21 from homeassistant.core import HomeAssistant
22 from homeassistant.exceptions import HomeAssistantError
23 from homeassistant.helpers.entity_platform import AddEntitiesCallback
24 from homeassistant.util import dt as dt_util
25 
26 from . import CalDavConfigEntry
27 from .api import async_get_calendars, get_attr_value
28 
29 _LOGGER = logging.getLogger(__name__)
30 
31 SCAN_INTERVAL = timedelta(minutes=15)
32 
33 SUPPORTED_COMPONENT = "VTODO"
34 TODO_STATUS_MAP = {
35  "NEEDS-ACTION": TodoItemStatus.NEEDS_ACTION,
36  "IN-PROCESS": TodoItemStatus.NEEDS_ACTION,
37  "COMPLETED": TodoItemStatus.COMPLETED,
38  "CANCELLED": TodoItemStatus.COMPLETED,
39 }
40 TODO_STATUS_MAP_INV: dict[TodoItemStatus, str] = {
41  TodoItemStatus.NEEDS_ACTION: "NEEDS-ACTION",
42  TodoItemStatus.COMPLETED: "COMPLETED",
43 }
44 
45 
47  hass: HomeAssistant,
48  entry: CalDavConfigEntry,
49  async_add_entities: AddEntitiesCallback,
50 ) -> None:
51  """Set up the CalDav todo platform for a config entry."""
52  calendars = await async_get_calendars(hass, entry.runtime_data, SUPPORTED_COMPONENT)
54  (
56  calendar,
57  entry.entry_id,
58  )
59  for calendar in calendars
60  ),
61  True,
62  )
63 
64 
65 def _todo_item(resource: caldav.CalendarObjectResource) -> TodoItem | None:
66  """Convert a caldav Todo into a TodoItem."""
67  if (
68  not hasattr(resource.instance, "vtodo")
69  or not (todo := resource.instance.vtodo)
70  or (uid := get_attr_value(todo, "uid")) is None
71  or (summary := get_attr_value(todo, "summary")) is None
72  ):
73  return None
74  due: date | datetime | None = None
75  if due_value := get_attr_value(todo, "due"):
76  if isinstance(due_value, datetime):
77  due = dt_util.as_local(due_value)
78  elif isinstance(due_value, date):
79  due = due_value
80  return TodoItem(
81  uid=uid,
82  summary=summary,
83  status=TODO_STATUS_MAP.get(
84  get_attr_value(todo, "status") or "",
85  TodoItemStatus.NEEDS_ACTION,
86  ),
87  due=due,
88  description=get_attr_value(todo, "description"),
89  )
90 
91 
93  """CalDAV To-do list entity."""
94 
95  _attr_has_entity_name = True
96  _attr_supported_features = (
97  TodoListEntityFeature.CREATE_TODO_ITEM
98  | TodoListEntityFeature.UPDATE_TODO_ITEM
99  | TodoListEntityFeature.DELETE_TODO_ITEM
100  | TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
101  | TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM
102  | TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
103  )
104 
105  def __init__(self, calendar: caldav.Calendar, config_entry_id: str) -> None:
106  """Initialize WebDavTodoListEntity."""
107  self._calendar_calendar = calendar
108  self._attr_name_attr_name = (calendar.name or "Unknown").capitalize()
109  self._attr_unique_id_attr_unique_id = f"{config_entry_id}-{calendar.id}"
110 
111  async def async_update(self) -> None:
112  """Update To-do list entity state."""
113  results = await self.hasshass.async_add_executor_job(
114  partial(
115  self._calendar_calendar.search,
116  todo=True,
117  include_completed=True,
118  )
119  )
120  self._attr_todo_items_attr_todo_items = [
121  todo_item
122  for resource in results
123  if (todo_item := _todo_item(resource)) is not None
124  ]
125 
126  async def async_create_todo_item(self, item: TodoItem) -> None:
127  """Add an item to the To-do list."""
128  item_data: dict[str, Any] = {}
129  if summary := item.summary:
130  item_data["summary"] = summary
131  if status := item.status:
132  item_data["status"] = TODO_STATUS_MAP_INV.get(status, "NEEDS-ACTION")
133  if due := item.due:
134  item_data["due"] = due
135  if description := item.description:
136  item_data["description"] = description
137  try:
138  await self.hasshass.async_add_executor_job(
139  partial(self._calendar_calendar.save_todo, **item_data),
140  )
141  except (requests.ConnectionError, DAVError) as err:
142  raise HomeAssistantError(f"CalDAV save error: {err}") from err
143 
144  async def async_update_todo_item(self, item: TodoItem) -> None:
145  """Update a To-do item."""
146  uid: str = cast(str, item.uid)
147  try:
148  todo = await self.hasshass.async_add_executor_job(
149  self._calendar_calendar.todo_by_uid, uid
150  )
151  except NotFoundError as err:
152  raise HomeAssistantError(f"Could not find To-do item {uid}") from err
153  except (requests.ConnectionError, DAVError) as err:
154  raise HomeAssistantError(f"CalDAV lookup error: {err}") from err
155  vtodo = todo.icalendar_component # type: ignore[attr-defined]
156  vtodo["SUMMARY"] = item.summary or ""
157  if status := item.status:
158  vtodo["STATUS"] = TODO_STATUS_MAP_INV.get(status, "NEEDS-ACTION")
159  if due := item.due:
160  todo.set_due(due) # type: ignore[attr-defined]
161  else:
162  vtodo.pop("DUE", None)
163  if description := item.description:
164  vtodo["DESCRIPTION"] = description
165  else:
166  vtodo.pop("DESCRIPTION", None)
167  try:
168  await self.hasshass.async_add_executor_job(
169  partial(
170  todo.save,
171  no_create=True,
172  obj_type="todo",
173  ),
174  )
175  except (requests.ConnectionError, DAVError) as err:
176  raise HomeAssistantError(f"CalDAV save error: {err}") from err
177 
178  async def async_delete_todo_items(self, uids: list[str]) -> None:
179  """Delete To-do items."""
180  tasks = (
181  self.hasshass.async_add_executor_job(self._calendar_calendar.todo_by_uid, uid)
182  for uid in uids
183  )
184 
185  try:
186  items = await asyncio.gather(*tasks)
187  except NotFoundError as err:
188  raise HomeAssistantError("Could not find To-do item") from err
189  except (requests.ConnectionError, DAVError) as err:
190  raise HomeAssistantError(f"CalDAV lookup error: {err}") from err
191 
192  # Run serially as some CalDAV servers do not support concurrent modifications
193  for item in items:
194  try:
195  await self.hasshass.async_add_executor_job(item.delete)
196  except (requests.ConnectionError, DAVError) as err:
197  raise HomeAssistantError(f"CalDAV delete error: {err}") from err
None async_delete_todo_items(self, list[str] uids)
Definition: todo.py:178
None __init__(self, caldav.Calendar calendar, str config_entry_id)
Definition: todo.py:105
list[caldav.Calendar] async_get_calendars(HomeAssistant hass, caldav.DAVClient client, str component)
Definition: api.py:10
str|None get_attr_value(caldav.CalendarObjectResource obj, str attribute)
Definition: api.py:23
None async_setup_entry(HomeAssistant hass, CalDavConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: todo.py:50
TodoItem|None _todo_item(caldav.CalendarObjectResource resource)
Definition: todo.py:65