1 """The todo integration."""
3 from __future__
import annotations
5 from collections.abc
import Callable, Iterable
9 from typing
import Any, final
11 from propcache
import cached_property
12 import voluptuous
as vol
44 TodoListEntityFeature,
48 _LOGGER = logging.getLogger(__name__)
50 ENTITY_ID_FORMAT = DOMAIN +
".{}"
51 PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
52 PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
53 SCAN_INTERVAL = datetime.timedelta(seconds=60)
56 @dataclasses.dataclass
58 """A description of To-do item fields and validation requirements."""
61 """Field name for service calls."""
64 """Field name for TodoItem."""
66 validation: Callable[[Any], Any]
67 """Voluptuous validation function."""
69 required_feature: TodoListEntityFeature
70 """Entity feature that enables this field."""
75 service_field=ATTR_DUE_DATE,
76 validation=vol.Any(cv.date,
None),
77 todo_item_field=ATTR_DUE,
78 required_feature=TodoListEntityFeature.SET_DUE_DATE_ON_ITEM,
81 service_field=ATTR_DUE_DATETIME,
82 validation=vol.Any(vol.All(cv.datetime, dt_util.as_local),
None),
83 todo_item_field=ATTR_DUE,
84 required_feature=TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM,
87 service_field=ATTR_DESCRIPTION,
88 validation=vol.Any(cv.string,
None),
89 todo_item_field=ATTR_DESCRIPTION,
90 required_feature=TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM,
94 TODO_ITEM_FIELD_SCHEMA = {
95 vol.Optional(desc.service_field): desc.validation
for desc
in TODO_ITEM_FIELDS
97 TODO_ITEM_FIELD_VALIDATIONS = [cv.has_at_most_one_key(ATTR_DUE_DATE, ATTR_DUE_DATETIME)]
101 supported_features: int |
None, call_data: dict[str, Any]
103 """Validate service call fields against entity supported features."""
104 for desc
in TODO_ITEM_FIELDS:
105 if desc.service_field
not in call_data:
107 if not supported_features
or not supported_features & desc.required_feature:
109 translation_domain=DOMAIN,
110 translation_key=
"update_field_not_supported",
111 translation_placeholders={
"service_field": desc.service_field},
115 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
116 """Set up Todo entities."""
117 component = hass.data[DATA_COMPONENT] = EntityComponent[TodoListEntity](
118 _LOGGER, DOMAIN, hass, SCAN_INTERVAL
121 frontend.async_register_built_in_panel(hass,
"todo",
"todo",
"mdi:clipboard-list")
123 websocket_api.async_register_command(hass, websocket_handle_subscribe_todo_items)
124 websocket_api.async_register_command(hass, websocket_handle_todo_item_list)
125 websocket_api.async_register_command(hass, websocket_handle_todo_item_move)
127 component.async_register_entity_service(
128 TodoServices.ADD_ITEM,
130 cv.make_entity_service_schema(
132 vol.Required(ATTR_ITEM): vol.All(cv.string, vol.Length(min=1)),
133 **TODO_ITEM_FIELD_SCHEMA,
136 *TODO_ITEM_FIELD_VALIDATIONS,
138 _async_add_todo_item,
139 required_features=[TodoListEntityFeature.CREATE_TODO_ITEM],
141 component.async_register_entity_service(
142 TodoServices.UPDATE_ITEM,
144 cv.make_entity_service_schema(
146 vol.Required(ATTR_ITEM): vol.All(cv.string, vol.Length(min=1)),
147 vol.Optional(ATTR_RENAME): vol.All(cv.string, vol.Length(min=1)),
148 vol.Optional(ATTR_STATUS): vol.In(
149 {TodoItemStatus.NEEDS_ACTION, TodoItemStatus.COMPLETED},
151 **TODO_ITEM_FIELD_SCHEMA,
154 *TODO_ITEM_FIELD_VALIDATIONS,
155 cv.has_at_least_one_key(
158 *[desc.service_field
for desc
in TODO_ITEM_FIELDS],
161 _async_update_todo_item,
162 required_features=[TodoListEntityFeature.UPDATE_TODO_ITEM],
164 component.async_register_entity_service(
165 TodoServices.REMOVE_ITEM,
166 cv.make_entity_service_schema(
168 vol.Required(ATTR_ITEM): vol.All(cv.ensure_list, [cv.string]),
171 _async_remove_todo_items,
172 required_features=[TodoListEntityFeature.DELETE_TODO_ITEM],
174 component.async_register_entity_service(
175 TodoServices.GET_ITEMS,
176 cv.make_entity_service_schema(
178 vol.Optional(ATTR_STATUS): vol.All(
180 [vol.In({TodoItemStatus.NEEDS_ACTION, TodoItemStatus.COMPLETED})],
184 _async_get_todo_items,
185 supports_response=SupportsResponse.ONLY,
187 component.async_register_entity_service(
188 TodoServices.REMOVE_COMPLETED_ITEMS,
190 _async_remove_completed_items,
191 required_features=[TodoListEntityFeature.DELETE_TODO_ITEM],
194 await component.async_setup(config)
199 """Set up a config entry."""
204 """Unload a config entry."""
208 @dataclasses.dataclass
210 """A To-do item in a To-do list."""
212 summary: str |
None =
None
213 """The summary that represents the item."""
215 uid: str |
None =
None
216 """A unique identifier for the To-do item."""
218 status: TodoItemStatus |
None =
None
219 """A status or confirmation of the To-do item."""
221 due: datetime.date | datetime.datetime |
None =
None
222 """The date and time that a to-do is expected to be completed.
224 This field may be a date or datetime depending whether the entity feature
225 DUE_DATE or DUE_DATETIME are set.
228 description: str |
None =
None
229 """A more complete description of than that provided by the summary.
231 This field may be set when TodoListEntityFeature.DESCRIPTION is supported by
236 CACHED_PROPERTIES_WITH_ATTR_ = {
242 """An entity that represents a To-do list."""
244 _attr_todo_items: list[TodoItem] |
None =
None
245 _update_listeners: list[Callable[[list[JsonValueType] |
None],
None]] |
None =
None
249 """Return the entity state as the count of incomplete items."""
253 return sum([item.status == TodoItemStatus.NEEDS_ACTION
for item
in items])
257 """Return the To-do items in the To-do list."""
258 return self._attr_todo_items
261 """Add an item to the To-do list."""
262 raise NotImplementedError
265 """Update an item in the To-do list."""
266 raise NotImplementedError
269 """Delete an item in the To-do list."""
270 raise NotImplementedError
273 self, uid: str, previous_uid: str |
None =
None
275 """Move an item in the To-do list.
277 The To-do item with the specified `uid` should be moved to the position
278 in the list after the specified by `previous_uid` or `None` for the first
279 position in the To-do list.
281 raise NotImplementedError
287 listener: Callable[[list[JsonValueType] |
None],
None],
289 """Subscribe to To-do list item updates.
291 Called by websocket API.
298 def unsubscribe() -> None:
307 """Push updated To-do items to all listeners."""
311 todo_items: list[JsonValueType] = [
312 dataclasses.asdict(item)
for item
in self.
todo_itemstodo_items
or ()
319 """Notify to-do item subscribers."""
324 @websocket_api.websocket_command(
{
vol.Required("type"):
"todo/item/subscribe",
325 vol.Required(
"entity_id"): cv.entity_domain(DOMAIN),
328 @websocket_api.async_response
332 """Subscribe to To-do list item updates."""
333 entity_id: str = msg[
"entity_id"]
335 if not (entity := hass.data[DATA_COMPONENT].
get_entity(entity_id)):
336 connection.send_error(
339 f
"To-do list entity not found: {entity_id}",
344 def todo_item_listener(todo_items: list[JsonValueType] |
None) ->
None:
345 """Push updated To-do list items to websocket."""
346 connection.send_message(
347 websocket_api.event_message(
355 connection.subscriptions[msg[
"id"]] = entity.async_subscribe_updates(
358 connection.send_result(msg[
"id"])
361 entity.async_update_listeners()
365 """Convert CalendarEvent dataclass items to dictionary of attributes."""
366 result: dict[str, str] = {}
367 for name, value
in obj:
370 if isinstance(value, (datetime.date, datetime.datetime)):
371 result[name] = value.isoformat()
373 result[name] =
str(value)
377 @websocket_api.websocket_command(
{
vol.Required("type"):
"todo/item/list",
378 vol.Required(
"entity_id"): cv.entity_id,
381 @websocket_api.async_response
385 """Handle the list of To-do items in a To-do- list."""
387 not (entity_id := msg[CONF_ENTITY_ID])
388 or not (entity := hass.data[DATA_COMPONENT].
get_entity(entity_id))
389 or not isinstance(entity, TodoListEntity)
391 connection.send_error(msg[
"id"], ERR_NOT_FOUND,
"Entity not found")
394 items: list[TodoItem] = entity.todo_items
or []
395 connection.send_message(
396 websocket_api.result_message(
400 dataclasses.asdict(item, dict_factory=_api_items_factory)
408 @websocket_api.websocket_command(
{
vol.Required("type"):
"todo/item/move",
409 vol.Required(
"entity_id"): cv.entity_id,
410 vol.Required(
"uid"): cv.string,
411 vol.Optional(
"previous_uid"): cv.string,
414 @websocket_api.async_response
418 """Handle move of a To-do item within a To-do list."""
419 if not (entity := hass.data[DATA_COMPONENT].
get_entity(msg[
"entity_id"])):
420 connection.send_error(msg[
"id"], ERR_NOT_FOUND,
"Entity not found")
424 not entity.supported_features
425 or not entity.supported_features & TodoListEntityFeature.MOVE_TODO_ITEM
427 connection.send_message(
428 websocket_api.error_message(
431 "To-do list does not support To-do item reordering",
436 await entity.async_move_todo_item(
437 uid=msg[
"uid"], previous_uid=msg.get(
"previous_uid")
439 except HomeAssistantError
as ex:
440 connection.send_error(msg[
"id"],
"failed",
str(ex))
442 connection.send_result(msg[
"id"])
446 value: str, items: list[TodoItem] |
None
447 ) -> TodoItem |
None:
448 """Find a To-do List item by uid or summary name."""
449 for item
in items
or ():
450 if value
in (item.uid, item.summary):
456 """Add an item to the To-do list."""
458 await entity.async_create_todo_item(
460 summary=call.data[
"item"],
461 status=TodoItemStatus.NEEDS_ACTION,
463 desc.todo_item_field: call.data[desc.service_field]
464 for desc
in TODO_ITEM_FIELDS
465 if desc.service_field
in call.data
472 """Update an item in the To-do list."""
473 item = call.data[
"item"]
477 translation_domain=DOMAIN,
478 translation_key=
"item_not_found",
479 translation_placeholders={
"item": item},
487 updated_data = dataclasses.asdict(found)
488 if summary := call.data.get(
"rename"):
489 updated_data[
"summary"] = summary
490 if status := call.data.get(
"status"):
491 updated_data[
"status"] = status
494 desc.todo_item_field: call.data[desc.service_field]
495 for desc
in TODO_ITEM_FIELDS
496 if desc.service_field
in call.data
499 await entity.async_update_todo_item(item=
TodoItem(**updated_data))
503 """Remove an item in the To-do list."""
505 for item
in call.data.get(
"item", []):
507 if not found
or not found.uid:
509 translation_domain=DOMAIN,
510 translation_key=
"item_not_found",
511 translation_placeholders={
"item": item},
513 uids.append(found.uid)
514 await entity.async_delete_todo_items(uids=uids)
518 entity: TodoListEntity, call: ServiceCall
520 """Return items in the To-do list."""
523 dataclasses.asdict(item, dict_factory=_api_items_factory)
524 for item
in entity.todo_items
or ()
525 if not (statuses := call.data.get(
"status"))
or item.status
in statuses
531 """Remove all completed items from the To-do list."""
534 for item
in entity.todo_items
or ()
535 if item.status == TodoItemStatus.COMPLETED
and item.uid
538 await entity.async_delete_todo_items(uids=uids)
539
None async_move_todo_item(self, str uid, str|None previous_uid=None)
None async_update_listeners(self)
None async_create_todo_item(self, TodoItem item)
None async_update_todo_item(self, TodoItem item)
None async_delete_todo_items(self, list[str] uids)
CALLBACK_TYPE async_subscribe_updates(self, Callable[[list[JsonValueType]|None], None] listener)
None _async_write_ha_state(self)
list[TodoItem]|None todo_items(self)
bool remove(self, _T matcher)
CalendarEntity get_entity(HomeAssistant hass, str entity_id)
None _validate_supported_features(int|None supported_features, dict[str, Any] call_data)
None websocket_handle_todo_item_move(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
dict[str, str] _api_items_factory(Iterable[tuple[str, Any]] obj)
None websocket_handle_subscribe_todo_items(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
TodoItem|None _find_by_uid_or_summary(str value, list[TodoItem]|None items)
None _async_remove_completed_items(TodoListEntity entity, ServiceCall _)
None _async_remove_todo_items(TodoListEntity entity, ServiceCall call)
None _async_add_todo_item(TodoListEntity entity, ServiceCall call)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
dict[str, Any] _async_get_todo_items(TodoListEntity entity, ServiceCall call)
None websocket_handle_todo_item_list(HomeAssistant hass, websocket_api.ActiveConnection connection, dict[str, Any] msg)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
None _async_update_todo_item(TodoListEntity entity, ServiceCall call)
bool async_setup(HomeAssistant hass, ConfigType config)