mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 15:17:35 +00:00
Dynamically create and delete todo lists in mealie (#121710)
This commit is contained in:
parent
c223709c7c
commit
73475aa675
@ -75,8 +75,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: MealieConfigEntry) -> bo
|
|||||||
shoppinglist_coordinator = MealieShoppingListCoordinator(hass, client)
|
shoppinglist_coordinator = MealieShoppingListCoordinator(hass, client)
|
||||||
|
|
||||||
await mealplan_coordinator.async_config_entry_first_refresh()
|
await mealplan_coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
await shoppinglist_coordinator.async_get_shopping_lists()
|
|
||||||
await shoppinglist_coordinator.async_config_entry_first_refresh()
|
await shoppinglist_coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
entry.runtime_data = MealieData(
|
entry.runtime_data = MealieData(
|
||||||
|
@ -96,8 +96,16 @@ class MealieMealplanCoordinator(
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ShoppingListData:
|
||||||
|
"""Data class for shopping list data."""
|
||||||
|
|
||||||
|
shopping_list: ShoppingList
|
||||||
|
items: list[ShoppingItem]
|
||||||
|
|
||||||
|
|
||||||
class MealieShoppingListCoordinator(
|
class MealieShoppingListCoordinator(
|
||||||
MealieDataUpdateCoordinator[dict[str, list[ShoppingItem]]]
|
MealieDataUpdateCoordinator[dict[str, ShoppingListData]]
|
||||||
):
|
):
|
||||||
"""Class to manage fetching Mealie Shopping list data."""
|
"""Class to manage fetching Mealie Shopping list data."""
|
||||||
|
|
||||||
@ -109,36 +117,25 @@ class MealieShoppingListCoordinator(
|
|||||||
client=client,
|
client=client,
|
||||||
update_interval=timedelta(minutes=5),
|
update_interval=timedelta(minutes=5),
|
||||||
)
|
)
|
||||||
self.shopping_lists: list[ShoppingList]
|
|
||||||
|
|
||||||
async def async_get_shopping_lists(self) -> list[ShoppingList]:
|
|
||||||
"""Return shopping lists."""
|
|
||||||
try:
|
|
||||||
self.shopping_lists = (await self.client.get_shopping_lists()).items
|
|
||||||
except MealieAuthenticationError as error:
|
|
||||||
raise ConfigEntryAuthFailed from error
|
|
||||||
except MealieConnectionError as error:
|
|
||||||
raise UpdateFailed(error) from error
|
|
||||||
return self.shopping_lists
|
|
||||||
|
|
||||||
async def _async_update_data(
|
async def _async_update_data(
|
||||||
self,
|
self,
|
||||||
) -> dict[str, list[ShoppingItem]]:
|
) -> dict[str, ShoppingListData]:
|
||||||
shopping_list_items: dict[str, list[ShoppingItem]] = {}
|
shopping_list_items = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for shopping_list in self.shopping_lists:
|
shopping_lists = (await self.client.get_shopping_lists()).items
|
||||||
|
for shopping_list in shopping_lists:
|
||||||
shopping_list_id = shopping_list.list_id
|
shopping_list_id = shopping_list.list_id
|
||||||
|
|
||||||
shopping_items = (
|
shopping_items = (
|
||||||
await self.client.get_shopping_items(shopping_list_id)
|
await self.client.get_shopping_items(shopping_list_id)
|
||||||
).items
|
).items
|
||||||
|
|
||||||
shopping_list_items[shopping_list_id] = shopping_items
|
shopping_list_items[shopping_list_id] = ShoppingListData(
|
||||||
|
shopping_list=shopping_list, items=shopping_items
|
||||||
|
)
|
||||||
except MealieAuthenticationError as error:
|
except MealieAuthenticationError as error:
|
||||||
raise ConfigEntryAuthFailed from error
|
raise ConfigEntryAuthFailed from error
|
||||||
except MealieConnectionError as error:
|
except MealieConnectionError as error:
|
||||||
raise UpdateFailed(error) from error
|
raise UpdateFailed(error) from error
|
||||||
|
|
||||||
return shopping_list_items
|
return shopping_list_items
|
||||||
|
@ -25,7 +25,7 @@ async def async_get_config_entry_diagnostics(
|
|||||||
for entry_type, mealplans in data.mealplan_coordinator.data.items()
|
for entry_type, mealplans in data.mealplan_coordinator.data.items()
|
||||||
},
|
},
|
||||||
"shoppinglist": {
|
"shoppinglist": {
|
||||||
list_id: [asdict(item) for item in shopping_list]
|
list_id: asdict(shopping_list)
|
||||||
for list_id, shopping_list in data.shoppinglist_coordinator.data.items()
|
for list_id, shopping_list in data.shoppinglist_coordinator.data.items()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
from aiomealie import MealieError, MutateShoppingItem, ShoppingItem, ShoppingList
|
from aiomealie import MealieError, MutateShoppingItem, ShoppingItem, ShoppingList
|
||||||
|
|
||||||
from homeassistant.components.todo import (
|
from homeassistant.components.todo import (
|
||||||
|
DOMAIN as TODO_DOMAIN,
|
||||||
TodoItem,
|
TodoItem,
|
||||||
TodoItemStatus,
|
TodoItemStatus,
|
||||||
TodoListEntity,
|
TodoListEntity,
|
||||||
@ -12,6 +13,7 @@ from homeassistant.components.todo import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
@ -48,10 +50,36 @@ async def async_setup_entry(
|
|||||||
"""Set up the todo platform for entity."""
|
"""Set up the todo platform for entity."""
|
||||||
coordinator = entry.runtime_data.shoppinglist_coordinator
|
coordinator = entry.runtime_data.shoppinglist_coordinator
|
||||||
|
|
||||||
async_add_entities(
|
added_lists: set[str] = set()
|
||||||
MealieShoppingListTodoListEntity(coordinator, shopping_list)
|
|
||||||
for shopping_list in coordinator.shopping_lists
|
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):
|
class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity):
|
||||||
@ -69,17 +97,22 @@ class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity):
|
|||||||
coordinator: MealieShoppingListCoordinator
|
coordinator: MealieShoppingListCoordinator
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, coordinator: MealieShoppingListCoordinator, shopping_list: ShoppingList
|
self, coordinator: MealieShoppingListCoordinator, shopping_list_id: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create the todo entity."""
|
"""Create the todo entity."""
|
||||||
super().__init__(coordinator, shopping_list.list_id)
|
super().__init__(coordinator, shopping_list_id)
|
||||||
self._shopping_list = shopping_list
|
self._shopping_list_id = shopping_list_id
|
||||||
self._attr_name = shopping_list.name
|
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
|
@property
|
||||||
def shopping_items(self) -> list[ShoppingItem]:
|
def shopping_items(self) -> list[ShoppingItem]:
|
||||||
"""Get the shopping items for this list."""
|
"""Get the shopping items for this list."""
|
||||||
return self.coordinator.data[self._shopping_list.list_id]
|
return self.coordinator.data[self._shopping_list_id].items
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def todo_items(self) -> list[TodoItem] | None:
|
def todo_items(self) -> list[TodoItem] | None:
|
||||||
@ -93,7 +126,7 @@ class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity):
|
|||||||
position = self.shopping_items[-1].position + 1
|
position = self.shopping_items[-1].position + 1
|
||||||
|
|
||||||
new_shopping_item = MutateShoppingItem(
|
new_shopping_item = MutateShoppingItem(
|
||||||
list_id=self._shopping_list.list_id,
|
list_id=self._shopping_list_id,
|
||||||
note=item.summary.strip() if item.summary else item.summary,
|
note=item.summary.strip() if item.summary else item.summary,
|
||||||
position=position,
|
position=position,
|
||||||
)
|
)
|
||||||
@ -104,7 +137,7 @@ class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity):
|
|||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="add_item_error",
|
translation_key="add_item_error",
|
||||||
translation_placeholders={
|
translation_placeholders={
|
||||||
"shopping_list_name": self._shopping_list.name
|
"shopping_list_name": self.shopping_list.name
|
||||||
},
|
},
|
||||||
) from exception
|
) from exception
|
||||||
finally:
|
finally:
|
||||||
@ -164,7 +197,7 @@ class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity):
|
|||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="update_item_error",
|
translation_key="update_item_error",
|
||||||
translation_placeholders={
|
translation_placeholders={
|
||||||
"shopping_list_name": self._shopping_list.name
|
"shopping_list_name": self.shopping_list.name
|
||||||
},
|
},
|
||||||
) from exception
|
) from exception
|
||||||
finally:
|
finally:
|
||||||
@ -180,7 +213,7 @@ class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity):
|
|||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="delete_item_error",
|
translation_key="delete_item_error",
|
||||||
translation_placeholders={
|
translation_placeholders={
|
||||||
"shopping_list_name": self._shopping_list.name
|
"shopping_list_name": self.shopping_list.name
|
||||||
},
|
},
|
||||||
) from exception
|
) from exception
|
||||||
finally:
|
finally:
|
||||||
@ -238,6 +271,4 @@ class MealieShoppingListTodoListEntity(MealieEntity, TodoListEntity):
|
|||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return False if shopping list no longer available."""
|
"""Return False if shopping list no longer available."""
|
||||||
return (
|
return super().available and self._shopping_list_id in self.coordinator.data
|
||||||
super().available and self._shopping_list.list_id in self.coordinator.data
|
|
||||||
)
|
|
||||||
|
@ -350,7 +350,8 @@
|
|||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
'shoppinglist': dict({
|
'shoppinglist': dict({
|
||||||
'27edbaab-2ec6-441f-8490-0283ea77585f': list([
|
'27edbaab-2ec6-441f-8490-0283ea77585f': dict({
|
||||||
|
'items': list([
|
||||||
dict({
|
dict({
|
||||||
'checked': False,
|
'checked': False,
|
||||||
'disable_amount': True,
|
'disable_amount': True,
|
||||||
@ -394,7 +395,13 @@
|
|||||||
'unit_id': None,
|
'unit_id': None,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
'e9d78ff2-4b23-4b77-a3a8-464827100b46': list([
|
'shopping_list': dict({
|
||||||
|
'list_id': '27edbaab-2ec6-441f-8490-0283ea77585f',
|
||||||
|
'name': 'Supermarket',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'e9d78ff2-4b23-4b77-a3a8-464827100b46': dict({
|
||||||
|
'items': list([
|
||||||
dict({
|
dict({
|
||||||
'checked': False,
|
'checked': False,
|
||||||
'disable_amount': True,
|
'disable_amount': True,
|
||||||
@ -438,7 +445,13 @@
|
|||||||
'unit_id': None,
|
'unit_id': None,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
'f8438635-8211-4be8-80d0-0aa42e37a5f2': list([
|
'shopping_list': dict({
|
||||||
|
'list_id': 'e9d78ff2-4b23-4b77-a3a8-464827100b46',
|
||||||
|
'name': 'Freezer',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'f8438635-8211-4be8-80d0-0aa42e37a5f2': dict({
|
||||||
|
'items': list([
|
||||||
dict({
|
dict({
|
||||||
'checked': False,
|
'checked': False,
|
||||||
'disable_amount': True,
|
'disable_amount': True,
|
||||||
@ -482,6 +495,11 @@
|
|||||||
'unit_id': None,
|
'unit_id': None,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
|
'shopping_list': dict({
|
||||||
|
'list_id': 'f8438635-8211-4be8-80d0-0aa42e37a5f2',
|
||||||
|
'name': 'Special groceries',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
@ -135,25 +135,3 @@ async def test_shoppingitems_initialization_failure(
|
|||||||
await setup_integration(hass, mock_config_entry)
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
assert mock_config_entry.state is state
|
assert mock_config_entry.state is state
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
("exc", "state"),
|
|
||||||
[
|
|
||||||
(MealieConnectionError, ConfigEntryState.SETUP_ERROR),
|
|
||||||
(MealieAuthenticationError, ConfigEntryState.SETUP_ERROR),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def test_shoppinglists_initialization_failure(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
mock_mealie_client: AsyncMock,
|
|
||||||
mock_config_entry: MockConfigEntry,
|
|
||||||
exc: Exception,
|
|
||||||
state: ConfigEntryState,
|
|
||||||
) -> None:
|
|
||||||
"""Test initialization failure."""
|
|
||||||
mock_mealie_client.get_shopping_lists.side_effect = exc
|
|
||||||
|
|
||||||
await setup_integration(hass, mock_config_entry)
|
|
||||||
|
|
||||||
assert mock_config_entry.state is state
|
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
"""Tests for the Mealie todo."""
|
"""Tests for the Mealie todo."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from aiomealie import ShoppingListsResponse
|
||||||
from aiomealie.exceptions import MealieError
|
from aiomealie.exceptions import MealieError
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
import pytest
|
import pytest
|
||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.components.mealie import DOMAIN
|
||||||
from homeassistant.components.todo import (
|
from homeassistant.components.todo import (
|
||||||
ATTR_ITEM,
|
ATTR_ITEM,
|
||||||
ATTR_RENAME,
|
ATTR_RENAME,
|
||||||
@ -20,7 +24,12 @@ from homeassistant.helpers import entity_registry as er
|
|||||||
|
|
||||||
from . import setup_integration
|
from . import setup_integration
|
||||||
|
|
||||||
from tests.common import MockConfigEntry, snapshot_platform
|
from tests.common import (
|
||||||
|
MockConfigEntry,
|
||||||
|
async_fire_time_changed,
|
||||||
|
load_fixture,
|
||||||
|
snapshot_platform,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_entities(
|
async def test_entities(
|
||||||
@ -153,3 +162,37 @@ async def test_delete_todo_list_item_error(
|
|||||||
target={ATTR_ENTITY_ID: "todo.mealie_supermarket"},
|
target={ATTR_ENTITY_ID: "todo.mealie_supermarket"},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_runtime_management(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_mealie_client: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test for creating and deleting shopping lists."""
|
||||||
|
response = ShoppingListsResponse.from_json(
|
||||||
|
load_fixture("get_shopping_lists.json", DOMAIN)
|
||||||
|
).items
|
||||||
|
mock_mealie_client.get_shopping_lists.return_value = ShoppingListsResponse(
|
||||||
|
items=[response[0]]
|
||||||
|
)
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
assert hass.states.get("todo.mealie_supermarket") is not None
|
||||||
|
assert hass.states.get("todo.mealie_special_groceries") is None
|
||||||
|
|
||||||
|
mock_mealie_client.get_shopping_lists.return_value = ShoppingListsResponse(
|
||||||
|
items=response[0:2]
|
||||||
|
)
|
||||||
|
freezer.tick(timedelta(minutes=5))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert hass.states.get("todo.mealie_special_groceries") is not None
|
||||||
|
|
||||||
|
mock_mealie_client.get_shopping_lists.return_value = ShoppingListsResponse(
|
||||||
|
items=[response[0]]
|
||||||
|
)
|
||||||
|
freezer.tick(timedelta(minutes=5))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert hass.states.get("todo.mealie_special_groceries") is None
|
||||||
|
Loading…
x
Reference in New Issue
Block a user