mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 12:47:08 +00:00
Add complete item intent function for todo component (#127806)
* add complete item intent * fix error and add tests * fix merge conflict * improve error tests * improve error tests * add response_key * add check for non completed --------- Co-authored-by: Michael Hansen <mike@rhasspy.org>
This commit is contained in:
parent
1b15df3075
commit
62b6be900f
@ -11,11 +11,13 @@ from . import TodoItem, TodoItemStatus, TodoListEntity
|
||||
from .const import DATA_COMPONENT, DOMAIN
|
||||
|
||||
INTENT_LIST_ADD_ITEM = "HassListAddItem"
|
||||
INTENT_LIST_COMPLETE_ITEM = "HassListCompleteItem"
|
||||
|
||||
|
||||
async def async_setup_intents(hass: HomeAssistant) -> None:
|
||||
"""Set up the todo intents."""
|
||||
intent.async_register(hass, ListAddItemIntent())
|
||||
intent.async_register(hass, ListCompleteItemIntent())
|
||||
|
||||
|
||||
class ListAddItemIntent(intent.IntentHandler):
|
||||
@ -53,14 +55,92 @@ class ListAddItemIntent(intent.IntentHandler):
|
||||
match_result.states[0].entity_id
|
||||
)
|
||||
if target_list is None:
|
||||
raise intent.IntentHandleError(f"No to-do list: {list_name}")
|
||||
raise intent.IntentHandleError(
|
||||
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)
|
||||
)
|
||||
|
||||
response = intent_obj.create_response()
|
||||
response: intent.IntentResponse = intent_obj.create_response()
|
||||
response.response_type = intent.IntentResponseType.ACTION_DONE
|
||||
response.async_set_results(
|
||||
[
|
||||
intent.IntentResponseTarget(
|
||||
type=intent.IntentResponseTargetType.ENTITY,
|
||||
name=list_name,
|
||||
id=match_result.states[0].entity_id,
|
||||
)
|
||||
]
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
class ListCompleteItemIntent(intent.IntentHandler):
|
||||
"""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"
|
||||
)
|
||||
|
||||
# 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)
|
||||
and todo_item.status == TodoItemStatus.NEEDS_ACTION
|
||||
):
|
||||
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"
|
||||
)
|
||||
|
||||
# Mark as completed
|
||||
await target_list.async_update_todo_item(
|
||||
TodoItem(
|
||||
uid=matching_item.uid,
|
||||
summary=matching_item.summary,
|
||||
status=TodoItemStatus.COMPLETED,
|
||||
)
|
||||
)
|
||||
|
||||
response: intent.IntentResponse = intent_obj.create_response()
|
||||
response.response_type = intent.IntentResponseType.ACTION_DONE
|
||||
response.async_set_results(
|
||||
[
|
||||
|
@ -34,6 +34,13 @@ class MockTodoListEntity(TodoListEntity):
|
||||
"""Delete an item in the To-do list."""
|
||||
self._attr_todo_items = [item for item in self.items if item.uid not in uids]
|
||||
|
||||
async def async_update_todo_item(self, item: TodoItem) -> None:
|
||||
"""Update an item in the To-do list."""
|
||||
for idx, existing_item in enumerate(self.items):
|
||||
if existing_item.uid == item.uid:
|
||||
self._attr_todo_items[idx] = item
|
||||
break
|
||||
|
||||
|
||||
async def create_mock_platform(
|
||||
hass: HomeAssistant,
|
||||
|
@ -1,5 +1,7 @@
|
||||
"""Tests for the todo intents."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import conversation
|
||||
@ -7,10 +9,12 @@ from homeassistant.components.homeassistant.exposed_entities import async_expose
|
||||
from homeassistant.components.todo import (
|
||||
ATTR_ITEM,
|
||||
DOMAIN,
|
||||
TodoItem,
|
||||
TodoItemStatus,
|
||||
TodoListEntity,
|
||||
intent as todo_intent,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import intent
|
||||
@ -18,6 +22,7 @@ from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import MockTodoListEntity, create_mock_platform
|
||||
|
||||
from tests.common import async_mock_service
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
|
||||
@ -174,3 +179,114 @@ async def test_add_item_intent_errors(
|
||||
},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
|
||||
|
||||
async def test_complete_item_intent(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test the complete item intent."""
|
||||
entity1 = MockTodoListEntity(
|
||||
[
|
||||
TodoItem(summary="beer", uid="1", status=TodoItemStatus.NEEDS_ACTION),
|
||||
TodoItem(summary="wine", uid="2", status=TodoItemStatus.NEEDS_ACTION),
|
||||
]
|
||||
)
|
||||
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) == 2
|
||||
assert entity1.items[0].status == TodoItemStatus.NEEDS_ACTION
|
||||
|
||||
# Complete item
|
||||
async_mock_service(hass, DOMAIN, todo_intent.INTENT_LIST_COMPLETE_ITEM)
|
||||
response = await intent.async_handle(
|
||||
hass,
|
||||
DOMAIN,
|
||||
todo_intent.INTENT_LIST_COMPLETE_ITEM,
|
||||
{ATTR_ITEM: {"value": "beer"}, ATTR_NAME: {"value": "list 1"}},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
assert response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||
|
||||
assert len(entity1.items) == 2
|
||||
assert entity1.items[0].status == TodoItemStatus.COMPLETED
|
||||
|
||||
|
||||
async def test_complete_item_intent_errors(
|
||||
hass: HomeAssistant,
|
||||
test_entity: TodoListEntity,
|
||||
) -> None:
|
||||
"""Test errors with the complete 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 complete item in list that does not exist
|
||||
with pytest.raises(intent.MatchFailedError):
|
||||
await intent.async_handle(
|
||||
hass,
|
||||
"test",
|
||||
todo_intent.INTENT_LIST_COMPLETE_ITEM,
|
||||
{
|
||||
ATTR_ITEM: {"value": "wine"},
|
||||
ATTR_NAME: {"value": "This list does not exist"},
|
||||
},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
|
||||
# Try to complete item that does not exist
|
||||
with pytest.raises(intent.IntentHandleError):
|
||||
await intent.async_handle(
|
||||
hass,
|
||||
"test",
|
||||
todo_intent.INTENT_LIST_COMPLETE_ITEM,
|
||||
{ATTR_ITEM: {"value": "bread"}, ATTR_NAME: {"value": "list 1"}},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
|
||||
# Item is already completed
|
||||
with pytest.raises(intent.IntentHandleError):
|
||||
await intent.async_handle(
|
||||
hass,
|
||||
"test",
|
||||
todo_intent.INTENT_LIST_COMPLETE_ITEM,
|
||||
{ATTR_ITEM: {"value": "beer"}, ATTR_NAME: {"value": "list 1"}},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
|
||||
|
||||
async def test_complete_item_intent_ha_errors(
|
||||
hass: HomeAssistant,
|
||||
test_entity: TodoListEntity,
|
||||
) -> None:
|
||||
"""Test error handling of HA errors with the complete item intent."""
|
||||
test_entity._attr_name = "List 1"
|
||||
test_entity.entity_id = "todo.list_1"
|
||||
await create_mock_platform(hass, [test_entity])
|
||||
|
||||
# Mock the get_entity method to return None
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.helpers.entity_component.EntityComponent.get_entity",
|
||||
return_value=None,
|
||||
),
|
||||
pytest.raises(intent.IntentHandleError),
|
||||
):
|
||||
await intent.async_handle(
|
||||
hass,
|
||||
DOMAIN,
|
||||
todo_intent.INTENT_LIST_COMPLETE_ITEM,
|
||||
{ATTR_ITEM: {"value": "wine"}, ATTR_NAME: {"value": "List 1"}},
|
||||
assistant=conversation.DOMAIN,
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user