mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +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
|
from .const import DATA_COMPONENT, DOMAIN
|
||||||
|
|
||||||
INTENT_LIST_ADD_ITEM = "HassListAddItem"
|
INTENT_LIST_ADD_ITEM = "HassListAddItem"
|
||||||
|
INTENT_LIST_COMPLETE_ITEM = "HassListCompleteItem"
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_intents(hass: HomeAssistant) -> None:
|
async def async_setup_intents(hass: HomeAssistant) -> None:
|
||||||
"""Set up the todo intents."""
|
"""Set up the todo intents."""
|
||||||
intent.async_register(hass, ListAddItemIntent())
|
intent.async_register(hass, ListAddItemIntent())
|
||||||
|
intent.async_register(hass, ListCompleteItemIntent())
|
||||||
|
|
||||||
|
|
||||||
class ListAddItemIntent(intent.IntentHandler):
|
class ListAddItemIntent(intent.IntentHandler):
|
||||||
@ -53,14 +55,92 @@ class ListAddItemIntent(intent.IntentHandler):
|
|||||||
match_result.states[0].entity_id
|
match_result.states[0].entity_id
|
||||||
)
|
)
|
||||||
if target_list is None:
|
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
|
# Add to list
|
||||||
await target_list.async_create_todo_item(
|
await target_list.async_create_todo_item(
|
||||||
TodoItem(summary=item, status=TodoItemStatus.NEEDS_ACTION)
|
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.response_type = intent.IntentResponseType.ACTION_DONE
|
||||||
response.async_set_results(
|
response.async_set_results(
|
||||||
[
|
[
|
||||||
|
@ -34,6 +34,13 @@ class MockTodoListEntity(TodoListEntity):
|
|||||||
"""Delete an item in the To-do list."""
|
"""Delete an item in the To-do list."""
|
||||||
self._attr_todo_items = [item for item in self.items if item.uid not in uids]
|
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(
|
async def create_mock_platform(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
"""Tests for the todo intents."""
|
"""Tests for the todo intents."""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components import conversation
|
from homeassistant.components import conversation
|
||||||
@ -7,10 +9,12 @@ from homeassistant.components.homeassistant.exposed_entities import async_expose
|
|||||||
from homeassistant.components.todo import (
|
from homeassistant.components.todo import (
|
||||||
ATTR_ITEM,
|
ATTR_ITEM,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
TodoItem,
|
||||||
TodoItemStatus,
|
TodoItemStatus,
|
||||||
TodoListEntity,
|
TodoListEntity,
|
||||||
intent as todo_intent,
|
intent as todo_intent,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.const import ATTR_NAME
|
from homeassistant.const import ATTR_NAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import intent
|
from homeassistant.helpers import intent
|
||||||
@ -18,6 +22,7 @@ from homeassistant.setup import async_setup_component
|
|||||||
|
|
||||||
from . import MockTodoListEntity, create_mock_platform
|
from . import MockTodoListEntity, create_mock_platform
|
||||||
|
|
||||||
|
from tests.common import async_mock_service
|
||||||
from tests.typing import WebSocketGenerator
|
from tests.typing import WebSocketGenerator
|
||||||
|
|
||||||
|
|
||||||
@ -174,3 +179,114 @@ async def test_add_item_intent_errors(
|
|||||||
},
|
},
|
||||||
assistant=conversation.DOMAIN,
|
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