1
0
mirror of https://github.com/home-assistant/core.git synced 2026-02-15 07:36:16 +00:00

Add remove item intent for todo component (#152922)

This commit is contained in:
Damien Sorel
2026-02-13 19:38:22 +01:00
committed by GitHub
parent 815c708d19
commit 23e88a24f0
3 changed files with 134 additions and 57 deletions

View File

@@ -12,25 +12,29 @@ from .const import DATA_COMPONENT, DOMAIN
INTENT_LIST_ADD_ITEM = "HassListAddItem"
INTENT_LIST_COMPLETE_ITEM = "HassListCompleteItem"
INTENT_LIST_REMOVE_ITEM = "HassListRemoveItem"
async def async_setup_intents(hass: HomeAssistant) -> None:
"""Set up the todo intents."""
intent.async_register(hass, ListAddItemIntent())
intent.async_register(hass, ListCompleteItemIntent())
"""Set up the todo intent handlers."""
intent.async_register(hass, ListAddItemIntentHandler())
intent.async_register(hass, ListCompleteItemIntentHandler())
intent.async_register(hass, ListRemoveItemIntentHandler())
class ListAddItemIntent(intent.IntentHandler):
"""Handle ListAddItem intents."""
class ListBaseIntentHandler(intent.IntentHandler):
"""Base class for toto intent handlers."""
intent_type = INTENT_LIST_ADD_ITEM
description = "Add item to a todo list"
slot_schema = {
vol.Required("item"): intent.non_empty_string,
vol.Required("name"): intent.non_empty_string,
}
platforms = {DOMAIN}
async def _async_do_handle(self, target_list: TodoListEntity, item: str) -> None:
"""Execute action specific to this intent handler."""
raise NotImplementedError
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
"""Handle the intent."""
hass = intent_obj.hass
@@ -59,62 +63,46 @@ class ListAddItemIntent(intent.IntentHandler):
f"No to-do list: {list_name}", "list_not_found"
)
# Add to list
await target_list.async_create_todo_item(
TodoItem(summary=item, status=TodoItemStatus.NEEDS_ACTION)
)
# Execute specific action
await self._async_do_handle(target_list, item)
# Build intent response
response: intent.IntentResponse = intent_obj.create_response()
response.async_set_results(
[
intent.IntentResponseTarget(
type=intent.IntentResponseTargetType.ENTITY,
name=list_name,
id=match_result.states[0].entity_id,
id=target_list.entity_id,
)
]
)
return response
class ListCompleteItemIntent(intent.IntentHandler):
class ListAddItemIntentHandler(ListBaseIntentHandler):
"""Handle ListAddItem intents."""
intent_type = INTENT_LIST_ADD_ITEM
description = "Add item to a todo list"
async def _async_do_handle(self, target_list: TodoListEntity, item: str) -> None:
"""Execute action specific to this intent handler."""
# Add to list
await target_list.async_create_todo_item(
TodoItem(summary=item, status=TodoItemStatus.NEEDS_ACTION)
)
class ListCompleteItemIntentHandler(ListBaseIntentHandler):
"""Handle ListCompleteItem intents."""
intent_type = INTENT_LIST_COMPLETE_ITEM
description = "Complete item on a todo list"
slot_schema = {
vol.Required("item"): intent.non_empty_string,
vol.Required("name"): intent.non_empty_string,
}
platforms = {DOMAIN}
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
"""Handle the intent."""
hass = intent_obj.hass
slots = self.async_validate_slots(intent_obj.slots)
item = slots["item"]["value"]
list_name = slots["name"]["value"]
target_list: TodoListEntity | None = None
# Find matching list
match_constraints = intent.MatchTargetsConstraints(
name=list_name, domains=[DOMAIN], assistant=intent_obj.assistant
)
match_result = intent.async_match_targets(hass, match_constraints)
if not match_result.is_match:
raise intent.MatchFailedError(
result=match_result, constraints=match_constraints
)
target_list = hass.data[DATA_COMPONENT].get_entity(
match_result.states[0].entity_id
)
if target_list is None:
raise intent.IntentHandleError(
f"No to-do list: {list_name}", "list_not_found"
)
async def _async_do_handle(self, target_list: TodoListEntity, item: str) -> None:
"""Execute action specific to this intent handler."""
# Find item in list
matching_item = None
@@ -139,14 +127,26 @@ class ListCompleteItemIntent(intent.IntentHandler):
)
)
response: intent.IntentResponse = intent_obj.create_response()
response.async_set_results(
[
intent.IntentResponseTarget(
type=intent.IntentResponseTargetType.ENTITY,
name=list_name,
id=match_result.states[0].entity_id,
)
]
)
return response
class ListRemoveItemIntentHandler(ListBaseIntentHandler):
"""Handle LisRemoveItemIntent intents."""
intent_type = INTENT_LIST_REMOVE_ITEM
description = "Remove one or more items from a todo list"
async def _async_do_handle(self, target_list: TodoListEntity, item: str) -> None:
"""Execute action specific to this intent handler."""
# Find item in list
matching_item = None
for todo_item in target_list.todo_items or ():
if item in (todo_item.uid, todo_item.summary):
matching_item = todo_item
break
if not matching_item or not matching_item.uid:
raise intent.IntentHandleError(
f"Item '{item}' not found on list", "item_not_found"
)
# Remove items
await target_list.async_delete_todo_items(uids=[matching_item.uid])

View File

@@ -439,7 +439,7 @@ async def test_todo_add_item_fr(
with (
patch.object(hass.config, "language", "fr"),
patch(
"homeassistant.components.todo.intent.ListAddItemIntent.async_handle",
"homeassistant.components.todo.intent.ListAddItemIntentHandler.async_handle",
return_value=intent.IntentResponse(hass.config.language),
) as mock_handle,
):

View File

@@ -290,3 +290,80 @@ async def test_complete_item_intent_ha_errors(
{ATTR_ITEM: {"value": "wine"}, ATTR_NAME: {"value": "List 1"}},
assistant=conversation.DOMAIN,
)
async def test_remove_item_intent(
hass: HomeAssistant,
) -> None:
"""Test the remove item intent."""
entity1 = MockTodoListEntity(
[
TodoItem(summary="beer", uid="1", status=TodoItemStatus.NEEDS_ACTION),
TodoItem(summary="wine", uid="2", status=TodoItemStatus.NEEDS_ACTION),
TodoItem(summary="beer", uid="3", status=TodoItemStatus.COMPLETED),
]
)
entity1._attr_name = "List 1"
entity1.entity_id = "todo.list_1"
# Add entities to hass
config_entry = await create_mock_platform(hass, [entity1])
assert config_entry.state is ConfigEntryState.LOADED
assert len(entity1.items) == 3
# Remove item
async_mock_service(hass, DOMAIN, todo_intent.INTENT_LIST_REMOVE_ITEM)
response = await intent.async_handle(
hass,
DOMAIN,
todo_intent.INTENT_LIST_REMOVE_ITEM,
{ATTR_ITEM: {"value": "beer"}, ATTR_NAME: {"value": "list 1"}},
assistant=conversation.DOMAIN,
)
assert response.response_type == intent.IntentResponseType.ACTION_DONE
# only the first matching item has been removed
assert len(entity1.items) == 2
assert entity1.items[0].uid == "2"
assert entity1.items[1].uid == "3"
async def test_remove_item_intent_errors(
hass: HomeAssistant,
test_entity: TodoListEntity,
) -> None:
"""Test errors with the remove item intent."""
entity1 = MockTodoListEntity(
[
TodoItem(summary="beer", uid="1", status=TodoItemStatus.COMPLETED),
]
)
entity1._attr_name = "List 1"
entity1.entity_id = "todo.list_1"
# Add entities to hass
await create_mock_platform(hass, [entity1])
# Try to remove item in list that does not exist
with pytest.raises(intent.MatchFailedError):
await intent.async_handle(
hass,
"test",
todo_intent.INTENT_LIST_REMOVE_ITEM,
{
ATTR_ITEM: {"value": "wine"},
ATTR_NAME: {"value": "This list does not exist"},
},
assistant=conversation.DOMAIN,
)
# Try to remove item that does not exist
with pytest.raises(intent.IntentHandleError):
await intent.async_handle(
hass,
"test",
todo_intent.INTENT_LIST_REMOVE_ITEM,
{ATTR_ITEM: {"value": "bread"}, ATTR_NAME: {"value": "list 1"}},
assistant=conversation.DOMAIN,
)