Compare commits

...
Sign in to create a new pull request.

4 commits

Author SHA1 Message Date
Michael Hansen
a1c23b0235 Fix shopping list tests 2023-10-25 10:59:53 -05:00
Michael Hansen
4ba9358d5c Shopping list depends on intent/todo 2023-10-25 10:46:33 -05:00
Michael Hansen
800f816e9f Migrate HassShoppingListAddItem to todo 2023-10-25 10:46:33 -05:00
Michael Hansen
9b604b4a1c Add HassListAddItem intent 2023-10-25 10:46:33 -05:00
6 changed files with 149 additions and 25 deletions

View file

@ -1,38 +1,20 @@
"""Intents for the Shopping List integration."""
from __future__ import annotations
from homeassistant.core import HomeAssistant
from homeassistant.helpers import intent
import homeassistant.helpers.config_validation as cv
from . import DOMAIN, EVENT_SHOPPING_LIST_UPDATED
from . import DOMAIN
INTENT_ADD_ITEM = "HassShoppingListAddItem"
INTENT_LAST_ITEMS = "HassShoppingListLastItems"
async def async_setup_intents(hass):
async def async_setup_intents(hass: HomeAssistant) -> None:
"""Set up the Shopping List intents."""
intent.async_register(hass, AddItemIntent())
intent.async_register(hass, ListTopItemsIntent())
class AddItemIntent(intent.IntentHandler):
"""Handle AddItem intents."""
intent_type = INTENT_ADD_ITEM
slot_schema = {"item": cv.string}
async def async_handle(self, intent_obj: intent.Intent):
"""Handle the intent."""
slots = self.async_validate_slots(intent_obj.slots)
item = slots["item"]["value"]
await intent_obj.hass.data[DOMAIN].async_add(item)
response = intent_obj.create_response()
intent_obj.hass.bus.async_fire(EVENT_SHOPPING_LIST_UPDATED)
return response
class ListTopItemsIntent(intent.IntentHandler):
"""Handle AddItem intents."""

View file

@ -3,7 +3,7 @@
"name": "Shopping List",
"codeowners": [],
"config_flow": true,
"dependencies": ["http"],
"dependencies": ["http", "intent", "todo"],
"documentation": "https://www.home-assistant.io/integrations/shopping_list",
"iot_class": "local_push",
"quality_scale": "internal"

View file

@ -0,0 +1,73 @@
"""Intents for the todo integration."""
from __future__ import annotations
import voluptuous as vol
from homeassistant.components.shopping_list import EVENT_SHOPPING_LIST_UPDATED
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_ADD_ITEM = "HassShoppingListAddItem"
async def async_setup_intents(hass: HomeAssistant) -> None:
"""Set up the todo intents."""
intent.async_register(hass, AddItemIntent())
class AddItemIntent(intent.IntentHandler):
"""Handle AddItem intents."""
intent_type = INTENT_ADD_ITEM
slot_schema = {"item": cv.string, vol.Optional("list"): 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"]
component: EntityComponent[TodoListEntity] = hass.data[DOMAIN]
list_entities: list[TodoListEntity] = list(component.entities)
if not list_entities:
raise intent.IntentHandleError("No list entities")
target_list: TodoListEntity | None = None
if "list" in slots:
# Add to a list by name
list_name = slots["list"]["value"]
# Find matching list
matching_states = intent.async_match_states(
hass, name=list_name, domains=[DOMAIN]
)
for list_state in matching_states:
for maybe_list in list_entities:
if maybe_list.entity_id == list_state.entity_id:
target_list = maybe_list
break
if target_list is not None:
break
if target_list is None:
raise intent.IntentHandleError(f"No list named {list_name}")
else:
# Add to the first list
target_list = list_entities[0]
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
intent_obj.hass.bus.async_fire(EVENT_SHOPPING_LIST_UPDATED)
return response

View file

@ -21,13 +21,12 @@ def mock_shopping_list_io():
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Config Entry fixture."""
return MockConfigEntry(domain="shopping_list")
return MockConfigEntry(domain="shopping_list", entry_id="1234")
@pytest.fixture
async def sl_setup(hass: HomeAssistant, mock_config_entry: MockConfigEntry):
"""Set up the shopping list."""
mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)

View file

@ -11,7 +11,8 @@ from homeassistant.exceptions import HomeAssistantError
from tests.typing import WebSocketGenerator
TEST_ENTITY = "todo.shopping_list"
# NOTE: This depends on the config entry_id in sl_setup
TEST_ENTITY = "todo.shopping_list_1234"
@pytest.fixture

View file

@ -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."""
@ -728,3 +742,58 @@ 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_ADD_ITEM, {"item": {"value": "beer"}}
)
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 list by name
response = await intent.async_handle(
hass,
"test",
todo_intent.INTENT_ADD_ITEM,
{"item": {"value": "cheese"}, "list": {"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_ADD_ITEM,
{"item": {"value": "wine"}, "list": {"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"