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.""" """Intents for the Shopping List integration."""
from __future__ import annotations from __future__ import annotations
from homeassistant.core import HomeAssistant
from homeassistant.helpers import intent from homeassistant.helpers import intent
import homeassistant.helpers.config_validation as cv 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" INTENT_LAST_ITEMS = "HassShoppingListLastItems"
async def async_setup_intents(hass): async def async_setup_intents(hass: HomeAssistant) -> None:
"""Set up the Shopping List intents.""" """Set up the Shopping List intents."""
intent.async_register(hass, AddItemIntent())
intent.async_register(hass, ListTopItemsIntent()) 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): class ListTopItemsIntent(intent.IntentHandler):
"""Handle AddItem intents.""" """Handle AddItem intents."""

View file

@ -3,7 +3,7 @@
"name": "Shopping List", "name": "Shopping List",
"codeowners": [], "codeowners": [],
"config_flow": true, "config_flow": true,
"dependencies": ["http"], "dependencies": ["http", "intent", "todo"],
"documentation": "https://www.home-assistant.io/integrations/shopping_list", "documentation": "https://www.home-assistant.io/integrations/shopping_list",
"iot_class": "local_push", "iot_class": "local_push",
"quality_scale": "internal" "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 @pytest.fixture
def mock_config_entry() -> MockConfigEntry: def mock_config_entry() -> MockConfigEntry:
"""Config Entry fixture.""" """Config Entry fixture."""
return MockConfigEntry(domain="shopping_list") return MockConfigEntry(domain="shopping_list", entry_id="1234")
@pytest.fixture @pytest.fixture
async def sl_setup(hass: HomeAssistant, mock_config_entry: MockConfigEntry): async def sl_setup(hass: HomeAssistant, mock_config_entry: MockConfigEntry):
"""Set up the shopping list.""" """Set up the shopping list."""
mock_config_entry.add_to_hass(hass) mock_config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(mock_config_entry.entry_id) 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 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 @pytest.fixture

View file

@ -13,11 +13,13 @@ from homeassistant.components.todo import (
TodoItemStatus, TodoItemStatus,
TodoListEntity, TodoListEntity,
TodoListEntityFeature, TodoListEntityFeature,
intent as todo_intent,
) )
from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigFlow from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigFlow
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import intent
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from tests.common import ( from tests.common import (
@ -37,6 +39,18 @@ class MockFlow(ConfigFlow):
"""Test flow.""" """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) @pytest.fixture(autouse=True)
def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]:
"""Mock config flow.""" """Mock config flow."""
@ -728,3 +742,58 @@ async def test_move_item_unsupported(
resp = await client.receive_json() resp = await client.receive_json()
assert resp.get("id") == 1 assert resp.get("id") == 1
assert resp.get("error", {}).get("code") == "not_supported" 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"