From be8507f8706e22c23f241de45bd37f2f0a022639 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 14 Nov 2023 12:00:30 -0600 Subject: [PATCH] Add HassListAddItem intent (#103716) * Add HassListAddItem intent * Add missing list test --- homeassistant/components/todo/intent.py | 54 +++++++++++++++++ tests/components/todo/test_init.py | 81 +++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 homeassistant/components/todo/intent.py diff --git a/homeassistant/components/todo/intent.py b/homeassistant/components/todo/intent.py new file mode 100644 index 00000000000..ba3545d8dfd --- /dev/null +++ b/homeassistant/components/todo/intent.py @@ -0,0 +1,54 @@ +"""Intents for the todo integration.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_component import EntityComponent + +from . import DOMAIN, TodoItem, TodoListEntity + +INTENT_LIST_ADD_ITEM = "HassListAddItem" + + +async def async_setup_intents(hass: HomeAssistant) -> None: + """Set up the todo intents.""" + intent.async_register(hass, ListAddItemIntent()) + + +class ListAddItemIntent(intent.IntentHandler): + """Handle ListAddItem intents.""" + + intent_type = INTENT_LIST_ADD_ITEM + slot_schema = {"item": cv.string, "name": cv.string} + + async def async_handle(self, intent_obj: intent.Intent): + """Handle the intent.""" + hass = intent_obj.hass + + slots = self.async_validate_slots(intent_obj.slots) + item = slots["item"]["value"] + list_name = slots["name"]["value"] + + component: EntityComponent[TodoListEntity] = hass.data[DOMAIN] + target_list: TodoListEntity | None = None + + # Find matching list + for list_state in intent.async_match_states( + hass, name=list_name, domains=[DOMAIN] + ): + target_list = component.get_entity(list_state.entity_id) + if target_list is not None: + break + + if target_list is None: + raise intent.IntentHandleError(f"No to-do list: {list_name}") + + assert target_list is not None + + # Add to list + await target_list.async_create_todo_item(TodoItem(item)) + + response = intent_obj.create_response() + response.response_type = intent.IntentResponseType.ACTION_DONE + return response diff --git a/tests/components/todo/test_init.py b/tests/components/todo/test_init.py index 3e84049efa8..33f9af2b0c5 100644 --- a/tests/components/todo/test_init.py +++ b/tests/components/todo/test_init.py @@ -13,11 +13,13 @@ from homeassistant.components.todo import ( TodoItemStatus, TodoListEntity, TodoListEntityFeature, + intent as todo_intent, ) from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigFlow from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import intent from homeassistant.helpers.entity_platform import AddEntitiesCallback from tests.common import ( @@ -37,6 +39,18 @@ class MockFlow(ConfigFlow): """Test flow.""" +class MockTodoListEntity(TodoListEntity): + """Test todo list entity.""" + + def __init__(self) -> None: + """Initialize entity.""" + self.items: list[TodoItem] = [] + + async def async_create_todo_item(self, item: TodoItem) -> None: + """Add an item to the To-do list.""" + self.items.append(item) + + @pytest.fixture(autouse=True) def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: """Mock config flow.""" @@ -737,3 +751,70 @@ async def test_move_item_unsupported( resp = await client.receive_json() assert resp.get("id") == 1 assert resp.get("error", {}).get("code") == "not_supported" + + +async def test_add_item_intent( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test adding items to lists using an intent.""" + await todo_intent.async_setup_intents(hass) + + entity1 = MockTodoListEntity() + entity1._attr_name = "List 1" + entity1.entity_id = "todo.list_1" + + entity2 = MockTodoListEntity() + entity2._attr_name = "List 2" + entity2.entity_id = "todo.list_2" + + await create_mock_platform(hass, [entity1, entity2]) + + # Add to first list + response = await intent.async_handle( + hass, + "test", + todo_intent.INTENT_LIST_ADD_ITEM, + {"item": {"value": "beer"}, "name": {"value": "list 1"}}, + ) + assert response.response_type == intent.IntentResponseType.ACTION_DONE + + assert len(entity1.items) == 1 + assert len(entity2.items) == 0 + assert entity1.items[0].summary == "beer" + entity1.items.clear() + + # Add to second list + response = await intent.async_handle( + hass, + "test", + todo_intent.INTENT_LIST_ADD_ITEM, + {"item": {"value": "cheese"}, "name": {"value": "List 2"}}, + ) + assert response.response_type == intent.IntentResponseType.ACTION_DONE + + assert len(entity1.items) == 0 + assert len(entity2.items) == 1 + assert entity2.items[0].summary == "cheese" + + # List name is case insensitive + response = await intent.async_handle( + hass, + "test", + todo_intent.INTENT_LIST_ADD_ITEM, + {"item": {"value": "wine"}, "name": {"value": "lIST 2"}}, + ) + assert response.response_type == intent.IntentResponseType.ACTION_DONE + + assert len(entity1.items) == 0 + assert len(entity2.items) == 2 + assert entity2.items[1].summary == "wine" + + # Missing list + with pytest.raises(intent.IntentHandleError): + await intent.async_handle( + hass, + "test", + todo_intent.INTENT_LIST_ADD_ITEM, + {"item": {"value": "wine"}, "name": {"value": "This list does not exist"}}, + )