mirror of
https://github.com/home-assistant/core.git
synced 2025-07-26 06:37:52 +00:00
Add support for item removal to shopping list (#82992)
This commit is contained in:
parent
21c051935f
commit
80debae96d
@ -25,6 +25,7 @@ from .const import (
|
|||||||
SERVICE_COMPLETE_ITEM,
|
SERVICE_COMPLETE_ITEM,
|
||||||
SERVICE_INCOMPLETE_ALL,
|
SERVICE_INCOMPLETE_ALL,
|
||||||
SERVICE_INCOMPLETE_ITEM,
|
SERVICE_INCOMPLETE_ITEM,
|
||||||
|
SERVICE_REMOVE_ITEM,
|
||||||
)
|
)
|
||||||
|
|
||||||
ATTR_COMPLETE = "complete"
|
ATTR_COMPLETE = "complete"
|
||||||
@ -34,11 +35,12 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: {}}, extra=vol.ALLOW_EXTRA)
|
|||||||
ITEM_UPDATE_SCHEMA = vol.Schema({ATTR_COMPLETE: bool, ATTR_NAME: str})
|
ITEM_UPDATE_SCHEMA = vol.Schema({ATTR_COMPLETE: bool, ATTR_NAME: str})
|
||||||
PERSISTENCE = ".shopping_list.json"
|
PERSISTENCE = ".shopping_list.json"
|
||||||
|
|
||||||
SERVICE_ITEM_SCHEMA = vol.Schema({vol.Required(ATTR_NAME): vol.Any(None, cv.string)})
|
SERVICE_ITEM_SCHEMA = vol.Schema({vol.Required(ATTR_NAME): cv.string})
|
||||||
SERVICE_LIST_SCHEMA = vol.Schema({})
|
SERVICE_LIST_SCHEMA = vol.Schema({})
|
||||||
|
|
||||||
WS_TYPE_SHOPPING_LIST_ITEMS = "shopping_list/items"
|
WS_TYPE_SHOPPING_LIST_ITEMS = "shopping_list/items"
|
||||||
WS_TYPE_SHOPPING_LIST_ADD_ITEM = "shopping_list/items/add"
|
WS_TYPE_SHOPPING_LIST_ADD_ITEM = "shopping_list/items/add"
|
||||||
|
WS_TYPE_SHOPPING_LIST_REMOVE_ITEM = "shopping_list/items/remove"
|
||||||
WS_TYPE_SHOPPING_LIST_UPDATE_ITEM = "shopping_list/items/update"
|
WS_TYPE_SHOPPING_LIST_UPDATE_ITEM = "shopping_list/items/update"
|
||||||
WS_TYPE_SHOPPING_LIST_CLEAR_ITEMS = "shopping_list/items/clear"
|
WS_TYPE_SHOPPING_LIST_CLEAR_ITEMS = "shopping_list/items/clear"
|
||||||
|
|
||||||
@ -50,6 +52,13 @@ SCHEMA_WEBSOCKET_ADD_ITEM = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
|||||||
{vol.Required("type"): WS_TYPE_SHOPPING_LIST_ADD_ITEM, vol.Required("name"): str}
|
{vol.Required("type"): WS_TYPE_SHOPPING_LIST_ADD_ITEM, vol.Required("name"): str}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SCHEMA_WEBSOCKET_REMOVE_ITEM = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
vol.Required("type"): WS_TYPE_SHOPPING_LIST_REMOVE_ITEM,
|
||||||
|
vol.Required("item_id"): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
SCHEMA_WEBSOCKET_UPDATE_ITEM = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
SCHEMA_WEBSOCKET_UPDATE_ITEM = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Required("type"): WS_TYPE_SHOPPING_LIST_UPDATE_ITEM,
|
vol.Required("type"): WS_TYPE_SHOPPING_LIST_UPDATE_ITEM,
|
||||||
@ -85,26 +94,37 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
|||||||
async def add_item_service(call: ServiceCall) -> None:
|
async def add_item_service(call: ServiceCall) -> None:
|
||||||
"""Add an item with `name`."""
|
"""Add an item with `name`."""
|
||||||
data = hass.data[DOMAIN]
|
data = hass.data[DOMAIN]
|
||||||
if (name := call.data.get(ATTR_NAME)) is not None:
|
await data.async_add(call.data[ATTR_NAME])
|
||||||
await data.async_add(name)
|
|
||||||
|
|
||||||
async def complete_item_service(call: ServiceCall) -> None:
|
async def remove_item_service(call: ServiceCall) -> None:
|
||||||
"""Mark the item provided via `name` as completed."""
|
"""Remove the first item with matching `name`."""
|
||||||
data = hass.data[DOMAIN]
|
data = hass.data[DOMAIN]
|
||||||
if (name := call.data.get(ATTR_NAME)) is None:
|
name = call.data[ATTR_NAME]
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
item = [item for item in data.items if item["name"] == name][0]
|
item = [item for item in data.items if item["name"] == name][0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
_LOGGER.error("Removing of item failed: %s cannot be found", name)
|
_LOGGER.error("Removing of item failed: %s cannot be found", name)
|
||||||
|
else:
|
||||||
|
await data.async_remove(item["id"])
|
||||||
|
|
||||||
|
async def complete_item_service(call: ServiceCall) -> None:
|
||||||
|
"""Mark the first item with matching `name` as completed."""
|
||||||
|
data = hass.data[DOMAIN]
|
||||||
|
name = call.data[ATTR_NAME]
|
||||||
|
|
||||||
|
try:
|
||||||
|
item = [item for item in data.items if item["name"] == name][0]
|
||||||
|
except IndexError:
|
||||||
|
_LOGGER.error("Updating of item failed: %s cannot be found", name)
|
||||||
else:
|
else:
|
||||||
await data.async_update(item["id"], {"name": name, "complete": True})
|
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 item provided via `name` as incomplete."""
|
"""Mark the first item with matching `name` as incomplete."""
|
||||||
data = hass.data[DOMAIN]
|
data = hass.data[DOMAIN]
|
||||||
if (name := call.data.get(ATTR_NAME)) is None:
|
name = call.data[ATTR_NAME]
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
item = [item for item in data.items if item["name"] == name][0]
|
item = [item for item in data.items if item["name"] == name][0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
@ -130,6 +150,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
|||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_ADD_ITEM, add_item_service, schema=SERVICE_ITEM_SCHEMA
|
DOMAIN, SERVICE_ADD_ITEM, add_item_service, schema=SERVICE_ITEM_SCHEMA
|
||||||
)
|
)
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN, SERVICE_REMOVE_ITEM, remove_item_service, schema=SERVICE_ITEM_SCHEMA
|
||||||
|
)
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, SERVICE_COMPLETE_ITEM, complete_item_service, schema=SERVICE_ITEM_SCHEMA
|
DOMAIN, SERVICE_COMPLETE_ITEM, complete_item_service, schema=SERVICE_ITEM_SCHEMA
|
||||||
)
|
)
|
||||||
@ -179,6 +202,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
|||||||
websocket_handle_add,
|
websocket_handle_add,
|
||||||
SCHEMA_WEBSOCKET_ADD_ITEM,
|
SCHEMA_WEBSOCKET_ADD_ITEM,
|
||||||
)
|
)
|
||||||
|
websocket_api.async_register_command(
|
||||||
|
hass,
|
||||||
|
WS_TYPE_SHOPPING_LIST_REMOVE_ITEM,
|
||||||
|
websocket_handle_remove,
|
||||||
|
SCHEMA_WEBSOCKET_REMOVE_ITEM,
|
||||||
|
)
|
||||||
websocket_api.async_register_command(
|
websocket_api.async_register_command(
|
||||||
hass,
|
hass,
|
||||||
WS_TYPE_SHOPPING_LIST_UPDATE_ITEM,
|
WS_TYPE_SHOPPING_LIST_UPDATE_ITEM,
|
||||||
@ -217,6 +246,22 @@ class ShoppingData:
|
|||||||
)
|
)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
async def async_remove(self, item_id, context=None):
|
||||||
|
"""Remove a shopping list item."""
|
||||||
|
item = next((itm for itm in self.items if itm["id"] == item_id), None)
|
||||||
|
|
||||||
|
if item is None:
|
||||||
|
raise KeyError
|
||||||
|
|
||||||
|
self.items.remove(item)
|
||||||
|
await self.hass.async_add_executor_job(self.save)
|
||||||
|
self.hass.bus.async_fire(
|
||||||
|
EVENT_SHOPPING_LIST_UPDATED,
|
||||||
|
{"action": "remove", "item": item},
|
||||||
|
context=context,
|
||||||
|
)
|
||||||
|
return item
|
||||||
|
|
||||||
async def async_update(self, item_id, info, context=None):
|
async def async_update(self, item_id, info, context=None):
|
||||||
"""Update a shopping list item."""
|
"""Update a shopping list item."""
|
||||||
item = next((itm for itm in self.items if itm["id"] == item_id), None)
|
item = next((itm for itm in self.items if itm["id"] == item_id), None)
|
||||||
@ -363,7 +408,7 @@ def websocket_handle_items(
|
|||||||
connection: websocket_api.ActiveConnection,
|
connection: websocket_api.ActiveConnection,
|
||||||
msg: dict[str, Any],
|
msg: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle get shopping_list items."""
|
"""Handle getting shopping_list items."""
|
||||||
connection.send_message(
|
connection.send_message(
|
||||||
websocket_api.result_message(msg["id"], hass.data[DOMAIN].items)
|
websocket_api.result_message(msg["id"], hass.data[DOMAIN].items)
|
||||||
)
|
)
|
||||||
@ -375,18 +420,38 @@ async def websocket_handle_add(
|
|||||||
connection: websocket_api.ActiveConnection,
|
connection: websocket_api.ActiveConnection,
|
||||||
msg: dict[str, Any],
|
msg: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle add item to shopping_list."""
|
"""Handle adding item to shopping_list."""
|
||||||
item = await hass.data[DOMAIN].async_add(msg["name"], connection.context(msg))
|
item = await hass.data[DOMAIN].async_add(msg["name"], connection.context(msg))
|
||||||
connection.send_message(websocket_api.result_message(msg["id"], item))
|
connection.send_message(websocket_api.result_message(msg["id"], item))
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.async_response
|
||||||
|
async def websocket_handle_remove(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.ActiveConnection,
|
||||||
|
msg: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Handle removing shopping_list item."""
|
||||||
|
msg_id = msg.pop("id")
|
||||||
|
item_id = msg.pop("item_id")
|
||||||
|
msg.pop("type")
|
||||||
|
|
||||||
|
try:
|
||||||
|
item = await hass.data[DOMAIN].async_remove(item_id, connection.context(msg))
|
||||||
|
connection.send_message(websocket_api.result_message(msg_id, item))
|
||||||
|
except KeyError:
|
||||||
|
connection.send_message(
|
||||||
|
websocket_api.error_message(msg_id, "item_not_found", "Item not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.async_response
|
@websocket_api.async_response
|
||||||
async def websocket_handle_update(
|
async def websocket_handle_update(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
connection: websocket_api.ActiveConnection,
|
connection: websocket_api.ActiveConnection,
|
||||||
msg: dict[str, Any],
|
msg: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle update shopping_list item."""
|
"""Handle updating shopping_list item."""
|
||||||
msg_id = msg.pop("id")
|
msg_id = msg.pop("id")
|
||||||
item_id = msg.pop("item_id")
|
item_id = msg.pop("item_id")
|
||||||
msg.pop("type")
|
msg.pop("type")
|
||||||
|
@ -3,6 +3,7 @@ DOMAIN = "shopping_list"
|
|||||||
EVENT_SHOPPING_LIST_UPDATED = "shopping_list_updated"
|
EVENT_SHOPPING_LIST_UPDATED = "shopping_list_updated"
|
||||||
|
|
||||||
SERVICE_ADD_ITEM = "add_item"
|
SERVICE_ADD_ITEM = "add_item"
|
||||||
|
SERVICE_REMOVE_ITEM = "remove_item"
|
||||||
SERVICE_COMPLETE_ITEM = "complete_item"
|
SERVICE_COMPLETE_ITEM = "complete_item"
|
||||||
SERVICE_INCOMPLETE_ITEM = "incomplete_item"
|
SERVICE_INCOMPLETE_ITEM = "incomplete_item"
|
||||||
SERVICE_COMPLETE_ALL = "complete_all"
|
SERVICE_COMPLETE_ALL = "complete_all"
|
||||||
|
@ -10,9 +10,21 @@ add_item:
|
|||||||
selector:
|
selector:
|
||||||
text:
|
text:
|
||||||
|
|
||||||
|
remove_item:
|
||||||
|
name: Remove item
|
||||||
|
description: Remove the first item with matching name from the shopping list.
|
||||||
|
fields:
|
||||||
|
name:
|
||||||
|
name: Name
|
||||||
|
description: The name of the item to remove.
|
||||||
|
required: true
|
||||||
|
example: Beer
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
|
||||||
complete_item:
|
complete_item:
|
||||||
name: Complete item
|
name: Complete item
|
||||||
description: Mark an item as completed in the shopping list.
|
description: Mark the first item with matching name as completed in the shopping list.
|
||||||
fields:
|
fields:
|
||||||
name:
|
name:
|
||||||
name: Name
|
name: Name
|
||||||
@ -24,7 +36,7 @@ complete_item:
|
|||||||
|
|
||||||
incomplete_item:
|
incomplete_item:
|
||||||
name: Incomplete item
|
name: Incomplete item
|
||||||
description: Marks an item as incomplete in the shopping list.
|
description: Mark the first item with matching name as incomplete in the shopping list.
|
||||||
fields:
|
fields:
|
||||||
name:
|
name:
|
||||||
description: The name of the item to mark as incomplete.
|
description: The name of the item to mark as incomplete.
|
||||||
@ -35,11 +47,11 @@ incomplete_item:
|
|||||||
|
|
||||||
complete_all:
|
complete_all:
|
||||||
name: Complete all
|
name: Complete all
|
||||||
description: Marks all items as completed in the shopping list. It does not remove the items.
|
description: Mark all items as completed in the shopping list (without removing them from the list).
|
||||||
|
|
||||||
incomplete_all:
|
incomplete_all:
|
||||||
name: Incomplete all
|
name: Incomplete all
|
||||||
description: Marks all items as incomplete in the shopping list.
|
description: Mark all items as incomplete in the shopping list.
|
||||||
|
|
||||||
clear_completed_items:
|
clear_completed_items:
|
||||||
name: Clear completed items
|
name: Clear completed items
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
"""Test shopping list component."""
|
"""Test shopping list component."""
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.shopping_list.const import (
|
from homeassistant.components.shopping_list.const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
EVENT_SHOPPING_LIST_UPDATED,
|
EVENT_SHOPPING_LIST_UPDATED,
|
||||||
SERVICE_ADD_ITEM,
|
SERVICE_ADD_ITEM,
|
||||||
SERVICE_CLEAR_COMPLETED_ITEMS,
|
SERVICE_CLEAR_COMPLETED_ITEMS,
|
||||||
SERVICE_COMPLETE_ITEM,
|
SERVICE_COMPLETE_ITEM,
|
||||||
|
SERVICE_REMOVE_ITEM,
|
||||||
)
|
)
|
||||||
from homeassistant.components.websocket_api.const import (
|
from homeassistant.components.websocket_api.const import (
|
||||||
ERR_INVALID_FORMAT,
|
ERR_INVALID_FORMAT,
|
||||||
@ -29,6 +32,32 @@ async def test_add_item(hass, sl_setup):
|
|||||||
assert response.speech["plain"]["speech"] == "I've added beer to your shopping list"
|
assert response.speech["plain"]["speech"] == "I've added beer to your shopping list"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_remove_item(hass, sl_setup):
|
||||||
|
"""Test removiung list items."""
|
||||||
|
await intent.async_handle(
|
||||||
|
hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}}
|
||||||
|
)
|
||||||
|
|
||||||
|
await intent.async_handle(
|
||||||
|
hass, "test", "HassShoppingListAddItem", {"item": {"value": "cheese"}}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(hass.data[DOMAIN].items) == 2
|
||||||
|
|
||||||
|
# Remove a single item
|
||||||
|
item_id = hass.data[DOMAIN].items[0]["id"]
|
||||||
|
await hass.data[DOMAIN].async_remove(item_id)
|
||||||
|
|
||||||
|
assert len(hass.data[DOMAIN].items) == 1
|
||||||
|
|
||||||
|
item = hass.data[DOMAIN].items[0]
|
||||||
|
assert item["name"] == "cheese"
|
||||||
|
|
||||||
|
# Trying to remove the same item twice should fail
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
await hass.data[DOMAIN].async_remove(item_id)
|
||||||
|
|
||||||
|
|
||||||
async def test_update_list(hass, sl_setup):
|
async def test_update_list(hass, sl_setup):
|
||||||
"""Test updating all list items."""
|
"""Test updating all list items."""
|
||||||
await intent.async_handle(
|
await intent.async_handle(
|
||||||
@ -414,6 +443,47 @@ async def test_ws_add_item_fail(hass, hass_ws_client, sl_setup):
|
|||||||
assert len(hass.data["shopping_list"].items) == 0
|
assert len(hass.data["shopping_list"].items) == 0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ws_remove_item(hass, hass_ws_client, sl_setup):
|
||||||
|
"""Test removing shopping_list item websocket command."""
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED)
|
||||||
|
await client.send_json({"id": 5, "type": "shopping_list/items/add", "name": "soda"})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
first_item_id = msg["result"]["id"]
|
||||||
|
await client.send_json(
|
||||||
|
{"id": 6, "type": "shopping_list/items/add", "name": "cheese"}
|
||||||
|
)
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert len(events) == 2
|
||||||
|
|
||||||
|
items = hass.data["shopping_list"].items
|
||||||
|
assert len(items) == 2
|
||||||
|
|
||||||
|
await client.send_json(
|
||||||
|
{"id": 7, "type": "shopping_list/items/remove", "item_id": first_item_id}
|
||||||
|
)
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert len(events) == 3
|
||||||
|
assert msg["success"] is True
|
||||||
|
|
||||||
|
items = hass.data["shopping_list"].items
|
||||||
|
assert len(items) == 1
|
||||||
|
assert items[0]["name"] == "cheese"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ws_remove_item_fail(hass, hass_ws_client, sl_setup):
|
||||||
|
"""Test removing shopping_list item failure websocket command."""
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED)
|
||||||
|
await client.send_json({"id": 5, "type": "shopping_list/items/add", "name": "soda"})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
await client.send_json({"id": 6, "type": "shopping_list/items/remove"})
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert msg["success"] is False
|
||||||
|
assert len(events) == 1
|
||||||
|
assert len(hass.data["shopping_list"].items) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_ws_reorder_items(hass, hass_ws_client, sl_setup):
|
async def test_ws_reorder_items(hass, hass_ws_client, sl_setup):
|
||||||
"""Test reordering shopping_list items websocket command."""
|
"""Test reordering shopping_list items websocket command."""
|
||||||
await intent.async_handle(
|
await intent.async_handle(
|
||||||
@ -558,6 +628,40 @@ async def test_add_item_service(hass, sl_setup):
|
|||||||
assert len(events) == 1
|
assert len(events) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_remove_item_service(hass, sl_setup):
|
||||||
|
"""Test removing shopping_list item service."""
|
||||||
|
events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED)
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_ADD_ITEM,
|
||||||
|
{ATTR_NAME: "beer"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_ADD_ITEM,
|
||||||
|
{ATTR_NAME: "cheese"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(hass.data[DOMAIN].items) == 2
|
||||||
|
assert len(events) == 2
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_REMOVE_ITEM,
|
||||||
|
{ATTR_NAME: "beer"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(hass.data[DOMAIN].items) == 1
|
||||||
|
assert hass.data[DOMAIN].items[0]["name"] == "cheese"
|
||||||
|
assert len(events) == 3
|
||||||
|
|
||||||
|
|
||||||
async def test_clear_completed_items_service(hass, sl_setup):
|
async def test_clear_completed_items_service(hass, sl_setup):
|
||||||
"""Test clearing completed shopping_list items service."""
|
"""Test clearing completed shopping_list items service."""
|
||||||
events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED)
|
events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user