Home Assistant Unofficial Reference 2024.12.1
todo.py
Go to the documentation of this file.
1 """A Local To-do todo platform."""
2 
3 import asyncio
4 import datetime
5 import logging
6 
7 from ical.calendar import Calendar
8 from ical.calendar_stream import IcsCalendarStream
9 from ical.store import TodoStore
10 from ical.todo import Todo, TodoStatus
11 
13  TodoItem,
14  TodoItemStatus,
15  TodoListEntity,
16  TodoListEntityFeature,
17 )
18 from homeassistant.core import HomeAssistant
19 from homeassistant.exceptions import HomeAssistantError
20 from homeassistant.helpers.entity_platform import AddEntitiesCallback
21 from homeassistant.setup import SetupPhases, async_pause_setup
22 from homeassistant.util import dt as dt_util
23 
24 from . import LocalTodoConfigEntry
25 from .const import CONF_TODO_LIST_NAME
26 from .store import LocalTodoListStore
27 
28 _LOGGER = logging.getLogger(__name__)
29 
30 
31 PRODID = "-//homeassistant.io//local_todo 2.0//EN"
32 PRODID_REQUIRES_MIGRATION = "-//homeassistant.io//local_todo 1.0//EN"
33 
34 ICS_TODO_STATUS_MAP = {
35  TodoStatus.IN_PROCESS: TodoItemStatus.NEEDS_ACTION,
36  TodoStatus.NEEDS_ACTION: TodoItemStatus.NEEDS_ACTION,
37  TodoStatus.COMPLETED: TodoItemStatus.COMPLETED,
38  TodoStatus.CANCELLED: TodoItemStatus.COMPLETED,
39 }
40 ICS_TODO_STATUS_MAP_INV = {
41  TodoItemStatus.COMPLETED: TodoStatus.COMPLETED,
42  TodoItemStatus.NEEDS_ACTION: TodoStatus.NEEDS_ACTION,
43 }
44 
45 
46 def _migrate_calendar(calendar: Calendar) -> bool:
47  """Upgrade due dates to rfc5545 format.
48 
49  In rfc5545 due dates are exclusive, however we previously set the due date
50  as inclusive based on what the user set in the UI. A task is considered
51  overdue at midnight at the start of a date so we need to shift the due date
52  to the next day for old calendar versions.
53  """
54  if calendar.prodid is None or calendar.prodid != PRODID_REQUIRES_MIGRATION:
55  return False
56  migrated = False
57  for todo in calendar.todos:
58  if todo.due is None or isinstance(todo.due, datetime.datetime):
59  continue
60  todo.due += datetime.timedelta(days=1)
61  migrated = True
62  return migrated
63 
64 
66  hass: HomeAssistant,
67  config_entry: LocalTodoConfigEntry,
68  async_add_entities: AddEntitiesCallback,
69 ) -> None:
70  """Set up the local_todo todo platform."""
71 
72  store = config_entry.runtime_data
73  ics = await store.async_load()
74 
75  with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES):
76  # calendar_from_ics will dynamically load packages
77  # the first time it is called, so we need to do it
78  # in a separate thread to avoid blocking the event loop
79  calendar: Calendar = await hass.async_add_import_executor_job(
80  IcsCalendarStream.calendar_from_ics, ics
81  )
82  migrated = _migrate_calendar(calendar)
83  calendar.prodid = PRODID
84 
85  name = config_entry.data[CONF_TODO_LIST_NAME]
86  entity = LocalTodoListEntity(store, calendar, name, unique_id=config_entry.entry_id)
87  async_add_entities([entity], True)
88 
89  if migrated:
90  await entity.async_save()
91 
92 
93 def _convert_item(item: TodoItem) -> Todo:
94  """Convert a HomeAssistant TodoItem to an ical Todo."""
95  todo = Todo()
96  if item.uid:
97  todo.uid = item.uid
98  if item.summary:
99  todo.summary = item.summary
100  if item.status:
101  todo.status = ICS_TODO_STATUS_MAP_INV[item.status]
102  todo.due = item.due
103  if todo.due and not isinstance(todo.due, datetime.datetime):
104  todo.due += datetime.timedelta(days=1)
105  todo.description = item.description
106  return todo
107 
108 
110  """A To-do List representation of the Shopping List."""
111 
112  _attr_has_entity_name = True
113  _attr_supported_features = (
114  TodoListEntityFeature.CREATE_TODO_ITEM
115  | TodoListEntityFeature.DELETE_TODO_ITEM
116  | TodoListEntityFeature.UPDATE_TODO_ITEM
117  | TodoListEntityFeature.MOVE_TODO_ITEM
118  | TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM
119  | TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
120  | TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
121  )
122  _attr_should_poll = False
123 
124  def __init__(
125  self,
126  store: LocalTodoListStore,
127  calendar: Calendar,
128  name: str,
129  unique_id: str,
130  ) -> None:
131  """Initialize LocalTodoListEntity."""
132  self._store_store = store
133  self._calendar_calendar = calendar
134  self._calendar_lock_calendar_lock = asyncio.Lock()
135  self._attr_name_attr_name = name.capitalize()
136  self._attr_unique_id_attr_unique_id = unique_id
137 
138  def _new_todo_store(self) -> TodoStore:
139  return TodoStore(self._calendar_calendar, tzinfo=dt_util.get_default_time_zone())
140 
141  async def async_update(self) -> None:
142  """Update entity state based on the local To-do items."""
143  todo_items = []
144  for item in self._calendar_calendar.todos:
145  if (due := item.due) and not isinstance(due, datetime.datetime):
146  due -= datetime.timedelta(days=1)
147  todo_items.append(
148  TodoItem(
149  uid=item.uid,
150  summary=item.summary or "",
151  status=ICS_TODO_STATUS_MAP.get(
152  item.status or TodoStatus.NEEDS_ACTION,
153  TodoItemStatus.NEEDS_ACTION,
154  ),
155  due=due,
156  description=item.description,
157  )
158  )
159  self._attr_todo_items_attr_todo_items = todo_items
160 
161  async def async_create_todo_item(self, item: TodoItem) -> None:
162  """Add an item to the To-do list."""
163  todo = _convert_item(item)
164  async with self._calendar_lock_calendar_lock:
165  todo_store = self._new_todo_store_new_todo_store()
166  await self.hasshass.async_add_executor_job(todo_store.add, todo)
167  await self.async_saveasync_save()
168  await self.async_update_ha_stateasync_update_ha_state(force_refresh=True)
169 
170  async def async_update_todo_item(self, item: TodoItem) -> None:
171  """Update an item to the To-do list."""
172  todo = _convert_item(item)
173  async with self._calendar_lock_calendar_lock:
174  todo_store = self._new_todo_store_new_todo_store()
175  await self.hasshass.async_add_executor_job(todo_store.edit, todo.uid, todo)
176  await self.async_saveasync_save()
177  await self.async_update_ha_stateasync_update_ha_state(force_refresh=True)
178 
179  async def async_delete_todo_items(self, uids: list[str]) -> None:
180  """Delete an item from the To-do list."""
181  store = self._new_todo_store_new_todo_store()
182  async with self._calendar_lock_calendar_lock:
183  for uid in uids:
184  store.delete(uid)
185  await self.async_saveasync_save()
186  await self.async_update_ha_stateasync_update_ha_state(force_refresh=True)
187 
189  self, uid: str, previous_uid: str | None = None
190  ) -> None:
191  """Re-order an item to the To-do list."""
192  if uid == previous_uid:
193  return
194  async with self._calendar_lock_calendar_lock:
195  todos = self._calendar_calendar.todos
196  item_idx: dict[str, int] = {itm.uid: idx for idx, itm in enumerate(todos)}
197  if uid not in item_idx:
198  raise HomeAssistantError(
199  "Item '{uid}' not found in todo list {self.entity_id}"
200  )
201  if previous_uid and previous_uid not in item_idx:
202  raise HomeAssistantError(
203  "Item '{previous_uid}' not found in todo list {self.entity_id}"
204  )
205  dst_idx = item_idx[previous_uid] + 1 if previous_uid else 0
206  src_idx = item_idx[uid]
207  src_item = todos.pop(src_idx)
208  if dst_idx > src_idx:
209  dst_idx -= 1
210  todos.insert(dst_idx, src_item)
211  await self.async_saveasync_save()
212  await self.async_update_ha_stateasync_update_ha_state(force_refresh=True)
213 
214  async def async_save(self) -> None:
215  """Persist the todo list to disk."""
216  content = IcsCalendarStream.calendar_to_ics(self._calendar_calendar)
217  await self._store_store.async_store(content)
None __init__(self, LocalTodoListStore store, Calendar calendar, str name, str unique_id)
Definition: todo.py:130
None async_move_todo_item(self, str uid, str|None previous_uid=None)
Definition: todo.py:190
None async_update_ha_state(self, bool force_refresh=False)
Definition: entity.py:942
None async_setup_entry(HomeAssistant hass, LocalTodoConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: todo.py:69
Todo _convert_item(TodoItem item)
Definition: todo.py:93
bool _migrate_calendar(Calendar calendar)
Definition: todo.py:46
Generator[None] async_pause_setup(core.HomeAssistant hass, SetupPhases phase)
Definition: setup.py:691