Add support for item removal to shopping list (#82992)

pull/83125/head
Philip Allgaier 2022-12-02 12:40:59 +01:00 committed by GitHub
parent 21c051935f
commit 80debae96d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 199 additions and 17 deletions

View File

@ -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")

View File

@ -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"

View File

@ -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

View File

@ -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)