core/homeassistant/components/todo/__init__.py

261 lines
8.6 KiB
Python

"""The todo integration."""
import dataclasses
import datetime
import logging
from typing import Any
import voluptuous as vol
from homeassistant.components import frontend, websocket_api
from homeassistant.components.websocket_api import ERR_NOT_FOUND, ERR_NOT_SUPPORTED
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, TodoItemStatus, TodoListEntityFeature
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = datetime.timedelta(seconds=60)
ENTITY_ID_FORMAT = DOMAIN + ".{}"
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Todo entities."""
component = hass.data[DOMAIN] = EntityComponent[TodoListEntity](
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
)
frontend.async_register_built_in_panel(hass, "todo", "todo", "mdi:clipboard-list")
websocket_api.async_register_command(hass, websocket_handle_todo_item_list)
websocket_api.async_register_command(hass, websocket_handle_todo_item_move)
component.async_register_entity_service(
"add_item",
{
vol.Required("item"): vol.All(cv.string, vol.Length(min=1)),
},
_async_add_todo_item,
required_features=[TodoListEntityFeature.CREATE_TODO_ITEM],
)
component.async_register_entity_service(
"update_item",
vol.All(
cv.make_entity_service_schema(
{
vol.Required("item"): vol.All(cv.string, vol.Length(min=1)),
vol.Optional("rename"): vol.All(cv.string, vol.Length(min=1)),
vol.Optional("status"): vol.In(
{TodoItemStatus.NEEDS_ACTION, TodoItemStatus.COMPLETED}
),
}
),
cv.has_at_least_one_key("rename", "status"),
),
_async_update_todo_item,
required_features=[TodoListEntityFeature.UPDATE_TODO_ITEM],
)
component.async_register_entity_service(
"remove_item",
cv.make_entity_service_schema(
{
vol.Required("item"): vol.All(cv.ensure_list, [cv.string]),
}
),
_async_remove_todo_items,
required_features=[TodoListEntityFeature.DELETE_TODO_ITEM],
)
await component.async_setup(config)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
component: EntityComponent[TodoListEntity] = hass.data[DOMAIN]
return await component.async_setup_entry(entry)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
component: EntityComponent[TodoListEntity] = hass.data[DOMAIN]
return await component.async_unload_entry(entry)
@dataclasses.dataclass
class TodoItem:
"""A To-do item in a To-do list."""
summary: str | None = None
"""The summary that represents the item."""
uid: str | None = None
"""A unique identifier for the To-do item."""
status: TodoItemStatus | None = None
"""A status or confirmation of the To-do item."""
class TodoListEntity(Entity):
"""An entity that represents a To-do list."""
_attr_todo_items: list[TodoItem] | None = None
@property
def state(self) -> int | None:
"""Return the entity state as the count of incomplete items."""
items = self.todo_items
if items is None:
return None
return sum([item.status == TodoItemStatus.NEEDS_ACTION for item in items])
@property
def todo_items(self) -> list[TodoItem] | None:
"""Return the To-do items in the To-do list."""
return self._attr_todo_items
async def async_create_todo_item(self, item: TodoItem) -> None:
"""Add an item to the To-do list."""
raise NotImplementedError()
async def async_update_todo_item(self, item: TodoItem) -> None:
"""Update an item in the To-do list."""
raise NotImplementedError()
async def async_delete_todo_items(self, uids: list[str]) -> None:
"""Delete an item in the To-do list."""
raise NotImplementedError()
async def async_move_todo_item(
self, uid: str, previous_uid: str | None = None
) -> None:
"""Move an item in the To-do list.
The To-do item with the specified `uid` should be moved to the position
in the list after the specified by `previous_uid` or `None` for the first
position in the To-do list.
"""
raise NotImplementedError()
@websocket_api.websocket_command(
{
vol.Required("type"): "todo/item/list",
vol.Required("entity_id"): cv.entity_id,
}
)
@websocket_api.async_response
async def websocket_handle_todo_item_list(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None:
"""Handle the list of To-do items in a To-do- list."""
component: EntityComponent[TodoListEntity] = hass.data[DOMAIN]
if (
not (entity_id := msg[CONF_ENTITY_ID])
or not (entity := component.get_entity(entity_id))
or not isinstance(entity, TodoListEntity)
):
connection.send_error(msg["id"], ERR_NOT_FOUND, "Entity not found")
return
items: list[TodoItem] = entity.todo_items or []
connection.send_message(
websocket_api.result_message(
msg["id"], {"items": [dataclasses.asdict(item) for item in items]}
)
)
@websocket_api.websocket_command(
{
vol.Required("type"): "todo/item/move",
vol.Required("entity_id"): cv.entity_id,
vol.Required("uid"): cv.string,
vol.Optional("previous_uid"): cv.string,
}
)
@websocket_api.async_response
async def websocket_handle_todo_item_move(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None:
"""Handle move of a To-do item within a To-do list."""
component: EntityComponent[TodoListEntity] = hass.data[DOMAIN]
if not (entity := component.get_entity(msg["entity_id"])):
connection.send_error(msg["id"], ERR_NOT_FOUND, "Entity not found")
return
if (
not entity.supported_features
or not entity.supported_features & TodoListEntityFeature.MOVE_TODO_ITEM
):
connection.send_message(
websocket_api.error_message(
msg["id"],
ERR_NOT_SUPPORTED,
"To-do list does not support To-do item reordering",
)
)
return
try:
await entity.async_move_todo_item(
uid=msg["uid"], previous_uid=msg.get("previous_uid")
)
except HomeAssistantError as ex:
connection.send_error(msg["id"], "failed", str(ex))
else:
connection.send_result(msg["id"])
def _find_by_uid_or_summary(
value: str, items: list[TodoItem] | None
) -> TodoItem | None:
"""Find a To-do List item by uid or summary name."""
for item in items or ():
if value in (item.uid, item.summary):
return item
return None
async def _async_add_todo_item(entity: TodoListEntity, call: ServiceCall) -> None:
"""Add an item to the To-do list."""
await entity.async_create_todo_item(
item=TodoItem(summary=call.data["item"], status=TodoItemStatus.NEEDS_ACTION)
)
async def _async_update_todo_item(entity: TodoListEntity, call: ServiceCall) -> None:
"""Update an item in the To-do list."""
item = call.data["item"]
found = _find_by_uid_or_summary(item, entity.todo_items)
if not found:
raise ValueError(f"Unable to find To-do item '{item}'")
update_item = TodoItem(
uid=found.uid, summary=call.data.get("rename"), status=call.data.get("status")
)
await entity.async_update_todo_item(item=update_item)
async def _async_remove_todo_items(entity: TodoListEntity, call: ServiceCall) -> None:
"""Remove an item in the To-do list."""
uids = []
for item in call.data.get("item", []):
found = _find_by_uid_or_summary(item, entity.todo_items)
if not found or not found.uid:
raise ValueError(f"Unable to find To-do item '{item}")
uids.append(found.uid)
await entity.async_delete_todo_items(uids=uids)