Add To-do due date and description fields (#104128)

* Add To-do due date and description fields

* Fix due date schema

* Revert devcontainer change

* Split due date and due date time

* Add tests for config validation function

* Add timezone converstion tests

* Add local todo due date/time and description implementation

* Revert configuration

* Revert test changes

* Add comments for the todo item field description

* Rename function _validate_supported_features

* Fix issues in items factory

* Readability improvements

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Rename CONF to ATTR usages

* Simplify local time validator

* Rename TodoListEntityFeature fields for setting extended fields

* Remove duplicate validations

* Update subscribe test

* Fix local_todo tests

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Allen Porter 2023-11-28 04:01:12 -08:00 committed by GitHub
parent 2a4a5d0a07
commit b8cc3349be
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 554 additions and 29 deletions

View file

@ -90,6 +90,9 @@ class LocalTodoListEntity(TodoListEntity):
| TodoListEntityFeature.DELETE_TODO_ITEM
| TodoListEntityFeature.UPDATE_TODO_ITEM
| TodoListEntityFeature.MOVE_TODO_ITEM
| TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM
| TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
)
_attr_should_poll = False
@ -115,6 +118,8 @@ class LocalTodoListEntity(TodoListEntity):
status=ICS_TODO_STATUS_MAP.get(
item.status or TodoStatus.NEEDS_ACTION, TodoItemStatus.NEEDS_ACTION
),
due=item.due,
description=item.description,
)
for item in self._calendar.todos
]

View file

@ -1,6 +1,6 @@
"""The todo integration."""
from collections.abc import Callable
from collections.abc import Callable, Iterable
import dataclasses
import datetime
import logging
@ -28,9 +28,18 @@ from homeassistant.helpers.config_validation import ( # noqa: F401
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
from homeassistant.util.json import JsonValueType
from .const import DOMAIN, TodoItemStatus, TodoListEntityFeature
from .const import (
ATTR_DESCRIPTION,
ATTR_DUE,
ATTR_DUE_DATE,
ATTR_DUE_DATE_TIME,
DOMAIN,
TodoItemStatus,
TodoListEntityFeature,
)
_LOGGER = logging.getLogger(__name__)
@ -39,6 +48,65 @@ SCAN_INTERVAL = datetime.timedelta(seconds=60)
ENTITY_ID_FORMAT = DOMAIN + ".{}"
@dataclasses.dataclass
class TodoItemFieldDescription:
"""A description of To-do item fields and validation requirements."""
service_field: str
"""Field name for service calls."""
todo_item_field: str
"""Field name for TodoItem."""
validation: Callable[[Any], Any]
"""Voluptuous validation function."""
required_feature: TodoListEntityFeature
"""Entity feature that enables this field."""
TODO_ITEM_FIELDS = [
TodoItemFieldDescription(
service_field=ATTR_DUE_DATE,
validation=cv.date,
todo_item_field=ATTR_DUE,
required_feature=TodoListEntityFeature.SET_DUE_DATE_ON_ITEM,
),
TodoItemFieldDescription(
service_field=ATTR_DUE_DATE_TIME,
validation=vol.All(cv.datetime, dt_util.as_local),
todo_item_field=ATTR_DUE,
required_feature=TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM,
),
TodoItemFieldDescription(
service_field=ATTR_DESCRIPTION,
validation=cv.string,
todo_item_field=ATTR_DESCRIPTION,
required_feature=TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM,
),
]
TODO_ITEM_FIELD_SCHEMA = {
vol.Optional(desc.service_field): desc.validation for desc in TODO_ITEM_FIELDS
}
TODO_ITEM_FIELD_VALIDATIONS = [
cv.has_at_most_one_key(ATTR_DUE_DATE, ATTR_DUE_DATE_TIME)
]
def _validate_supported_features(
supported_features: int | None, call_data: dict[str, Any]
) -> None:
"""Validate service call fields against entity supported features."""
for desc in TODO_ITEM_FIELDS:
if desc.service_field not in call_data:
continue
if not supported_features or not supported_features & desc.required_feature:
raise ValueError(
f"Entity does not support setting field '{desc.service_field}'"
)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Todo entities."""
component = hass.data[DOMAIN] = EntityComponent[TodoListEntity](
@ -53,9 +121,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
component.async_register_entity_service(
"add_item",
{
vol.Required("item"): vol.All(cv.string, vol.Length(min=1)),
},
vol.All(
cv.make_entity_service_schema(
{
vol.Required("item"): vol.All(cv.string, vol.Length(min=1)),
**TODO_ITEM_FIELD_SCHEMA,
}
),
*TODO_ITEM_FIELD_VALIDATIONS,
),
_async_add_todo_item,
required_features=[TodoListEntityFeature.CREATE_TODO_ITEM],
)
@ -69,9 +143,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
vol.Optional("status"): vol.In(
{TodoItemStatus.NEEDS_ACTION, TodoItemStatus.COMPLETED},
),
**TODO_ITEM_FIELD_SCHEMA,
}
),
cv.has_at_least_one_key("rename", "status"),
*TODO_ITEM_FIELD_VALIDATIONS,
cv.has_at_least_one_key(
"rename", "status", *[desc.service_field for desc in TODO_ITEM_FIELDS]
),
),
_async_update_todo_item,
required_features=[TodoListEntityFeature.UPDATE_TODO_ITEM],
@ -135,6 +213,20 @@ class TodoItem:
status: TodoItemStatus | None = None
"""A status or confirmation of the To-do item."""
due: datetime.date | datetime.datetime | None = None
"""The date and time that a to-do is expected to be completed.
This field may be a date or datetime depending whether the entity feature
DUE_DATE or DUE_DATETIME are set.
"""
description: str | None = None
"""A more complete description of than that provided by the summary.
This field may be set when TodoListEntityFeature.DESCRIPTION is supported by
the entity.
"""
class TodoListEntity(Entity):
"""An entity that represents a To-do list."""
@ -262,6 +354,19 @@ async def websocket_handle_subscribe_todo_items(
entity.async_update_listeners()
def _api_items_factory(obj: Iterable[tuple[str, Any]]) -> dict[str, str]:
"""Convert CalendarEvent dataclass items to dictionary of attributes."""
result: dict[str, str] = {}
for name, value in obj:
if value is None:
continue
if isinstance(value, (datetime.date, datetime.datetime)):
result[name] = value.isoformat()
else:
result[name] = str(value)
return result
@websocket_api.websocket_command(
{
vol.Required("type"): "todo/item/list",
@ -285,7 +390,13 @@ async def websocket_handle_todo_item_list(
items: list[TodoItem] = entity.todo_items or []
connection.send_message(
websocket_api.result_message(
msg["id"], {"items": [dataclasses.asdict(item) for item in items]}
msg["id"],
{
"items": [
dataclasses.asdict(item, dict_factory=_api_items_factory)
for item in items
]
},
)
)
@ -342,8 +453,17 @@ def _find_by_uid_or_summary(
async def _async_add_todo_item(entity: TodoListEntity, call: ServiceCall) -> None:
"""Add an item to the To-do list."""
_validate_supported_features(entity.supported_features, call.data)
await entity.async_create_todo_item(
item=TodoItem(summary=call.data["item"], status=TodoItemStatus.NEEDS_ACTION)
item=TodoItem(
summary=call.data["item"],
status=TodoItemStatus.NEEDS_ACTION,
**{
desc.todo_item_field: call.data[desc.service_field]
for desc in TODO_ITEM_FIELDS
if desc.service_field in call.data
},
)
)
@ -354,11 +474,20 @@ async def _async_update_todo_item(entity: TodoListEntity, call: ServiceCall) ->
if not found:
raise ValueError(f"Unable to find To-do item '{item}'")
update_item = TodoItem(
uid=found.uid, summary=call.data.get("rename"), status=call.data.get("status")
)
_validate_supported_features(entity.supported_features, call.data)
await entity.async_update_todo_item(item=update_item)
await entity.async_update_todo_item(
item=TodoItem(
uid=found.uid,
summary=call.data.get("rename"),
status=call.data.get("status"),
**{
desc.todo_item_field: call.data[desc.service_field]
for desc in TODO_ITEM_FIELDS
if desc.service_field in call.data
},
)
)
async def _async_remove_todo_items(entity: TodoListEntity, call: ServiceCall) -> None:
@ -378,7 +507,7 @@ async def _async_get_todo_items(
"""Return items in the To-do list."""
return {
"items": [
dataclasses.asdict(item)
dataclasses.asdict(item, dict_factory=_api_items_factory)
for item in entity.todo_items or ()
if not (statuses := call.data.get("status")) or item.status in statuses
]

View file

@ -4,6 +4,11 @@ from enum import IntFlag, StrEnum
DOMAIN = "todo"
ATTR_DUE = "due"
ATTR_DUE_DATE = "due_date"
ATTR_DUE_DATE_TIME = "due_date_time"
ATTR_DESCRIPTION = "description"
class TodoListEntityFeature(IntFlag):
"""Supported features of the To-do List entity."""
@ -12,6 +17,9 @@ class TodoListEntityFeature(IntFlag):
DELETE_TODO_ITEM = 2
UPDATE_TODO_ITEM = 4
MOVE_TODO_ITEM = 8
SET_DUE_DATE_ON_ITEM = 16
SET_DUE_DATETIME_ON_ITEM = 32
SET_DESCRIPTION_ON_ITEM = 64
class TodoItemStatus(StrEnum):

View file

@ -25,6 +25,18 @@ add_item:
example: "Submit income tax return"
selector:
text:
due_date:
example: "2023-11-17"
selector:
date:
due_date_time:
example: "2023-11-17 13:30:00"
selector:
datetime:
description:
example: "A more complete description of the to-do item than that provided by the summary."
selector:
text:
update_item:
target:
entity:
@ -49,6 +61,18 @@ update_item:
options:
- needs_action
- completed
due_date:
example: "2023-11-17"
selector:
date:
due_date_time:
example: "2023-11-17 13:30:00"
selector:
datetime:
description:
example: "A more complete description of the to-do item than that provided by the summary."
selector:
text:
remove_item:
target:
entity:

View file

@ -23,6 +23,18 @@
"item": {
"name": "Item name",
"description": "The name that represents the to-do item."
},
"due_date": {
"name": "Due date",
"description": "The date the to-do item is expected to be completed."
},
"due_date_time": {
"name": "Due date time",
"description": "The date and time the to-do item is expected to be completed."
},
"description": {
"name": "Description",
"description": "A more complete description of the to-do item than provided by the item name."
}
}
},
@ -41,6 +53,18 @@
"status": {
"name": "Set status",
"description": "A status or confirmation of the to-do item."
},
"due_date": {
"name": "Due date",
"description": "The date the to-do item is expected to be completed."
},
"due_date_time": {
"name": "Due date time",
"description": "The date and time the to-do item is expected to be completed."
},
"description": {
"name": "Description",
"description": "A more complete description of the to-do item than provided by the item name."
}
}
},

View file

@ -2,6 +2,7 @@
from collections.abc import Awaitable, Callable
import textwrap
from typing import Any
import pytest
@ -58,11 +59,31 @@ async def ws_move_item(
return move
@pytest.fixture(autouse=True)
def set_time_zone(hass: HomeAssistant) -> None:
"""Set the time zone for the tests that keesp UTC-6 all year round."""
hass.config.set_time_zone("America/Regina")
@pytest.mark.parametrize(
("item_data", "expected_item_data"),
[
({}, {}),
({"due_date": "2023-11-17"}, {"due": "2023-11-17"}),
(
{"due_date_time": "2023-11-17T11:30:00+00:00"},
{"due": "2023-11-17T05:30:00-06:00"},
),
({"description": "Additional detail"}, {"description": "Additional detail"}),
],
)
async def test_add_item(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
setup_integration: None,
ws_get_items: Callable[[], Awaitable[dict[str, str]]],
item_data: dict[str, Any],
expected_item_data: dict[str, Any],
) -> None:
"""Test adding a todo item."""
@ -73,7 +94,7 @@ async def test_add_item(
await hass.services.async_call(
TODO_DOMAIN,
"add_item",
{"item": "replace batteries"},
{"item": "replace batteries", **item_data},
target={"entity_id": TEST_ENTITY},
blocking=True,
)
@ -82,6 +103,8 @@ async def test_add_item(
assert len(items) == 1
assert items[0]["summary"] == "replace batteries"
assert items[0]["status"] == "needs_action"
for k, v in expected_item_data.items():
assert items[0][k] == v
assert "uid" in items[0]
state = hass.states.get(TEST_ENTITY)
@ -89,16 +112,30 @@ async def test_add_item(
assert state.state == "1"
@pytest.mark.parametrize(
("item_data", "expected_item_data"),
[
({}, {}),
({"due_date": "2023-11-17"}, {"due": "2023-11-17"}),
(
{"due_date_time": "2023-11-17T11:30:00+00:00"},
{"due": "2023-11-17T05:30:00-06:00"},
),
({"description": "Additional detail"}, {"description": "Additional detail"}),
],
)
async def test_remove_item(
hass: HomeAssistant,
setup_integration: None,
ws_get_items: Callable[[], Awaitable[dict[str, str]]],
item_data: dict[str, Any],
expected_item_data: dict[str, Any],
) -> None:
"""Test removing a todo item."""
await hass.services.async_call(
TODO_DOMAIN,
"add_item",
{"item": "replace batteries"},
{"item": "replace batteries", **item_data},
target={"entity_id": TEST_ENTITY},
blocking=True,
)
@ -107,6 +144,8 @@ async def test_remove_item(
assert len(items) == 1
assert items[0]["summary"] == "replace batteries"
assert items[0]["status"] == "needs_action"
for k, v in expected_item_data.items():
assert items[0][k] == v
assert "uid" in items[0]
state = hass.states.get(TEST_ENTITY)
@ -168,10 +207,30 @@ async def test_bulk_remove(
assert state.state == "0"
@pytest.mark.parametrize(
("item_data", "expected_item_data", "expected_state"),
[
({"status": "completed"}, {"status": "completed"}, "0"),
({"due_date": "2023-11-17"}, {"due": "2023-11-17"}, "1"),
(
{"due_date_time": "2023-11-17T11:30:00+00:00"},
{"due": "2023-11-17T05:30:00-06:00"},
"1",
),
(
{"description": "Additional detail"},
{"description": "Additional detail"},
"1",
),
],
)
async def test_update_item(
hass: HomeAssistant,
setup_integration: None,
ws_get_items: Callable[[], Awaitable[dict[str, str]]],
item_data: dict[str, Any],
expected_item_data: dict[str, Any],
expected_state: str,
) -> None:
"""Test updating a todo item."""
@ -199,21 +258,22 @@ async def test_update_item(
await hass.services.async_call(
TODO_DOMAIN,
"update_item",
{"item": item["uid"], "status": "completed"},
{"item": item["uid"], **item_data},
target={"entity_id": TEST_ENTITY},
blocking=True,
)
# Verify item is marked as completed
# Verify item is updated
items = await ws_get_items()
assert len(items) == 1
item = items[0]
assert item["summary"] == "soda"
assert item["status"] == "completed"
for k, v in expected_item_data.items():
assert items[0][k] == v
state = hass.states.get(TEST_ENTITY)
assert state
assert state.state == "0"
assert state.state == expected_state
async def test_rename(

View file

@ -1,8 +1,10 @@
"""Tests for the todo integration."""
from collections.abc import Generator
import datetime
from typing import Any
from unittest.mock import AsyncMock
import zoneinfo
import pytest
import voluptuous as vol
@ -43,6 +45,8 @@ ITEM_2 = {
"summary": "Item #2",
"status": "completed",
}
TEST_TIMEZONE = zoneinfo.ZoneInfo("America/Regina")
TEST_OFFSET = "-06:00"
class MockFlow(ConfigFlow):
@ -108,6 +112,12 @@ def mock_setup_integration(hass: HomeAssistant) -> None:
)
@pytest.fixture(autouse=True)
def set_time_zone(hass: HomeAssistant) -> None:
"""Set the time zone for the tests that keesp UTC-6 all year round."""
hass.config.set_time_zone("America/Regina")
async def create_mock_platform(
hass: HomeAssistant,
entities: list[TodoListEntity],
@ -263,7 +273,7 @@ async def test_unsupported_websocket(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test a To-do list that does not support features."""
"""Test a To-do list for an entity that does not exist."""
entity1 = TodoListEntity()
entity1.entity_id = "todo.entity1"
@ -327,23 +337,42 @@ async def test_add_item_service_raises(
@pytest.mark.parametrize(
("item_data", "expected_error"),
("item_data", "expected_exception", "expected_error"),
[
({}, "required key not provided"),
({"item": ""}, "length of value must be at least 1"),
({}, vol.Invalid, "required key not provided"),
({"item": ""}, vol.Invalid, "length of value must be at least 1"),
(
{"item": "Submit forms", "description": "Submit tax forms"},
ValueError,
"does not support setting field 'description'",
),
(
{"item": "Submit forms", "due_date": "2023-11-17"},
ValueError,
"does not support setting field 'due_date'",
),
(
{
"item": "Submit forms",
"due_date_time": f"2023-11-17T17:00:00{TEST_OFFSET}",
},
ValueError,
"does not support setting field 'due_date_time'",
),
],
)
async def test_add_item_service_invalid_input(
hass: HomeAssistant,
test_entity: TodoListEntity,
item_data: dict[str, Any],
expected_exception: str,
expected_error: str,
) -> None:
"""Test invalid input to the add item service."""
await create_mock_platform(hass, [test_entity])
with pytest.raises(vol.Invalid, match=expected_error):
with pytest.raises(expected_exception, match=expected_error):
await hass.services.async_call(
DOMAIN,
"add_item",
@ -353,6 +382,82 @@ async def test_add_item_service_invalid_input(
)
@pytest.mark.parametrize(
("supported_entity_feature", "item_data", "expected_item"),
(
(
TodoListEntityFeature.SET_DUE_DATE_ON_ITEM,
{"item": "New item", "due_date": "2023-11-13"},
TodoItem(
summary="New item",
status=TodoItemStatus.NEEDS_ACTION,
due=datetime.date(2023, 11, 13),
),
),
(
TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM,
{"item": "New item", "due_date_time": f"2023-11-13T17:00:00{TEST_OFFSET}"},
TodoItem(
summary="New item",
status=TodoItemStatus.NEEDS_ACTION,
due=datetime.datetime(2023, 11, 13, 17, 00, 00, tzinfo=TEST_TIMEZONE),
),
),
(
TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM,
{"item": "New item", "due_date_time": "2023-11-13T17:00:00+00:00"},
TodoItem(
summary="New item",
status=TodoItemStatus.NEEDS_ACTION,
due=datetime.datetime(2023, 11, 13, 11, 00, 00, tzinfo=TEST_TIMEZONE),
),
),
(
TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM,
{"item": "New item", "due_date_time": "2023-11-13"},
TodoItem(
summary="New item",
status=TodoItemStatus.NEEDS_ACTION,
due=datetime.datetime(2023, 11, 13, 0, 00, 00, tzinfo=TEST_TIMEZONE),
),
),
(
TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM,
{"item": "New item", "description": "Submit revised draft"},
TodoItem(
summary="New item",
status=TodoItemStatus.NEEDS_ACTION,
description="Submit revised draft",
),
),
),
)
async def test_add_item_service_extended_fields(
hass: HomeAssistant,
test_entity: TodoListEntity,
supported_entity_feature: int,
item_data: dict[str, Any],
expected_item: TodoItem,
) -> None:
"""Test adding an item in a To-do list."""
test_entity._attr_supported_features |= supported_entity_feature
await create_mock_platform(hass, [test_entity])
await hass.services.async_call(
DOMAIN,
"add_item",
{"item": "New item", **item_data},
target={"entity_id": "todo.entity1"},
blocking=True,
)
args = test_entity.async_create_todo_item.call_args
assert args
item = args.kwargs.get("item")
assert item == expected_item
async def test_update_todo_item_service_by_id(
hass: HomeAssistant,
test_entity: TodoListEntity,
@ -555,6 +660,82 @@ async def test_update_item_service_invalid_input(
)
@pytest.mark.parametrize(
("update_data"),
[
({"due_date_time": f"2023-11-13T17:00:00{TEST_OFFSET}"}),
({"due_date": "2023-11-13"}),
({"description": "Submit revised draft"}),
],
)
async def test_update_todo_item_field_unsupported(
hass: HomeAssistant,
test_entity: TodoListEntity,
update_data: dict[str, Any],
) -> None:
"""Test updating an item in a To-do list."""
await create_mock_platform(hass, [test_entity])
with pytest.raises(ValueError, match="does not support"):
await hass.services.async_call(
DOMAIN,
"update_item",
{"item": "1", **update_data},
target={"entity_id": "todo.entity1"},
blocking=True,
)
@pytest.mark.parametrize(
("supported_entity_feature", "update_data", "expected_update"),
(
(
TodoListEntityFeature.SET_DUE_DATE_ON_ITEM,
{"due_date": "2023-11-13"},
TodoItem(uid="1", due=datetime.date(2023, 11, 13)),
),
(
TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM,
{"due_date_time": f"2023-11-13T17:00:00{TEST_OFFSET}"},
TodoItem(
uid="1",
due=datetime.datetime(2023, 11, 13, 17, 0, 0, tzinfo=TEST_TIMEZONE),
),
),
(
TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM,
{"description": "Submit revised draft"},
TodoItem(uid="1", description="Submit revised draft"),
),
),
)
async def test_update_todo_item_extended_fields(
hass: HomeAssistant,
test_entity: TodoListEntity,
supported_entity_feature: int,
update_data: dict[str, Any],
expected_update: TodoItem,
) -> None:
"""Test updating an item in a To-do list."""
test_entity._attr_supported_features |= supported_entity_feature
await create_mock_platform(hass, [test_entity])
await hass.services.async_call(
DOMAIN,
"update_item",
{"item": "1", **update_data},
target={"entity_id": "todo.entity1"},
blocking=True,
)
args = test_entity.async_update_todo_item.call_args
assert args
item = args.kwargs.get("item")
assert item == expected_update
async def test_remove_todo_item_service_by_id(
hass: HomeAssistant,
test_entity: TodoListEntity,
@ -971,8 +1152,20 @@ async def test_subscribe(
event_message = msg["event"]
assert event_message == {
"items": [
{"summary": "Item #1", "uid": "1", "status": "needs_action"},
{"summary": "Item #2", "uid": "2", "status": "completed"},
{
"summary": "Item #1",
"uid": "1",
"status": "needs_action",
"due": None,
"description": None,
},
{
"summary": "Item #2",
"uid": "2",
"status": "completed",
"due": None,
"description": None,
},
]
}
test_entity._attr_todo_items = [
@ -985,9 +1178,27 @@ async def test_subscribe(
event_message = msg["event"]
assert event_message == {
"items": [
{"summary": "Item #1", "uid": "1", "status": "needs_action"},
{"summary": "Item #2", "uid": "2", "status": "completed"},
{"summary": "Item #3", "uid": "3", "status": "needs_action"},
{
"summary": "Item #1",
"uid": "1",
"status": "needs_action",
"due": None,
"description": None,
},
{
"summary": "Item #2",
"uid": "2",
"status": "completed",
"due": None,
"description": None,
},
{
"summary": "Item #3",
"uid": "3",
"status": "needs_action",
"due": None,
"description": None,
},
]
}
@ -1023,3 +1234,67 @@ async def test_subscribe_entity_does_not_exist(
"code": "invalid_entity_id",
"message": "To-do list entity not found: todo.unknown",
}
@pytest.mark.parametrize(
("item_data", "expected_item_data"),
[
({"due": datetime.date(2023, 11, 17)}, {"due": "2023-11-17"}),
(
{"due": datetime.datetime(2023, 11, 17, 17, 0, 0, tzinfo=TEST_TIMEZONE)},
{"due": f"2023-11-17T17:00:00{TEST_OFFSET}"},
),
({"description": "Some description"}, {"description": "Some description"}),
],
)
async def test_list_todo_items_extended_fields(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
test_entity: TodoListEntity,
item_data: dict[str, Any],
expected_item_data: dict[str, Any],
) -> None:
"""Test listing items in a To-do list with extended fields."""
test_entity._attr_todo_items = [
TodoItem(
**ITEM_1,
**item_data,
),
]
await create_mock_platform(hass, [test_entity])
client = await hass_ws_client(hass)
await client.send_json(
{"id": 1, "type": "todo/item/list", "entity_id": "todo.entity1"}
)
resp = await client.receive_json()
assert resp.get("id") == 1
assert resp.get("success")
assert resp.get("result") == {
"items": [
{
**ITEM_1,
**expected_item_data,
},
]
}
result = await hass.services.async_call(
DOMAIN,
"get_items",
{},
target={"entity_id": "todo.entity1"},
blocking=True,
return_response=True,
)
assert result == {
"todo.entity1": {
"items": [
{
**ITEM_1,
**expected_item_data,
},
]
}
}