Add support for item removal to shopping list (#82992)
parent
21c051935f
commit
80debae96d
|
@ -25,6 +25,7 @@ from .const import (
|
|||
SERVICE_COMPLETE_ITEM,
|
||||
SERVICE_INCOMPLETE_ALL,
|
||||
SERVICE_INCOMPLETE_ITEM,
|
||||
SERVICE_REMOVE_ITEM,
|
||||
)
|
||||
|
||||
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})
|
||||
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({})
|
||||
|
||||
WS_TYPE_SHOPPING_LIST_ITEMS = "shopping_list/items"
|
||||
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_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}
|
||||
)
|
||||
|
||||
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(
|
||||
{
|
||||
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:
|
||||
"""Add an item with `name`."""
|
||||
data = hass.data[DOMAIN]
|
||||
if (name := call.data.get(ATTR_NAME)) is not None:
|
||||
await data.async_add(name)
|
||||
await data.async_add(call.data[ATTR_NAME])
|
||||
|
||||
async def complete_item_service(call: ServiceCall) -> None:
|
||||
"""Mark the item provided via `name` as completed."""
|
||||
async def remove_item_service(call: ServiceCall) -> None:
|
||||
"""Remove the first item with matching `name`."""
|
||||
data = hass.data[DOMAIN]
|
||||
if (name := call.data.get(ATTR_NAME)) is None:
|
||||
return
|
||||
name = call.data[ATTR_NAME]
|
||||
|
||||
try:
|
||||
item = [item for item in data.items if item["name"] == name][0]
|
||||
except IndexError:
|
||||
_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:
|
||||
await data.async_update(item["id"], {"name": name, "complete": True})
|
||||
|
||||
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]
|
||||
if (name := call.data.get(ATTR_NAME)) is None:
|
||||
return
|
||||
name = call.data[ATTR_NAME]
|
||||
|
||||
try:
|
||||
item = [item for item in data.items if item["name"] == name][0]
|
||||
except IndexError:
|
||||
|
@ -130,6 +150,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
|||
hass.services.async_register(
|
||||
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(
|
||||
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,
|
||||
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(
|
||||
hass,
|
||||
WS_TYPE_SHOPPING_LIST_UPDATE_ITEM,
|
||||
|
@ -217,6 +246,22 @@ class ShoppingData:
|
|||
)
|
||||
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):
|
||||
"""Update a shopping list item."""
|
||||
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,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Handle get shopping_list items."""
|
||||
"""Handle getting shopping_list items."""
|
||||
connection.send_message(
|
||||
websocket_api.result_message(msg["id"], hass.data[DOMAIN].items)
|
||||
)
|
||||
|
@ -375,18 +420,38 @@ async def websocket_handle_add(
|
|||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> 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))
|
||||
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
|
||||
async def websocket_handle_update(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Handle update shopping_list item."""
|
||||
"""Handle updating shopping_list item."""
|
||||
msg_id = msg.pop("id")
|
||||
item_id = msg.pop("item_id")
|
||||
msg.pop("type")
|
||||
|
|
|
@ -3,6 +3,7 @@ DOMAIN = "shopping_list"
|
|||
EVENT_SHOPPING_LIST_UPDATED = "shopping_list_updated"
|
||||
|
||||
SERVICE_ADD_ITEM = "add_item"
|
||||
SERVICE_REMOVE_ITEM = "remove_item"
|
||||
SERVICE_COMPLETE_ITEM = "complete_item"
|
||||
SERVICE_INCOMPLETE_ITEM = "incomplete_item"
|
||||
SERVICE_COMPLETE_ALL = "complete_all"
|
||||
|
|
|
@ -10,9 +10,21 @@ add_item:
|
|||
selector:
|
||||
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:
|
||||
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:
|
||||
name:
|
||||
name: Name
|
||||
|
@ -24,7 +36,7 @@ complete_item:
|
|||
|
||||
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:
|
||||
name:
|
||||
description: The name of the item to mark as incomplete.
|
||||
|
@ -35,11 +47,11 @@ incomplete_item:
|
|||
|
||||
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:
|
||||
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:
|
||||
name: Clear completed items
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
"""Test shopping list component."""
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.shopping_list.const import (
|
||||
DOMAIN,
|
||||
EVENT_SHOPPING_LIST_UPDATED,
|
||||
SERVICE_ADD_ITEM,
|
||||
SERVICE_CLEAR_COMPLETED_ITEMS,
|
||||
SERVICE_COMPLETE_ITEM,
|
||||
SERVICE_REMOVE_ITEM,
|
||||
)
|
||||
from homeassistant.components.websocket_api.const import (
|
||||
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"
|
||||
|
||||
|
||||
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):
|
||||
"""Test updating all list items."""
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
"""Test reordering shopping_list items websocket command."""
|
||||
await intent.async_handle(
|
||||
|
@ -558,6 +628,40 @@ async def test_add_item_service(hass, sl_setup):
|
|||
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):
|
||||
"""Test clearing completed shopping_list items service."""
|
||||
events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED)
|
||||
|
|
Loading…
Reference in New Issue