Home Assistant Unofficial Reference 2024.12.1
todo.py
Go to the documentation of this file.
1 """Todo platform for Mealie."""
2 
3 from __future__ import annotations
4 
5 from aiomealie import MealieError, MutateShoppingItem, ShoppingItem, ShoppingList
6 
8  DOMAIN as TODO_DOMAIN,
9  TodoItem,
10  TodoItemStatus,
11  TodoListEntity,
12  TodoListEntityFeature,
13 )
14 from homeassistant.core import HomeAssistant
15 from homeassistant.exceptions import HomeAssistantError
16 from homeassistant.helpers import entity_registry as er
17 from homeassistant.helpers.entity_platform import AddEntitiesCallback
18 
19 from .const import DOMAIN
20 from .coordinator import MealieConfigEntry, MealieShoppingListCoordinator
21 from .entity import MealieEntity
22 
23 TODO_STATUS_MAP = {
24  False: TodoItemStatus.NEEDS_ACTION,
25  True: TodoItemStatus.COMPLETED,
26 }
27 TODO_STATUS_MAP_INV = {v: k for k, v in TODO_STATUS_MAP.items()}
28 
29 
30 def _convert_api_item(item: ShoppingItem) -> TodoItem:
31  """Convert Mealie shopping list items into a TodoItem."""
32 
33  return TodoItem(
34  summary=item.display,
35  uid=item.item_id,
36  status=TODO_STATUS_MAP.get(
37  item.checked,
38  TodoItemStatus.NEEDS_ACTION,
39  ),
40  due=None,
41  description=None,
42  )
43 
44 
46  hass: HomeAssistant,
47  entry: MealieConfigEntry,
48  async_add_entities: AddEntitiesCallback,
49 ) -> None:
50  """Set up the todo platform for entity."""
51  coordinator = entry.runtime_data.shoppinglist_coordinator
52 
53  added_lists: set[str] = set()
54 
55  assert entry.unique_id is not None
56 
57  def _async_delete_entities(lists: set[str]) -> None:
58  """Delete entities for removed shopping lists."""
59  entity_registry = er.async_get(hass)
60  for list_id in lists:
61  entity_id = entity_registry.async_get_entity_id(
62  TODO_DOMAIN, DOMAIN, f"{entry.unique_id}_{list_id}"
63  )
64  if entity_id:
65  entity_registry.async_remove(entity_id)
66 
67  def _async_entity_listener() -> None:
68  """Handle additions/deletions of shopping lists."""
69  received_lists = set(coordinator.data)
70  new_lists = received_lists - added_lists
71  removed_lists = added_lists - received_lists
72  if new_lists:
74  MealieShoppingListTodoListEntity(coordinator, shopping_list_id)
75  for shopping_list_id in new_lists
76  )
77  added_lists.update(new_lists)
78  if removed_lists:
79  _async_delete_entities(removed_lists)
80 
81  coordinator.async_add_listener(_async_entity_listener)
82  _async_entity_listener()
83 
84 
86  """A todo list entity."""
87 
88  _attr_supported_features = (
89  TodoListEntityFeature.CREATE_TODO_ITEM
90  | TodoListEntityFeature.UPDATE_TODO_ITEM
91  | TodoListEntityFeature.DELETE_TODO_ITEM
92  | TodoListEntityFeature.MOVE_TODO_ITEM
93  )
94 
95  _attr_translation_key = "shopping_list"
96 
97  coordinator: MealieShoppingListCoordinator
98 
99  def __init__(
100  self, coordinator: MealieShoppingListCoordinator, shopping_list_id: str
101  ) -> None:
102  """Create the todo entity."""
103  super().__init__(coordinator, shopping_list_id)
104  self._shopping_list_id_shopping_list_id = shopping_list_id
105  self._attr_name_attr_name = self.shopping_listshopping_list.name
106 
107  @property
108  def shopping_list(self) -> ShoppingList:
109  """Get the shopping list."""
110  return self.coordinator.data[self._shopping_list_id_shopping_list_id].shopping_list
111 
112  @property
113  def shopping_items(self) -> list[ShoppingItem]:
114  """Get the shopping items for this list."""
115  return self.coordinator.data[self._shopping_list_id_shopping_list_id].items
116 
117  @property
118  def todo_items(self) -> list[TodoItem] | None:
119  """Get the current set of To-do items."""
120  return [_convert_api_item(item) for item in self.shopping_itemsshopping_items]
121 
122  async def async_create_todo_item(self, item: TodoItem) -> None:
123  """Add an item to the list."""
124  position = 0
125  if len(self.shopping_itemsshopping_items) > 0:
126  position = self.shopping_itemsshopping_items[-1].position + 1
127 
128  new_shopping_item = MutateShoppingItem(
129  list_id=self._shopping_list_id_shopping_list_id,
130  note=item.summary.strip() if item.summary else item.summary,
131  position=position,
132  )
133  try:
134  await self.coordinator.client.add_shopping_item(new_shopping_item)
135  except MealieError as exception:
136  raise HomeAssistantError(
137  translation_domain=DOMAIN,
138  translation_key="add_item_error",
139  translation_placeholders={
140  "shopping_list_name": self.shopping_listshopping_list.name
141  },
142  ) from exception
143  finally:
144  await self.coordinator.async_refresh()
145 
146  async def async_update_todo_item(self, item: TodoItem) -> None:
147  """Update an item on the list."""
148  list_items = self.shopping_itemsshopping_items
149 
150  for items in list_items:
151  if items.item_id == item.uid:
152  position = items.position
153  break
154 
155  list_item: ShoppingItem | None = next(
156  (x for x in list_items if x.item_id == item.uid), None
157  )
158 
159  if not list_item:
160  raise HomeAssistantError(
161  translation_domain=DOMAIN,
162  translation_key="item_not_found_error",
163  translation_placeholders={"shopping_list_item": item.uid or ""},
164  )
165 
166  udpdate_shopping_item = MutateShoppingItem(
167  item_id=list_item.item_id,
168  list_id=list_item.list_id,
169  note=list_item.note,
170  display=list_item.display,
171  checked=item.status == TodoItemStatus.COMPLETED,
172  position=list_item.position,
173  is_food=list_item.is_food,
174  disable_amount=list_item.disable_amount,
175  quantity=list_item.quantity,
176  label_id=list_item.label_id,
177  food_id=list_item.food_id,
178  unit_id=list_item.unit_id,
179  )
180 
181  stripped_item_summary = item.summary.strip() if item.summary else item.summary
182 
183  if list_item.display.strip() != stripped_item_summary:
184  udpdate_shopping_item.note = stripped_item_summary
185  udpdate_shopping_item.position = position
186  udpdate_shopping_item.is_food = False
187  udpdate_shopping_item.food_id = None
188  udpdate_shopping_item.quantity = 0.0
189  udpdate_shopping_item.checked = item.status == TodoItemStatus.COMPLETED
190 
191  try:
192  await self.coordinator.client.update_shopping_item(
193  list_item.item_id, udpdate_shopping_item
194  )
195  except MealieError as exception:
196  raise HomeAssistantError(
197  translation_domain=DOMAIN,
198  translation_key="update_item_error",
199  translation_placeholders={
200  "shopping_list_name": self.shopping_listshopping_list.name
201  },
202  ) from exception
203  finally:
204  await self.coordinator.async_refresh()
205 
206  async def async_delete_todo_items(self, uids: list[str]) -> None:
207  """Delete items from the list."""
208  try:
209  for uid in uids:
210  await self.coordinator.client.delete_shopping_item(uid)
211  except MealieError as exception:
212  raise HomeAssistantError(
213  translation_domain=DOMAIN,
214  translation_key="delete_item_error",
215  translation_placeholders={
216  "shopping_list_name": self.shopping_listshopping_list.name
217  },
218  ) from exception
219  finally:
220  await self.coordinator.async_refresh()
221 
223  self, uid: str, previous_uid: str | None = None
224  ) -> None:
225  """Re-order an item on the list."""
226  if uid == previous_uid:
227  return
228  list_items: list[ShoppingItem] = self.shopping_itemsshopping_items
229 
230  item_idx = {itm.item_id: idx for idx, itm in enumerate(list_items)}
231  if uid not in item_idx:
232  raise HomeAssistantError(
233  translation_domain=DOMAIN,
234  translation_key="item_not_found_error",
235  translation_placeholders={"shopping_list_item": uid},
236  )
237  if previous_uid and previous_uid not in item_idx:
238  raise HomeAssistantError(
239  translation_domain=DOMAIN,
240  translation_key="item_not_found_error",
241  translation_placeholders={"shopping_list_item": previous_uid},
242  )
243  dst_idx = item_idx[previous_uid] + 1 if previous_uid else 0
244  src_idx = item_idx[uid]
245  src_item = list_items.pop(src_idx)
246  if dst_idx > src_idx:
247  dst_idx -= 1
248  list_items.insert(dst_idx, src_item)
249 
250  for position, item in enumerate(list_items):
251  mutate_shopping_item = MutateShoppingItem()
252  mutate_shopping_item.list_id = item.list_id
253  mutate_shopping_item.item_id = item.item_id
254  mutate_shopping_item.position = position
255  mutate_shopping_item.is_food = item.is_food
256  mutate_shopping_item.quantity = item.quantity
257  mutate_shopping_item.label_id = item.label_id
258  mutate_shopping_item.note = item.note
259  mutate_shopping_item.checked = item.checked
260 
261  if item.is_food:
262  mutate_shopping_item.food_id = item.food_id
263  mutate_shopping_item.unit_id = item.unit_id
264 
265  await self.coordinator.client.update_shopping_item(
266  mutate_shopping_item.item_id, mutate_shopping_item
267  )
268 
269  await self.coordinator.async_refresh()
270 
271  @property
272  def available(self) -> bool:
273  """Return False if shopping list no longer available."""
274  return super().available and self._shopping_list_id_shopping_list_id in self.coordinator.data
None async_move_todo_item(self, str uid, str|None previous_uid=None)
Definition: todo.py:224
None __init__(self, MealieShoppingListCoordinator coordinator, str shopping_list_id)
Definition: todo.py:101
TodoItem _convert_api_item(ShoppingItem item)
Definition: todo.py:30
None async_setup_entry(HomeAssistant hass, MealieConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: todo.py:49