Home Assistant Unofficial Reference 2024.12.1
todo.py
Go to the documentation of this file.
1 """Todo platform for the Habitica integration."""
2 
3 from __future__ import annotations
4 
5 import datetime
6 from enum import StrEnum
7 from typing import TYPE_CHECKING
8 
9 from aiohttp import ClientResponseError
10 
11 from homeassistant.components import persistent_notification
13  TodoItem,
14  TodoItemStatus,
15  TodoListEntity,
16  TodoListEntityFeature,
17 )
18 from homeassistant.core import HomeAssistant
19 from homeassistant.exceptions import ServiceValidationError
20 from homeassistant.helpers.entity import EntityDescription
21 from homeassistant.helpers.entity_platform import AddEntitiesCallback
22 from homeassistant.util import dt as dt_util
23 
24 from .const import ASSETS_URL, DOMAIN
25 from .coordinator import HabiticaDataUpdateCoordinator
26 from .entity import HabiticaBase
27 from .types import HabiticaConfigEntry, HabiticaTaskType
28 from .util import next_due_date
29 
30 PARALLEL_UPDATES = 1
31 
32 
33 class HabiticaTodoList(StrEnum):
34  """Habitica Entities."""
35 
36  HABITS = "habits"
37  DAILIES = "dailys"
38  TODOS = "todos"
39  REWARDS = "rewards"
40 
41 
43  hass: HomeAssistant,
44  config_entry: HabiticaConfigEntry,
45  async_add_entities: AddEntitiesCallback,
46 ) -> None:
47  """Set up the sensor from a config entry created in the integrations UI."""
48  coordinator = config_entry.runtime_data
49 
51  [
52  HabiticaTodosListEntity(coordinator),
53  HabiticaDailiesListEntity(coordinator),
54  ],
55  )
56 
57 
59  """Representation of Habitica task lists."""
60 
61  def __init__(
62  self,
63  coordinator: HabiticaDataUpdateCoordinator,
64  ) -> None:
65  """Initialize HabiticaTodoListEntity."""
66 
67  super().__init__(coordinator, self.entity_descriptionentity_description)
68 
69  async def async_delete_todo_items(self, uids: list[str]) -> None:
70  """Delete Habitica tasks."""
71  if len(uids) > 1 and self.entity_descriptionentity_description.key is HabiticaTodoList.TODOS:
72  try:
73  await self.coordinator.api.tasks.clearCompletedTodos.post()
74  except ClientResponseError as e:
76  translation_domain=DOMAIN,
77  translation_key="delete_completed_todos_failed",
78  ) from e
79  else:
80  for task_id in uids:
81  try:
82  await self.coordinator.api.tasks[task_id].delete()
83  except ClientResponseError as e:
85  translation_domain=DOMAIN,
86  translation_key=f"delete_{self.entity_description.key}_failed",
87  ) from e
88 
89  await self.coordinator.async_request_refresh()
90 
92  self, uid: str, previous_uid: str | None = None
93  ) -> None:
94  """Move an item in the To-do list."""
95  if TYPE_CHECKING:
96  assert self.todo_itemstodo_items
97 
98  if previous_uid:
99  pos = (
100  self.todo_itemstodo_items.index(
101  next(item for item in self.todo_itemstodo_items if item.uid == previous_uid)
102  )
103  + 1
104  )
105  else:
106  pos = 0
107 
108  try:
109  await self.coordinator.api.tasks[uid].move.to[str(pos)].post()
110 
111  except ClientResponseError as e:
113  translation_domain=DOMAIN,
114  translation_key=f"move_{self.entity_description.key}_item_failed",
115  translation_placeholders={"pos": str(pos)},
116  ) from e
117  else:
118  # move tasks in the coordinator until we have fresh data
119  tasks = self.coordinator.data.tasks
120  new_pos = (
121  tasks.index(next(task for task in tasks if task["id"] == previous_uid))
122  + 1
123  if previous_uid
124  else 0
125  )
126  old_pos = tasks.index(next(task for task in tasks if task["id"] == uid))
127  tasks.insert(new_pos, tasks.pop(old_pos))
128  await self.coordinator.async_request_refresh()
129 
130  async def async_update_todo_item(self, item: TodoItem) -> None:
131  """Update a Habitica todo."""
132  refresh_required = False
133  current_item = next(
134  (task for task in (self.todo_itemstodo_items or []) if task.uid == item.uid),
135  None,
136  )
137 
138  if TYPE_CHECKING:
139  assert item.uid
140  assert current_item
141 
142  if (
143  self.entity_descriptionentity_description.key is HabiticaTodoList.TODOS
144  and item.due is not None
145  ): # Only todos support a due date.
146  date = item.due.isoformat()
147  else:
148  date = None
149 
150  if (
151  item.summary != current_item.summary
152  or item.description != current_item.description
153  or item.due != current_item.due
154  ):
155  try:
156  await self.coordinator.api.tasks[item.uid].put(
157  text=item.summary,
158  notes=item.description or "",
159  date=date,
160  )
161  refresh_required = True
162  except ClientResponseError as e:
164  translation_domain=DOMAIN,
165  translation_key=f"update_{self.entity_description.key}_item_failed",
166  translation_placeholders={"name": item.summary or ""},
167  ) from e
168 
169  try:
170  # Score up or down if item status changed
171  if (
172  current_item.status is TodoItemStatus.NEEDS_ACTION
173  and item.status == TodoItemStatus.COMPLETED
174  ):
175  score_result = (
176  await self.coordinator.api.tasks[item.uid].score["up"].post()
177  )
178  refresh_required = True
179  elif (
180  current_item.status is TodoItemStatus.COMPLETED
181  and item.status == TodoItemStatus.NEEDS_ACTION
182  ):
183  score_result = (
184  await self.coordinator.api.tasks[item.uid].score["down"].post()
185  )
186  refresh_required = True
187  else:
188  score_result = None
189 
190  except ClientResponseError as e:
192  translation_domain=DOMAIN,
193  translation_key=f"score_{self.entity_description.key}_item_failed",
194  translation_placeholders={"name": item.summary or ""},
195  ) from e
196 
197  if score_result and (drop := score_result.get("_tmp", {}).get("drop", False)):
198  msg = (
199  f"![{drop["key"]}]({ASSETS_URL}Pet_{drop["type"]}_{drop["key"]}.png)\n"
200  f"{drop["dialog"]}"
201  )
202  persistent_notification.async_create(
203  self.hasshasshass, message=msg, title="Habitica"
204  )
205  if refresh_required:
206  await self.coordinator.async_request_refresh()
207 
208 
210  """List of Habitica todos."""
211 
212  _attr_supported_features = (
213  TodoListEntityFeature.CREATE_TODO_ITEM
214  | TodoListEntityFeature.DELETE_TODO_ITEM
215  | TodoListEntityFeature.UPDATE_TODO_ITEM
216  | TodoListEntityFeature.MOVE_TODO_ITEM
217  | TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
218  | TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
219  )
220  entity_description = EntityDescription(
221  key=HabiticaTodoList.TODOS,
222  translation_key=HabiticaTodoList.TODOS,
223  )
224 
225  @property
226  def todo_items(self) -> list[TodoItem]:
227  """Return the todo items."""
228 
229  return [
230  *(
231  TodoItem(
232  uid=task["id"],
233  summary=task["text"],
234  description=task["notes"],
235  due=(
236  dt_util.as_local(
237  datetime.datetime.fromisoformat(task["date"])
238  ).date()
239  if task.get("date")
240  else None
241  ),
242  status=(
243  TodoItemStatus.NEEDS_ACTION
244  if not task["completed"]
245  else TodoItemStatus.COMPLETED
246  ),
247  )
248  for task in self.coordinator.data.tasks
249  if task["type"] == HabiticaTaskType.TODO
250  ),
251  ]
252 
253  async def async_create_todo_item(self, item: TodoItem) -> None:
254  """Create a Habitica todo."""
255 
256  try:
257  await self.coordinator.api.tasks.user.post(
258  text=item.summary,
259  type=HabiticaTaskType.TODO,
260  notes=item.description,
261  date=item.due.isoformat() if item.due else None,
262  )
263  except ClientResponseError as e:
265  translation_domain=DOMAIN,
266  translation_key=f"create_{self.entity_description.key}_item_failed",
267  translation_placeholders={"name": item.summary or ""},
268  ) from e
269 
270  await self.coordinator.async_request_refresh()
271 
272 
274  """List of Habitica dailies."""
275 
276  _attr_supported_features = (
277  TodoListEntityFeature.UPDATE_TODO_ITEM
278  | TodoListEntityFeature.MOVE_TODO_ITEM
279  | TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
280  | TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
281  )
282  entity_description = EntityDescription(
283  key=HabiticaTodoList.DAILIES,
284  translation_key=HabiticaTodoList.DAILIES,
285  )
286 
287  @property
288  def todo_items(self) -> list[TodoItem]:
289  """Return the dailies.
290 
291  dailies don't have a date, but we still can show the next due date,
292  which is a calculated value based on recurrence of the task.
293  If a task is a yesterdaily, the due date is the last time
294  a new day has been started. This allows to check off dailies from yesterday,
295  that have been completed but forgotten to mark as completed before resetting the dailies.
296  Changes of the date input field in Home Assistant will be ignored.
297  """
298 
299  last_cron = self.coordinator.data.user["lastCron"]
300 
301  return [
302  *(
303  TodoItem(
304  uid=task["id"],
305  summary=task["text"],
306  description=task["notes"],
307  due=next_due_date(task, last_cron),
308  status=(
309  TodoItemStatus.COMPLETED
310  if task["completed"]
311  else TodoItemStatus.NEEDS_ACTION
312  ),
313  )
314  for task in self.coordinator.data.tasks
315  if task["type"] == HabiticaTaskType.DAILY
316  )
317  ]
None __init__(self, HabiticaDataUpdateCoordinator coordinator)
Definition: todo.py:64
None async_move_todo_item(self, str uid, str|None previous_uid=None)
Definition: todo.py:93
list[TodoItem]|None todo_items(self)
Definition: __init__.py:256
web.Response post(self, web.Request request, str config_key)
Definition: view.py:101
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
web.Response delete(self, web.Request request, str config_key)
Definition: view.py:144
None async_setup_entry(HomeAssistant hass, HabiticaConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: todo.py:46
datetime.date|None next_due_date(dict[str, Any] task, str last_cron)
Definition: util.py:30