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:
@@ -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])
|
||||
|
||||
@@ -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,
|
||||
):
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user