Add complete intent function for shopping list component (#128565)

* add intent

* add tests

* raise IntentHandleError

* add check for non completed

* Prefer completing non complete items

* cleanup

* cleanup tests

* rename test

Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>

* remove duplicated test

* update test

* complete all items

* fix event

* remove type def

* return speech slots

---------

Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
This commit is contained in:
Elias Wernicke 2025-05-27 21:35:14 +02:00 committed by GitHub
parent 4fcebf18dc
commit c20ad5fde1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 104 additions and 8 deletions

View File

@ -92,13 +92,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
"""Mark the first item with matching `name` as completed.""" """Mark the first item with matching `name` as completed."""
data = hass.data[DOMAIN] data = hass.data[DOMAIN]
name = call.data[ATTR_NAME] name = call.data[ATTR_NAME]
try: try:
item = [item for item in data.items if item["name"] == name][0] await data.async_complete(name)
except IndexError: except NoMatchingShoppingListItem:
_LOGGER.error("Updating of item failed: %s cannot be found", name) _LOGGER.error("Completing of item failed: %s cannot be found", name)
else:
await data.async_update(item["id"], {"name": name, "complete": True})
async def incomplete_item_service(call: ServiceCall) -> None: async def incomplete_item_service(call: ServiceCall) -> None:
"""Mark the first item with matching `name` as incomplete.""" """Mark the first item with matching `name` as incomplete."""
@ -258,6 +255,30 @@ class ShoppingData:
) )
return removed return removed
async def async_complete(
self, name: str, context: Context | None = None
) -> list[dict[str, JsonValueType]]:
"""Mark all shopping list items with the given name as complete."""
complete_items = [
item for item in self.items if item["name"] == name and not item["complete"]
]
if len(complete_items) == 0:
raise NoMatchingShoppingListItem
for item in complete_items:
_LOGGER.debug("Completing %s", item)
item["complete"] = True
await self.hass.async_add_executor_job(self.save)
self._async_notify()
for item in complete_items:
self.hass.bus.async_fire(
EVENT_SHOPPING_LIST_UPDATED,
{"action": "complete", "item": item},
context=context,
)
return complete_items
async def async_update( async def async_update(
self, item_id: str | None, info: dict[str, Any], context: Context | None = None self, item_id: str | None, info: dict[str, Any], context: Context | None = None
) -> dict[str, JsonValueType]: ) -> dict[str, JsonValueType]:

View File

@ -5,15 +5,17 @@ from __future__ import annotations
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, intent from homeassistant.helpers import config_validation as cv, intent
from . import DOMAIN, EVENT_SHOPPING_LIST_UPDATED from . import DOMAIN, EVENT_SHOPPING_LIST_UPDATED, NoMatchingShoppingListItem
INTENT_ADD_ITEM = "HassShoppingListAddItem" INTENT_ADD_ITEM = "HassShoppingListAddItem"
INTENT_COMPLETE_ITEM = "HassShoppingListCompleteItem"
INTENT_LAST_ITEMS = "HassShoppingListLastItems" INTENT_LAST_ITEMS = "HassShoppingListLastItems"
async def async_setup_intents(hass: HomeAssistant) -> None: 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, AddItemIntent())
intent.async_register(hass, CompleteItemIntent())
intent.async_register(hass, ListTopItemsIntent()) intent.async_register(hass, ListTopItemsIntent())
@ -36,6 +38,33 @@ class AddItemIntent(intent.IntentHandler):
return response return response
class CompleteItemIntent(intent.IntentHandler):
"""Handle CompleteItem intents."""
intent_type = INTENT_COMPLETE_ITEM
description = "Marks an item as completed on the shopping list"
slot_schema = {"item": cv.string}
platforms = {DOMAIN}
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
"""Handle the intent."""
slots = self.async_validate_slots(intent_obj.slots)
item = slots["item"]["value"].strip()
try:
complete_items = await intent_obj.hass.data[DOMAIN].async_complete(item)
except NoMatchingShoppingListItem:
complete_items = []
intent_obj.hass.bus.async_fire(EVENT_SHOPPING_LIST_UPDATED)
response = intent_obj.create_response()
response.async_set_speech_slots({"completed_items": complete_items})
response.response_type = intent.IntentResponseType.ACTION_DONE
return response
class ListTopItemsIntent(intent.IntentHandler): class ListTopItemsIntent(intent.IntentHandler):
"""Handle AddItem intents.""" """Handle AddItem intents."""
@ -47,7 +76,7 @@ class ListTopItemsIntent(intent.IntentHandler):
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
"""Handle the intent.""" """Handle the intent."""
items = intent_obj.hass.data[DOMAIN].items[-5:] items = intent_obj.hass.data[DOMAIN].items[-5:]
response = intent_obj.create_response() response: intent.IntentResponse = intent_obj.create_response()
if not items: if not items:
response.async_set_speech("There are no items on your shopping list") response.async_set_speech("There are no items on your shopping list")

View File

@ -4,6 +4,52 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import intent from homeassistant.helpers import intent
async def test_complete_item_intent(hass: HomeAssistant, sl_setup) -> None:
"""Test complete item."""
await intent.async_handle(
hass, "test", "HassShoppingListAddItem", {"item": {"value": "soda"}}
)
await intent.async_handle(
hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}}
)
await intent.async_handle(
hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}}
)
await intent.async_handle(
hass, "test", "HassShoppingListAddItem", {"item": {"value": "wine"}}
)
response = await intent.async_handle(
hass, "test", "HassShoppingListCompleteItem", {"item": {"value": "beer"}}
)
assert response.response_type == intent.IntentResponseType.ACTION_DONE
completed_items = response.speech_slots.get("completed_items")
assert len(completed_items) == 2
assert completed_items[0]["name"] == "beer"
assert hass.data["shopping_list"].items[1]["complete"]
assert hass.data["shopping_list"].items[2]["complete"]
# Complete again
response = await intent.async_handle(
hass, "test", "HassShoppingListCompleteItem", {"item": {"value": "beer"}}
)
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.speech_slots.get("completed_items") == []
assert hass.data["shopping_list"].items[1]["complete"]
assert hass.data["shopping_list"].items[2]["complete"]
async def test_complete_item_intent_not_found(hass: HomeAssistant, sl_setup) -> None:
"""Test completing a missing item."""
response = await intent.async_handle(
hass, "test", "HassShoppingListCompleteItem", {"item": {"value": "beer"}}
)
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert response.speech_slots.get("completed_items") == []
async def test_recent_items_intent(hass: HomeAssistant, sl_setup) -> None: async def test_recent_items_intent(hass: HomeAssistant, sl_setup) -> None:
"""Test recent items.""" """Test recent items."""
await intent.async_handle( await intent.async_handle(