From 5f13faac76419b2c0f278cd615a3bb1535b55306 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 15 Nov 2023 02:41:29 -0800 Subject: [PATCH] Add the todo.get_items service (#103285) --- homeassistant/components/todo/__init__.py | 30 +++++++++- homeassistant/components/todo/services.yaml | 15 +++++ homeassistant/components/todo/strings.json | 10 ++++ tests/components/todo/test_init.py | 65 ++++++++++++++++++++- 4 files changed, 116 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/todo/__init__.py b/homeassistant/components/todo/__init__.py index 968256ce3d9..1bd050b0872 100644 --- a/homeassistant/components/todo/__init__.py +++ b/homeassistant/components/todo/__init__.py @@ -11,7 +11,7 @@ from homeassistant.components import frontend, websocket_api from homeassistant.components.websocket_api import ERR_NOT_FOUND, ERR_NOT_SUPPORTED from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_ID -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 @@ -58,7 +58,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: vol.Required("item"): vol.All(cv.string, vol.Length(min=1)), vol.Optional("rename"): vol.All(cv.string, vol.Length(min=1)), vol.Optional("status"): vol.In( - {TodoItemStatus.NEEDS_ACTION, TodoItemStatus.COMPLETED} + {TodoItemStatus.NEEDS_ACTION, TodoItemStatus.COMPLETED}, ), } ), @@ -77,6 +77,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _async_remove_todo_items, required_features=[TodoListEntityFeature.DELETE_TODO_ITEM], ) + component.async_register_entity_service( + "get_items", + cv.make_entity_service_schema( + { + vol.Optional("status"): vol.All( + cv.ensure_list, + [vol.In({TodoItemStatus.NEEDS_ACTION, TodoItemStatus.COMPLETED})], + ), + } + ), + _async_get_todo_items, + supports_response=SupportsResponse.ONLY, + ) await component.async_setup(config) return True @@ -258,3 +271,16 @@ async def _async_remove_todo_items(entity: TodoListEntity, call: ServiceCall) -> raise ValueError(f"Unable to find To-do item '{item}") uids.append(found.uid) await entity.async_delete_todo_items(uids=uids) + + +async def _async_get_todo_items( + entity: TodoListEntity, call: ServiceCall +) -> dict[str, Any]: + """Return items in the To-do list.""" + return { + "items": [ + dataclasses.asdict(item) + for item in entity.todo_items or () + if not (statuses := call.data.get("status")) or item.status in statuses + ] + } diff --git a/homeassistant/components/todo/services.yaml b/homeassistant/components/todo/services.yaml index 1bdb8aca779..2030229f8d9 100644 --- a/homeassistant/components/todo/services.yaml +++ b/homeassistant/components/todo/services.yaml @@ -1,3 +1,18 @@ +get_items: + target: + entity: + domain: todo + fields: + status: + example: "needs_action" + default: needs_action + selector: + select: + translation_key: status + options: + - needs_action + - completed + multiple: true add_item: target: entity: diff --git a/homeassistant/components/todo/strings.json b/homeassistant/components/todo/strings.json index 6ba8aaba1a5..30058b28c56 100644 --- a/homeassistant/components/todo/strings.json +++ b/homeassistant/components/todo/strings.json @@ -6,6 +6,16 @@ } }, "services": { + "get_items": { + "name": "Get to-do list items", + "description": "Get items on a to-do list.", + "fields": { + "status": { + "name": "Status", + "description": "Only return to-do items with the specified statuses. Returns not completed actions by default." + } + } + }, "add_item": { "name": "Add to-do list item", "description": "Add a new to-do list item.", diff --git a/tests/components/todo/test_init.py b/tests/components/todo/test_init.py index 33f9af2b0c5..e6d4a8d1d06 100644 --- a/tests/components/todo/test_init.py +++ b/tests/components/todo/test_init.py @@ -33,6 +33,16 @@ from tests.common import ( from tests.typing import WebSocketGenerator TEST_DOMAIN = "test" +ITEM_1 = { + "uid": "1", + "summary": "Item #1", + "status": "needs_action", +} +ITEM_2 = { + "uid": "2", + "summary": "Item #2", + "status": "completed", +} class MockFlow(ConfigFlow): @@ -182,12 +192,63 @@ async def test_list_todo_items( assert resp.get("success") assert resp.get("result") == { "items": [ - {"summary": "Item #1", "uid": "1", "status": "needs_action"}, - {"summary": "Item #2", "uid": "2", "status": "completed"}, + ITEM_1, + ITEM_2, ] } +@pytest.mark.parametrize( + ("service_data", "expected_items"), + [ + ({}, [ITEM_1, ITEM_2]), + ( + [ + {"status": [TodoItemStatus.COMPLETED, TodoItemStatus.NEEDS_ACTION]}, + [ITEM_1, ITEM_2], + ] + ), + ( + [ + {"status": [TodoItemStatus.NEEDS_ACTION]}, + [ITEM_1], + ] + ), + ( + [ + {"status": [TodoItemStatus.COMPLETED]}, + [ITEM_2], + ] + ), + ], +) +async def test_get_items_service( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + test_entity: TodoListEntity, + service_data: dict[str, Any], + expected_items: list[dict[str, Any]], +) -> None: + """Test listing items in a To-do list from a service call.""" + + await create_mock_platform(hass, [test_entity]) + + state = hass.states.get("todo.entity1") + assert state + assert state.state == "1" + assert state.attributes == {"supported_features": 15} + + result = await hass.services.async_call( + DOMAIN, + "get_items", + service_data, + target={"entity_id": "todo.entity1"}, + blocking=True, + return_response=True, + ) + assert result == {"todo.entity1": {"items": expected_items}} + + async def test_unsupported_websocket( hass: HomeAssistant, hass_ws_client: WebSocketGenerator,