Skip to content

Commit

Permalink
Add todo.remove_completed_items service call (home-assistant#104035)
Browse files Browse the repository at this point in the history
* Extend `remove_item` service by status

* update services.yaml

* Create own service

* add tests

* Update tests/components/todo/test_init.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
  • Loading branch information
edenhaus and MartinHjelmare authored Nov 20, 2023
1 parent 5527cbd commit 9d3f374
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 11 deletions.
17 changes: 17 additions & 0 deletions homeassistant/components/todo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
_async_get_todo_items,
supports_response=SupportsResponse.ONLY,
)
component.async_register_entity_service(
"remove_completed_items",
{},
_async_remove_completed_items,
required_features=[TodoListEntityFeature.DELETE_TODO_ITEM],
)

await component.async_setup(config)
return True
Expand Down Expand Up @@ -284,3 +290,14 @@ async def _async_get_todo_items(
if not (statuses := call.data.get("status")) or item.status in statuses
]
}


async def _async_remove_completed_items(entity: TodoListEntity, _: ServiceCall) -> None:
"""Remove all completed items from the To-do list."""
uids = [
item.uid
for item in entity.todo_items or ()
if item.status == TodoItemStatus.COMPLETED and item.uid
]
if uids:
await entity.async_delete_todo_items(uids=uids)
2 changes: 2 additions & 0 deletions homeassistant/components/todo/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,5 @@ remove_item:
required: true
selector:
text:

remove_completed_items:
4 changes: 4 additions & 0 deletions homeassistant/components/todo/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
}
}
},
"remove_completed_items": {
"name": "Remove all completed to-do list items",
"description": "Remove all to-do list items that have been completed."
},
"remove_item": {
"name": "Remove a to-do list item",
"description": "Remove an existing to-do list item by its name.",
Expand Down
84 changes: 73 additions & 11 deletions tests/components/todo/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,22 @@ class MockFlow(ConfigFlow):
class MockTodoListEntity(TodoListEntity):
"""Test todo list entity."""

def __init__(self) -> None:
def __init__(self, items: list[TodoItem] | None = None) -> None:
"""Initialize entity."""
self.items: list[TodoItem] = []
self._attr_todo_items = items or []

@property
def items(self) -> list[TodoItem]:
"""Return the 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."""
self.items.append(item)
self._attr_todo_items.append(item)

async def async_delete_todo_items(self, uids: list[str]) -> None:
"""Delete an item in the To-do list."""
self._attr_todo_items = [item for item in self.items if item.uid not in uids]


@pytest.fixture(autouse=True)
Expand Down Expand Up @@ -130,21 +139,22 @@ async def async_setup_entry_platform(
@pytest.fixture(name="test_entity")
def mock_test_entity() -> TodoListEntity:
"""Fixture that creates a test TodoList entity with mock service calls."""
entity1 = TodoListEntity()
entity1 = MockTodoListEntity(
[
TodoItem(summary="Item #1", uid="1", status=TodoItemStatus.NEEDS_ACTION),
TodoItem(summary="Item #2", uid="2", status=TodoItemStatus.COMPLETED),
]
)
entity1.entity_id = "todo.entity1"
entity1._attr_supported_features = (
TodoListEntityFeature.CREATE_TODO_ITEM
| TodoListEntityFeature.UPDATE_TODO_ITEM
| TodoListEntityFeature.DELETE_TODO_ITEM
| TodoListEntityFeature.MOVE_TODO_ITEM
)
entity1._attr_todo_items = [
TodoItem(summary="Item #1", uid="1", status=TodoItemStatus.NEEDS_ACTION),
TodoItem(summary="Item #2", uid="2", status=TodoItemStatus.COMPLETED),
]
entity1.async_create_todo_item = AsyncMock()
entity1.async_create_todo_item = AsyncMock(wraps=entity1.async_create_todo_item)
entity1.async_update_todo_item = AsyncMock()
entity1.async_delete_todo_items = AsyncMock()
entity1.async_delete_todo_items = AsyncMock(wraps=entity1.async_delete_todo_items)
entity1.async_move_todo_item = AsyncMock()
return entity1

Expand Down Expand Up @@ -763,12 +773,16 @@ async def test_move_todo_item_service_invalid_input(
"rename": "Updated item",
},
),
(
"remove_completed_items",
None,
),
],
)
async def test_unsupported_service(
hass: HomeAssistant,
service_name: str,
payload: dict[str, Any],
payload: dict[str, Any] | None,
) -> None:
"""Test a To-do list that does not support features."""

Expand Down Expand Up @@ -879,3 +893,51 @@ async def test_add_item_intent(
todo_intent.INTENT_LIST_ADD_ITEM,
{"item": {"value": "wine"}, "name": {"value": "This list does not exist"}},
)


async def test_remove_completed_items_service(
hass: HomeAssistant,
test_entity: TodoListEntity,
) -> None:
"""Test remove completed todo items service."""
await create_mock_platform(hass, [test_entity])

await hass.services.async_call(
DOMAIN,
"remove_completed_items",
target={"entity_id": "todo.entity1"},
blocking=True,
)

args = test_entity.async_delete_todo_items.call_args
assert args
assert args.kwargs.get("uids") == ["2"]

test_entity.async_delete_todo_items.reset_mock()

# calling service multiple times will not call the entity method
await hass.services.async_call(
DOMAIN,
"remove_completed_items",
target={"entity_id": "todo.entity1"},
blocking=True,
)
test_entity.async_delete_todo_items.assert_not_called()


async def test_remove_completed_items_service_raises(
hass: HomeAssistant,
test_entity: TodoListEntity,
) -> None:
"""Test removing all completed item from a To-do list that raises an error."""

await create_mock_platform(hass, [test_entity])

test_entity.async_delete_todo_items.side_effect = HomeAssistantError("Ooops")
with pytest.raises(HomeAssistantError, match="Ooops"):
await hass.services.async_call(
DOMAIN,
"remove_completed_items",
target={"entity_id": "todo.entity1"},
blocking=True,
)

0 comments on commit 9d3f374

Please sign in to comment.