diff --git a/homeassistant/components/todo/__init__.py b/homeassistant/components/todo/__init__.py index 86004f931ccc..c7fd6236abc7 100644 --- a/homeassistant/components/todo/__init__.py +++ b/homeassistant/components/todo/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Iterable +import copy import dataclasses import datetime import logging @@ -28,7 +29,6 @@ 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 ( ATTR_DESCRIPTION, @@ -240,7 +240,7 @@ class TodoListEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """An entity that represents a To-do list.""" _attr_todo_items: list[TodoItem] | None = None - _update_listeners: list[Callable[[list[JsonValueType] | None], None]] | None = None + _update_listeners: list[Callable[[list[TodoItem]], None]] | None = None @property def state(self) -> int | None: @@ -281,13 +281,9 @@ class TodoListEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): @final @callback def async_subscribe_updates( - self, - listener: Callable[[list[JsonValueType] | None], None], + self, listener: Callable[[list[TodoItem]], None] ) -> CALLBACK_TYPE: - """Subscribe to To-do list item updates. - - Called by websocket API. - """ + """Subscribe to To-do list item updates.""" if self._update_listeners is None: self._update_listeners = [] self._update_listeners.append(listener) @@ -306,9 +302,7 @@ class TodoListEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): if not self._update_listeners: return - todo_items: list[JsonValueType] = [ - dataclasses.asdict(item) for item in self.todo_items or () - ] + todo_items = [copy.copy(item) for item in self.todo_items or []] for listener in self._update_listeners: listener(todo_items) @@ -341,13 +335,13 @@ async def websocket_handle_subscribe_todo_items( return @callback - def todo_item_listener(todo_items: list[JsonValueType] | None) -> None: + def todo_item_listener(todo_items: list[TodoItem]) -> None: """Push updated To-do list items to websocket.""" connection.send_message( websocket_api.event_message( msg["id"], { - "items": todo_items, + "items": [dataclasses.asdict(item) for item in todo_items], }, ) ) @@ -357,7 +351,7 @@ async def websocket_handle_subscribe_todo_items( ) connection.send_result(msg["id"]) - # Push an initial forecast update + # Push an initial list update entity.async_update_listeners() diff --git a/tests/components/todo/test_init.py b/tests/components/todo/test_init.py index 4b1fdcf1d926..b67fca49c4c8 100644 --- a/tests/components/todo/test_init.py +++ b/tests/components/todo/test_init.py @@ -1231,3 +1231,53 @@ async def test_list_todo_items_extended_fields( ] } } + + +async def test_async_subscribe_updates( + hass: HomeAssistant, test_entity: TodoListEntity +) -> None: + """Test async_subscribe_updates delivers list updates to listeners.""" + await create_mock_platform(hass, [test_entity]) + + received_updates: list[list[TodoItem]] = [] + + def listener(items: list[TodoItem]) -> None: + received_updates.append(items) + + unsub = test_entity.async_subscribe_updates(listener) + + # Trigger an update + test_entity.async_write_ha_state() + + assert len(received_updates) == 1 + items = received_updates[0] + assert len(items) == 2 + assert isinstance(items[0], TodoItem) + assert items[0].summary == "Item #1" + assert items[0].uid == "1" + assert items[0].status == TodoItemStatus.NEEDS_ACTION + assert isinstance(items[1], TodoItem) + assert items[1].summary == "Item #2" + assert items[1].uid == "2" + assert items[1].status == TodoItemStatus.COMPLETED + + # Verify items are copies (not the same objects) + assert items[0] is not test_entity.todo_items[0] + assert items[1] is not test_entity.todo_items[1] + + # Add a new item and trigger update + test_entity._attr_todo_items = [ + *test_entity._attr_todo_items, + TodoItem(summary="Item #3", uid="3", status=TodoItemStatus.NEEDS_ACTION), + ] + test_entity.async_write_ha_state() + + assert len(received_updates) == 2 + items = received_updates[1] + assert len(items) == 3 + assert items[2].summary == "Item #3" + + # Unsubscribe and verify no more updates + unsub() + test_entity.async_write_ha_state() + assert len(received_updates) == 2