From 35cdad943b1cdcba513661fc91bc11740dd49230 Mon Sep 17 00:00:00 2001 From: Lennard Scheibel <44374653+lscheibel@users.noreply.github.com> Date: Wed, 7 Sep 2022 05:18:27 +0200 Subject: [PATCH] Fix shopping_list service calls not notifying event bus (#77794) --- .../components/shopping_list/__init__.py | 58 ++++++++++++------- .../components/shopping_list/const.py | 1 + .../components/shopping_list/intent.py | 4 +- .../components/websocket_api/permissions.py | 2 +- tests/components/shopping_list/test_init.py | 49 ++++++++++++++++ 5 files changed, 89 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index 2af54722739..7344b729539 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -17,6 +17,7 @@ from homeassistant.util.json import load_json, save_json from .const import ( DOMAIN, + EVENT_SHOPPING_LIST_UPDATED, SERVICE_ADD_ITEM, SERVICE_CLEAR_COMPLETED_ITEMS, SERVICE_COMPLETE_ALL, @@ -29,7 +30,6 @@ ATTR_COMPLETE = "complete" _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({DOMAIN: {}}, extra=vol.ALLOW_EXTRA) -EVENT = "shopping_list_updated" ITEM_UPDATE_SCHEMA = vol.Schema({ATTR_COMPLETE: bool, ATTR_NAME: str}) PERSISTENCE = ".shopping_list.json" @@ -204,14 +204,19 @@ class ShoppingData: self.hass = hass self.items = [] - async def async_add(self, name): + async def async_add(self, name, context=None): """Add a shopping list item.""" item = {"name": name, "id": uuid.uuid4().hex, "complete": False} self.items.append(item) await self.hass.async_add_executor_job(self.save) + self.hass.bus.async_fire( + EVENT_SHOPPING_LIST_UPDATED, + {"action": "add", "item": item}, + context=context, + ) return item - async def async_update(self, item_id, info): + 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) @@ -221,22 +226,37 @@ class ShoppingData: info = ITEM_UPDATE_SCHEMA(info) item.update(info) await self.hass.async_add_executor_job(self.save) + self.hass.bus.async_fire( + EVENT_SHOPPING_LIST_UPDATED, + {"action": "update", "item": item}, + context=context, + ) return item - async def async_clear_completed(self): + async def async_clear_completed(self, context=None): """Clear completed items.""" self.items = [itm for itm in self.items if not itm["complete"]] await self.hass.async_add_executor_job(self.save) + self.hass.bus.async_fire( + EVENT_SHOPPING_LIST_UPDATED, + {"action": "clear"}, + context=context, + ) - async def async_update_list(self, info): + async def async_update_list(self, info, context=None): """Update all items in the list.""" for item in self.items: item.update(info) await self.hass.async_add_executor_job(self.save) + self.hass.bus.async_fire( + EVENT_SHOPPING_LIST_UPDATED, + {"action": "update_list"}, + context=context, + ) return self.items @callback - def async_reorder(self, item_ids): + def async_reorder(self, item_ids, context=None): """Reorder items.""" # The array for sorted items. new_items = [] @@ -259,6 +279,11 @@ class ShoppingData: new_items.append(all_items_mapping[key]) self.items = new_items self.hass.async_add_executor_job(self.save) + self.hass.bus.async_fire( + EVENT_SHOPPING_LIST_UPDATED, + {"action": "reorder"}, + context=context, + ) async def async_load(self): """Load items.""" @@ -298,7 +323,6 @@ class UpdateShoppingListItemView(http.HomeAssistantView): try: item = await request.app["hass"].data[DOMAIN].async_update(item_id, data) - request.app["hass"].bus.async_fire(EVENT) return self.json(item) except KeyError: return self.json_message("Item not found", HTTPStatus.NOT_FOUND) @@ -316,7 +340,6 @@ class CreateShoppingListItemView(http.HomeAssistantView): async def post(self, request, data): """Create a new shopping list item.""" item = await request.app["hass"].data[DOMAIN].async_add(data["name"]) - request.app["hass"].bus.async_fire(EVENT) return self.json(item) @@ -330,7 +353,6 @@ class ClearCompletedItemsView(http.HomeAssistantView): """Retrieve if API is running.""" hass = request.app["hass"] await hass.data[DOMAIN].async_clear_completed() - hass.bus.async_fire(EVENT) return self.json_message("Cleared completed items.") @@ -353,10 +375,7 @@ async def websocket_handle_add( msg: dict, ) -> None: """Handle add item to shopping_list.""" - item = await hass.data[DOMAIN].async_add(msg["name"]) - hass.bus.async_fire( - EVENT, {"action": "add", "item": item}, context=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)) @@ -373,9 +392,8 @@ async def websocket_handle_update( data = msg try: - item = await hass.data[DOMAIN].async_update(item_id, data) - hass.bus.async_fire( - EVENT, {"action": "update", "item": item}, context=connection.context(msg) + item = await hass.data[DOMAIN].async_update( + item_id, data, connection.context(msg) ) connection.send_message(websocket_api.result_message(msg_id, item)) except KeyError: @@ -391,8 +409,7 @@ async def websocket_handle_clear( msg: dict, ) -> None: """Handle clearing shopping_list items.""" - await hass.data[DOMAIN].async_clear_completed() - hass.bus.async_fire(EVENT, {"action": "clear"}, context=connection.context(msg)) + await hass.data[DOMAIN].async_clear_completed(connection.context(msg)) connection.send_message(websocket_api.result_message(msg["id"])) @@ -410,10 +427,7 @@ def websocket_handle_reorder( """Handle reordering shopping_list items.""" msg_id = msg.pop("id") try: - hass.data[DOMAIN].async_reorder(msg.pop("item_ids")) - hass.bus.async_fire( - EVENT, {"action": "reorder"}, context=connection.context(msg) - ) + hass.data[DOMAIN].async_reorder(msg.pop("item_ids"), connection.context(msg)) connection.send_result(msg_id) except KeyError: connection.send_error( diff --git a/homeassistant/components/shopping_list/const.py b/homeassistant/components/shopping_list/const.py index 2969fc8f86d..fffc1064226 100644 --- a/homeassistant/components/shopping_list/const.py +++ b/homeassistant/components/shopping_list/const.py @@ -1,5 +1,6 @@ """All constants related to the shopping list component.""" DOMAIN = "shopping_list" +EVENT_SHOPPING_LIST_UPDATED = "shopping_list_updated" SERVICE_ADD_ITEM = "add_item" SERVICE_COMPLETE_ITEM = "complete_item" diff --git a/homeassistant/components/shopping_list/intent.py b/homeassistant/components/shopping_list/intent.py index a6808b99328..4f5a39171b8 100644 --- a/homeassistant/components/shopping_list/intent.py +++ b/homeassistant/components/shopping_list/intent.py @@ -2,7 +2,7 @@ from homeassistant.helpers import intent import homeassistant.helpers.config_validation as cv -from . import DOMAIN, EVENT +from . import DOMAIN, EVENT_SHOPPING_LIST_UPDATED INTENT_ADD_ITEM = "HassShoppingListAddItem" INTENT_LAST_ITEMS = "HassShoppingListLastItems" @@ -28,7 +28,7 @@ class AddItemIntent(intent.IntentHandler): response = intent_obj.create_response() response.async_set_speech(f"I've added {item} to your shopping list") - intent_obj.hass.bus.async_fire(EVENT) + intent_obj.hass.bus.async_fire(EVENT_SHOPPING_LIST_UPDATED) return response diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py index 5dade8eeb2a..280594580e8 100644 --- a/homeassistant/components/websocket_api/permissions.py +++ b/homeassistant/components/websocket_api/permissions.py @@ -11,7 +11,7 @@ from homeassistant.components.lovelace.const import EVENT_LOVELACE_UPDATED from homeassistant.components.persistent_notification import ( EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, ) -from homeassistant.components.shopping_list import EVENT as EVENT_SHOPPING_LIST_UPDATED +from homeassistant.components.shopping_list import EVENT_SHOPPING_LIST_UPDATED from homeassistant.const import ( EVENT_COMPONENT_LOADED, EVENT_CORE_CONFIG_UPDATE, diff --git a/tests/components/shopping_list/test_init.py b/tests/components/shopping_list/test_init.py index 3c18929f6a1..515d0818460 100644 --- a/tests/components/shopping_list/test_init.py +++ b/tests/components/shopping_list/test_init.py @@ -3,6 +3,7 @@ from http import HTTPStatus from homeassistant.components.shopping_list.const import ( DOMAIN, + EVENT_SHOPPING_LIST_UPDATED, SERVICE_ADD_ITEM, SERVICE_CLEAR_COMPLETED_ITEMS, SERVICE_COMPLETE_ITEM, @@ -15,6 +16,8 @@ from homeassistant.components.websocket_api.const import ( from homeassistant.const import ATTR_NAME from homeassistant.helpers import intent +from tests.common import async_capture_events + async def test_add_item(hass, sl_setup): """Test adding an item intent.""" @@ -136,10 +139,12 @@ async def test_ws_get_items(hass, hass_ws_client, sl_setup): ) 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"}) msg = await client.receive_json() assert msg["success"] is True + assert len(events) == 0 assert msg["id"] == 5 assert msg["type"] == TYPE_RESULT @@ -166,11 +171,13 @@ async def test_deprecated_api_update(hass, hass_client, sl_setup): wine_id = hass.data["shopping_list"].items[1]["id"] client = await hass_client() + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) resp = await client.post( f"/api/shopping_list/item/{beer_id}", json={"name": "soda"} ) assert resp.status == HTTPStatus.OK + assert len(events) == 1 data = await resp.json() assert data == {"id": beer_id, "name": "soda", "complete": False} @@ -179,6 +186,7 @@ async def test_deprecated_api_update(hass, hass_client, sl_setup): ) assert resp.status == HTTPStatus.OK + assert len(events) == 2 data = await resp.json() assert data == {"id": wine_id, "name": "wine", "complete": True} @@ -199,6 +207,7 @@ async def test_ws_update_item(hass, hass_ws_client, sl_setup): beer_id = hass.data["shopping_list"].items[0]["id"] wine_id = hass.data["shopping_list"].items[1]["id"] client = await hass_ws_client(hass) + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) await client.send_json( { "id": 5, @@ -211,6 +220,8 @@ async def test_ws_update_item(hass, hass_ws_client, sl_setup): assert msg["success"] is True data = msg["result"] assert data == {"id": beer_id, "name": "soda", "complete": False} + assert len(events) == 1 + await client.send_json( { "id": 6, @@ -223,6 +234,7 @@ async def test_ws_update_item(hass, hass_ws_client, sl_setup): assert msg["success"] is True data = msg["result"] assert data == {"id": wine_id, "name": "wine", "complete": True} + assert len(events) == 2 beer, wine = hass.data["shopping_list"].items assert beer == {"id": beer_id, "name": "soda", "complete": False} @@ -237,9 +249,11 @@ async def test_api_update_fails(hass, hass_client, sl_setup): ) client = await hass_client() + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) resp = await client.post("/api/shopping_list/non_existing", json={"name": "soda"}) assert resp.status == HTTPStatus.NOT_FOUND + assert len(events) == 0 beer_id = hass.data["shopping_list"].items[0]["id"] resp = await client.post(f"/api/shopping_list/item/{beer_id}", json={"name": 123}) @@ -253,6 +267,7 @@ async def test_ws_update_item_fail(hass, hass_ws_client, sl_setup): hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} ) client = await hass_ws_client(hass) + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) await client.send_json( { "id": 5, @@ -265,9 +280,12 @@ async def test_ws_update_item_fail(hass, hass_ws_client, sl_setup): assert msg["success"] is False data = msg["error"] assert data == {"code": "item_not_found", "message": "Item not found"} + assert len(events) == 0 + await client.send_json({"id": 6, "type": "shopping_list/items/update", "name": 123}) msg = await client.receive_json() assert msg["success"] is False + assert len(events) == 0 async def test_deprecated_api_clear_completed(hass, hass_client, sl_setup): @@ -284,15 +302,18 @@ async def test_deprecated_api_clear_completed(hass, hass_client, sl_setup): wine_id = hass.data["shopping_list"].items[1]["id"] client = await hass_client() + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) # Mark beer as completed resp = await client.post( f"/api/shopping_list/item/{beer_id}", json={"complete": True} ) assert resp.status == HTTPStatus.OK + assert len(events) == 1 resp = await client.post("/api/shopping_list/clear_completed") assert resp.status == HTTPStatus.OK + assert len(events) == 2 items = hass.data["shopping_list"].items assert len(items) == 1 @@ -311,6 +332,7 @@ async def test_ws_clear_items(hass, hass_ws_client, sl_setup): beer_id = hass.data["shopping_list"].items[0]["id"] wine_id = hass.data["shopping_list"].items[1]["id"] client = await hass_ws_client(hass) + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) await client.send_json( { "id": 5, @@ -321,24 +343,29 @@ async def test_ws_clear_items(hass, hass_ws_client, sl_setup): ) msg = await client.receive_json() assert msg["success"] is True + assert len(events) == 1 + await client.send_json({"id": 6, "type": "shopping_list/items/clear"}) msg = await client.receive_json() assert msg["success"] is True items = hass.data["shopping_list"].items assert len(items) == 1 assert items[0] == {"id": wine_id, "name": "wine", "complete": False} + assert len(events) == 2 async def test_deprecated_api_create(hass, hass_client, sl_setup): """Test the API.""" client = await hass_client() + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) resp = await client.post("/api/shopping_list/item", json={"name": "soda"}) assert resp.status == HTTPStatus.OK data = await resp.json() assert data["name"] == "soda" assert data["complete"] is False + assert len(events) == 1 items = hass.data["shopping_list"].items assert len(items) == 1 @@ -350,21 +377,26 @@ async def test_deprecated_api_create_fail(hass, hass_client, sl_setup): """Test the API.""" client = await hass_client() + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) resp = await client.post("/api/shopping_list/item", json={"name": 1234}) assert resp.status == HTTPStatus.BAD_REQUEST assert len(hass.data["shopping_list"].items) == 0 + assert len(events) == 0 async def test_ws_add_item(hass, hass_ws_client, sl_setup): """Test adding 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() assert msg["success"] is True data = msg["result"] assert data["name"] == "soda" assert data["complete"] is False + assert len(events) == 1 + items = hass.data["shopping_list"].items assert len(items) == 1 assert items[0]["name"] == "soda" @@ -374,9 +406,11 @@ async def test_ws_add_item(hass, hass_ws_client, sl_setup): async def test_ws_add_item_fail(hass, hass_ws_client, sl_setup): """Test adding 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": 123}) msg = await client.receive_json() assert msg["success"] is False + assert len(events) == 0 assert len(hass.data["shopping_list"].items) == 0 @@ -397,6 +431,7 @@ async def test_ws_reorder_items(hass, hass_ws_client, sl_setup): apple_id = hass.data["shopping_list"].items[2]["id"] client = await hass_ws_client(hass) + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) await client.send_json( { "id": 6, @@ -406,6 +441,7 @@ async def test_ws_reorder_items(hass, hass_ws_client, sl_setup): ) msg = await client.receive_json() assert msg["success"] is True + assert len(events) == 1 assert hass.data["shopping_list"].items[0] == { "id": wine_id, "name": "wine", @@ -432,6 +468,7 @@ async def test_ws_reorder_items(hass, hass_ws_client, sl_setup): } ) _ = await client.receive_json() + assert len(events) == 2 await client.send_json( { @@ -442,6 +479,7 @@ async def test_ws_reorder_items(hass, hass_ws_client, sl_setup): ) msg = await client.receive_json() assert msg["success"] is True + assert len(events) == 3 assert hass.data["shopping_list"].items[0] == { "id": apple_id, "name": "apple", @@ -476,6 +514,7 @@ async def test_ws_reorder_items_failure(hass, hass_ws_client, sl_setup): apple_id = hass.data["shopping_list"].items[2]["id"] client = await hass_ws_client(hass) + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) # Testing sending bad item id. await client.send_json( @@ -488,6 +527,7 @@ async def test_ws_reorder_items_failure(hass, hass_ws_client, sl_setup): msg = await client.receive_json() assert msg["success"] is False assert msg["error"]["code"] == ERR_NOT_FOUND + assert len(events) == 0 # Testing not sending all unchecked item ids. await client.send_json( @@ -500,10 +540,12 @@ async def test_ws_reorder_items_failure(hass, hass_ws_client, sl_setup): msg = await client.receive_json() assert msg["success"] is False assert msg["error"]["code"] == ERR_INVALID_FORMAT + assert len(events) == 0 async def test_add_item_service(hass, sl_setup): """Test adding shopping_list item service.""" + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) await hass.services.async_call( DOMAIN, SERVICE_ADD_ITEM, @@ -513,10 +555,12 @@ async def test_add_item_service(hass, sl_setup): await hass.async_block_till_done() assert len(hass.data[DOMAIN].items) == 1 + assert len(events) == 1 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) await hass.services.async_call( DOMAIN, SERVICE_ADD_ITEM, @@ -525,7 +569,9 @@ async def test_clear_completed_items_service(hass, sl_setup): ) await hass.async_block_till_done() assert len(hass.data[DOMAIN].items) == 1 + assert len(events) == 1 + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) await hass.services.async_call( DOMAIN, SERVICE_COMPLETE_ITEM, @@ -534,7 +580,9 @@ async def test_clear_completed_items_service(hass, sl_setup): ) await hass.async_block_till_done() assert len(hass.data[DOMAIN].items) == 1 + assert len(events) == 1 + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) await hass.services.async_call( DOMAIN, SERVICE_CLEAR_COMPLETED_ITEMS, @@ -543,3 +591,4 @@ async def test_clear_completed_items_service(hass, sl_setup): ) await hass.async_block_till_done() assert len(hass.data[DOMAIN].items) == 0 + assert len(events) == 1