Deduplicate TodoListEntity item updates on state writes (#182741)

This commit is contained in:
Allen Porter
2026-09-25 14:01:31 +02:00
committed by GitHub
parent 5df5347cc3
commit 8708fcdbf6
2 changed files with 73 additions and 5 deletions
+8 -5
View File
@@ -257,6 +257,7 @@ class TodoListEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
_attr_todo_items: list[TodoItem] | None = None
_update_listeners: list[Callable[[list[TodoItem] | None], None]] | None = None
_last_broadcast_items: list[TodoItem] | None = None
@property
@override
@@ -316,14 +317,16 @@ class TodoListEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@callback
def async_update_listeners(self) -> None:
"""Push updated To-do items to all listeners."""
items = self.todo_items
if items == self._last_broadcast_items:
return
self._last_broadcast_items = (
[copy.copy(item) for item in items] if items is not None else None
)
if not self._update_listeners:
return
items = self.todo_items
todo_items = [copy.copy(item) for item in items] if items is not None else None
for listener in self._update_listeners:
listener(todo_items)
listener(self._last_broadcast_items)
@callback
@override
+65
View File
@@ -2,9 +2,11 @@
import dataclasses
import datetime
from datetime import timedelta
from typing import Any
import zoneinfo
from freezegun.api import FrozenDateTimeFactory
import probatio
import pytest
from syrupy.assertion import SnapshotAssertion
@@ -36,6 +38,7 @@ from homeassistant.setup import async_setup_component
from . import create_mock_platform
from tests.common import async_fire_time_changed
from tests.typing import WebSocketGenerator
TEST_TIMEZONE = zoneinfo.ZoneInfo("America/Regina")
@@ -1314,6 +1317,7 @@ async def test_async_subscribe_updates(
hass: HomeAssistant, test_entity: TodoListEntity
) -> None:
"""Test async_subscribe_updates delivers list updates to listeners."""
test_entity._attr_todo_items = []
await create_mock_platform(hass, [test_entity])
received_updates: list[list[TodoItem] | None] = []
@@ -1324,10 +1328,15 @@ async def test_async_subscribe_updates(
unsub = test_entity.async_subscribe_updates(listener)
# Trigger an update
test_entity._attr_todo_items = [
TodoItem(summary="Item #1", uid="1", status=TodoItemStatus.NEEDS_ACTION),
TodoItem(summary="Item #2", uid="2", status=TodoItemStatus.COMPLETED),
]
test_entity.async_write_ha_state()
assert len(received_updates) == 1
items = received_updates[0]
assert items is not None
assert len(items) == 2
assert isinstance(items[0], TodoItem)
assert items[0].summary == "Item #1"
@@ -1374,5 +1383,61 @@ async def test_async_subscribe_updates(
# Unsubscribe and verify no more updates
unsub()
test_entity._attr_todo_items = []
test_entity.async_write_ha_state()
assert len(received_updates) == 4
async def test_subscribe_duplicate_state_write_does_not_broadcast(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
test_entity: TodoListEntity,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test that unchanged items on update interval do not broadcast duplicates."""
await create_mock_platform(hass, [test_entity])
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{
"type": "todo/item/subscribe",
"entity_id": test_entity.entity_id,
}
)
msg = await client.receive_json()
assert msg["success"]
subscription_id = msg["id"]
# Initial update
msg = await client.receive_json()
assert msg["id"] == subscription_id
assert msg["type"] == "event"
assert len(msg["event"]["items"]) == 2
# Advance time to trigger the entity platform's polling update interval (60s)
freezer.tick(timedelta(seconds=60))
async_fire_time_changed(hass)
await hass.async_block_till_done()
# The update interval ran, but items did not change: no broadcast should occur.
# The next message received should be the pong for our ping.
await client.send_json_auto_id({"type": "ping"})
msg = await client.receive_json()
assert msg["type"] == "pong"
# Now update items and advance time again
assert test_entity.todo_items is not None
test_entity._attr_todo_items = [
*test_entity.todo_items,
TodoItem(summary="Item #3", uid="3", status=TodoItemStatus.NEEDS_ACTION),
]
freezer.tick(timedelta(seconds=60))
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Items changed, so a broadcast should now occur
msg = await client.receive_json()
assert msg["id"] == subscription_id
assert msg["type"] == "event"
assert len(msg["event"]["items"]) == 3