core/homeassistant/components/mealie/todo.py

266 lines
9.4 KiB
Python

"""Todo platform for Mealie."""
from __future__ import annotations
from aiomealie import MealieError, MutateShoppingItem, ShoppingItem, ShoppingList
from homeassistant.components.todo import (
DOMAIN as TODO_DOMAIN,
TodoItem,
TodoItemStatus,
TodoListEntity,
TodoListEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import MealieConfigEntry, MealieShoppingListCoordinator
from .entity import MealieEntity
PARALLEL_UPDATES = 0
TODO_STATUS_MAP = {
False: TodoItemStatus.NEEDS_ACTION,
True: TodoItemStatus.COMPLETED,
}
TODO_STATUS_MAP_INV = {v: k for k, v in TODO_STATUS_MAP.items()}
def _convert_api_item(item: ShoppingItem) -> TodoItem:
"""Convert Mealie shopping list items into a TodoItem."""
return TodoItem(
summary=item.display,
uid=item.item_id,
status=TODO_STATUS_MAP.get(
item.checked,
TodoItemStatus.NEEDS_ACTION,
),
due=None,
description=None,
)
async def async_setup_entry(
hass: HomeAssistant,
entry: MealieConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the todo platform for entity."""
coordinator = entry.runtime_data.shoppinglist_coordinator
added_lists: set[str] = set()
assert entry.unique_id is not None
def _async_delete_entities(lists: set[str]) -> None:
"""Delete entities for removed shopping lists."""
entity_registry = er.async_get(hass)
for list_id in lists:
entity_id = entity_registry.async_get_entity_id(
TODO_DOMAIN, DOMAIN, f"{entry.unique_id}_{list_id}"
)
if entity_id:
entity_registry.async_remove(entity_id)
def _async_entity_listener() -> None:
"""Handle additions/deletions of shopping lists."""
received_lists = set(coordinator.data)
new_lists = received_lists - added_lists
removed_lists = added_lists - received_lists
if new_lists:
async_add_entities(
MealieShoppingListTodoListEntity(coordinator, shopping_list_id)
for shopping_list_id in new_lists
)
added_lists.update(new_lists)
if removed_lists:
_async_delete_entities(removed_lists)
coordinator.async_add_listener(_async_entity_listener)
_async_entity_listener()
class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity):
"""A todo list entity."""
_attr_supported_features = (
TodoListEntityFeature.CREATE_TODO_ITEM
| TodoListEntityFeature.UPDATE_TODO_ITEM
| TodoListEntityFeature.DELETE_TODO_ITEM
| TodoListEntityFeature.MOVE_TODO_ITEM
)
_attr_translation_key = "shopping_list"
coordinator: MealieShoppingListCoordinator
def __init__(
self, coordinator: MealieShoppingListCoordinator, shopping_list_id: str
) -> None:
"""Create the todo entity."""
super().__init__(coordinator, shopping_list_id)
self._shopping_list_id = shopping_list_id
self._attr_name = self.shopping_list.name
@property
def shopping_list(self) -> ShoppingList:
"""Get the shopping list."""
return self.coordinator.data[self._shopping_list_id].shopping_list
@property
def shopping_items(self) -> list[ShoppingItem]:
"""Get the shopping items for this list."""
return self.coordinator.data[self._shopping_list_id].items
@property
def todo_items(self) -> list[TodoItem] | None:
"""Get the current set of To-do items."""
return [_convert_api_item(item) for item in self.shopping_items]
async def async_create_todo_item(self, item: TodoItem) -> None:
"""Add an item to the list."""
position = 0
if len(self.shopping_items) > 0:
position = self.shopping_items[-1].position + 1
new_shopping_item = MutateShoppingItem(
list_id=self._shopping_list_id,
note=item.summary.strip() if item.summary else item.summary,
position=position,
)
try:
await self.coordinator.client.add_shopping_item(new_shopping_item)
except MealieError as exception:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="add_item_error",
translation_placeholders={
"shopping_list_name": self.shopping_list.name
},
) from exception
finally:
await self.coordinator.async_refresh()
async def async_update_todo_item(self, item: TodoItem) -> None:
"""Update an item on the list."""
list_items = self.shopping_items
list_item: ShoppingItem | None = next(
(x for x in list_items if x.item_id == item.uid), None
)
assert list_item is not None
position = list_item.position
update_shopping_item = MutateShoppingItem(
item_id=list_item.item_id,
list_id=list_item.list_id,
note=list_item.note,
display=list_item.display,
checked=item.status == TodoItemStatus.COMPLETED,
position=position,
is_food=list_item.is_food,
disable_amount=list_item.disable_amount,
quantity=list_item.quantity,
label_id=list_item.label_id,
food_id=list_item.food_id,
unit_id=list_item.unit_id,
)
stripped_item_summary = item.summary.strip() if item.summary else item.summary
if list_item.display.strip() != stripped_item_summary:
update_shopping_item.note = stripped_item_summary
update_shopping_item.position = position
update_shopping_item.is_food = False
update_shopping_item.food_id = None
update_shopping_item.quantity = 0.0
update_shopping_item.checked = item.status == TodoItemStatus.COMPLETED
try:
await self.coordinator.client.update_shopping_item(
list_item.item_id, update_shopping_item
)
except MealieError as exception:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="update_item_error",
translation_placeholders={
"shopping_list_name": self.shopping_list.name
},
) from exception
finally:
await self.coordinator.async_refresh()
async def async_delete_todo_items(self, uids: list[str]) -> None:
"""Delete items from the list."""
try:
for uid in uids:
await self.coordinator.client.delete_shopping_item(uid)
except MealieError as exception:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="delete_item_error",
translation_placeholders={
"shopping_list_name": self.shopping_list.name
},
) from exception
finally:
await self.coordinator.async_refresh()
async def async_move_todo_item(
self, uid: str, previous_uid: str | None = None
) -> None:
"""Re-order an item on the list."""
if uid == previous_uid:
return
list_items: list[ShoppingItem] = self.shopping_items
item_idx = {itm.item_id: idx for idx, itm in enumerate(list_items)}
if uid not in item_idx:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="item_not_found_error",
translation_placeholders={"shopping_list_item": uid},
)
if previous_uid and previous_uid not in item_idx:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="item_not_found_error",
translation_placeholders={"shopping_list_item": previous_uid},
)
dst_idx = item_idx[previous_uid] + 1 if previous_uid else 0
src_idx = item_idx[uid]
src_item = list_items.pop(src_idx)
if dst_idx > src_idx:
dst_idx -= 1
list_items.insert(dst_idx, src_item)
for position, item in enumerate(list_items):
mutate_shopping_item = MutateShoppingItem()
mutate_shopping_item.list_id = item.list_id
mutate_shopping_item.item_id = item.item_id
mutate_shopping_item.position = position
mutate_shopping_item.is_food = item.is_food
mutate_shopping_item.quantity = item.quantity
mutate_shopping_item.label_id = item.label_id
mutate_shopping_item.note = item.note
mutate_shopping_item.checked = item.checked
if item.is_food:
mutate_shopping_item.food_id = item.food_id
mutate_shopping_item.unit_id = item.unit_id
await self.coordinator.client.update_shopping_item(
mutate_shopping_item.item_id, mutate_shopping_item
)
await self.coordinator.async_refresh()
@property
def available(self) -> bool:
"""Return False if shopping list no longer available."""
return super().available and self._shopping_list_id in self.coordinator.data