diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index bd08e19c4e2..3dc26fe007a 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -18,6 +18,8 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.util.json import JsonArrayType, load_json_array from .const import ( + ATTR_REVERSE, + DEFAULT_REVERSE, DOMAIN, EVENT_SHOPPING_LIST_UPDATED, SERVICE_ADD_ITEM, @@ -27,6 +29,7 @@ from .const import ( SERVICE_INCOMPLETE_ALL, SERVICE_INCOMPLETE_ITEM, SERVICE_REMOVE_ITEM, + SERVICE_SORT, ) ATTR_COMPLETE = "complete" @@ -38,6 +41,9 @@ PERSISTENCE = ".shopping_list.json" SERVICE_ITEM_SCHEMA = vol.Schema({vol.Required(ATTR_NAME): cv.string}) SERVICE_LIST_SCHEMA = vol.Schema({}) +SERVICE_SORT_SCHEMA = vol.Schema( + {vol.Optional(ATTR_REVERSE, default=DEFAULT_REVERSE): bool} +) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -111,6 +117,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b """Clear all completed items from the list.""" await data.async_clear_completed() + async def sort_list_service(call: ServiceCall) -> None: + """Sort all items by name.""" + await data.async_sort(call.data[ATTR_REVERSE]) + data = hass.data[DOMAIN] = ShoppingData(hass) await data.async_load() @@ -147,6 +157,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b clear_completed_items_service, schema=SERVICE_LIST_SCHEMA, ) + hass.services.async_register( + DOMAIN, + SERVICE_SORT, + sort_list_service, + schema=SERVICE_SORT_SCHEMA, + ) hass.http.register_view(ShoppingListView) hass.http.register_view(CreateShoppingListItemView) @@ -277,6 +293,16 @@ class ShoppingData: context=context, ) + async def async_sort(self, reverse=False, context=None): + """Sort items by name.""" + self.items = sorted(self.items, key=lambda item: item["name"], reverse=reverse) + self.hass.async_add_executor_job(self.save) + self.hass.bus.async_fire( + EVENT_SHOPPING_LIST_UPDATED, + {"action": "sorted"}, + context=context, + ) + async def async_load(self) -> None: """Load items.""" diff --git a/homeassistant/components/shopping_list/const.py b/homeassistant/components/shopping_list/const.py index 05dc05137c0..c519123a414 100644 --- a/homeassistant/components/shopping_list/const.py +++ b/homeassistant/components/shopping_list/const.py @@ -2,6 +2,10 @@ DOMAIN = "shopping_list" EVENT_SHOPPING_LIST_UPDATED = "shopping_list_updated" +ATTR_REVERSE = "reverse" + +DEFAULT_REVERSE = False + SERVICE_ADD_ITEM = "add_item" SERVICE_REMOVE_ITEM = "remove_item" SERVICE_COMPLETE_ITEM = "complete_item" @@ -9,3 +13,4 @@ SERVICE_INCOMPLETE_ITEM = "incomplete_item" SERVICE_COMPLETE_ALL = "complete_all" SERVICE_INCOMPLETE_ALL = "incomplete_all" SERVICE_CLEAR_COMPLETED_ITEMS = "clear_completed_items" +SERVICE_SORT = "sort" diff --git a/homeassistant/components/shopping_list/services.yaml b/homeassistant/components/shopping_list/services.yaml index c41bc1333dc..250912f49cd 100644 --- a/homeassistant/components/shopping_list/services.yaml +++ b/homeassistant/components/shopping_list/services.yaml @@ -56,3 +56,14 @@ incomplete_all: clear_completed_items: name: Clear completed items description: Clear completed items from the shopping list. + +sort: + name: Sort all items + description: Sort all items by name in the shopping list. + fields: + reverse: + name: Sort reverse + description: Whether to sort in reverse (descending) order. + default: false + selector: + boolean: diff --git a/tests/components/shopping_list/test_init.py b/tests/components/shopping_list/test_init.py index bfd781f03c6..e5f1e30efdb 100644 --- a/tests/components/shopping_list/test_init.py +++ b/tests/components/shopping_list/test_init.py @@ -5,12 +5,14 @@ import pytest from homeassistant.components.shopping_list import NoMatchingShoppingListItem from homeassistant.components.shopping_list.const import ( + ATTR_REVERSE, DOMAIN, EVENT_SHOPPING_LIST_UPDATED, SERVICE_ADD_ITEM, SERVICE_CLEAR_COMPLETED_ITEMS, SERVICE_COMPLETE_ITEM, SERVICE_REMOVE_ITEM, + SERVICE_SORT, ) from homeassistant.components.websocket_api.const import ( ERR_INVALID_FORMAT, @@ -657,8 +659,6 @@ async def test_add_item_service(hass: HomeAssistant, sl_setup) -> None: {ATTR_NAME: "beer"}, blocking=True, ) - await hass.async_block_till_done() - assert len(hass.data[DOMAIN].items) == 1 assert len(events) == 1 @@ -672,15 +672,12 @@ async def test_remove_item_service(hass: HomeAssistant, sl_setup) -> None: {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 @@ -690,8 +687,6 @@ async def test_remove_item_service(hass: HomeAssistant, sl_setup) -> None: {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 @@ -706,7 +701,6 @@ async def test_clear_completed_items_service(hass: HomeAssistant, sl_setup) -> N {ATTR_NAME: "beer"}, blocking=True, ) - await hass.async_block_till_done() assert len(hass.data[DOMAIN].items) == 1 assert len(events) == 1 @@ -717,7 +711,6 @@ async def test_clear_completed_items_service(hass: HomeAssistant, sl_setup) -> N {ATTR_NAME: "beer"}, blocking=True, ) - await hass.async_block_till_done() assert len(hass.data[DOMAIN].items) == 1 assert len(events) == 1 @@ -728,6 +721,44 @@ async def test_clear_completed_items_service(hass: HomeAssistant, sl_setup) -> N {}, blocking=True, ) - await hass.async_block_till_done() assert len(hass.data[DOMAIN].items) == 0 assert len(events) == 1 + + +async def test_sort_list_service(hass: HomeAssistant, sl_setup) -> None: + """Test sort_all service.""" + + for name in ("zzz", "ddd", "aaa"): + await hass.services.async_call( + DOMAIN, + SERVICE_ADD_ITEM, + {ATTR_NAME: name}, + blocking=True, + ) + + # sort ascending + events = async_capture_events(hass, EVENT_SHOPPING_LIST_UPDATED) + await hass.services.async_call( + DOMAIN, + SERVICE_SORT, + {ATTR_REVERSE: False}, + blocking=True, + ) + + assert hass.data[DOMAIN].items[0][ATTR_NAME] == "aaa" + assert hass.data[DOMAIN].items[1][ATTR_NAME] == "ddd" + assert hass.data[DOMAIN].items[2][ATTR_NAME] == "zzz" + assert len(events) == 1 + + # sort descending + await hass.services.async_call( + DOMAIN, + SERVICE_SORT, + {ATTR_REVERSE: True}, + blocking=True, + ) + + assert hass.data[DOMAIN].items[0][ATTR_NAME] == "zzz" + assert hass.data[DOMAIN].items[1][ATTR_NAME] == "ddd" + assert hass.data[DOMAIN].items[2][ATTR_NAME] == "aaa" + assert len(events) == 2