Fix todo item triggers after config entry reload (#181255)

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
Co-authored-by: abmantis <amfcalt@gmail.com>
This commit is contained in:
Wallace Fan
2026-09-08 12:52:55 +01:00
committed by GitHub
co-authored by Cursor Abílio Costa abmantis
parent 1d9ae4b184
commit 5059007bfe
2 changed files with 130 additions and 4 deletions
+44 -2
View File
@@ -10,11 +10,24 @@ from typing import TYPE_CHECKING, cast, override
import voluptuous as vol
from homeassistant.const import ATTR_ENTITY_ID, CONF_OPTIONS, CONF_TARGET
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback, split_entity_id
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_OPTIONS,
CONF_TARGET,
STATE_UNAVAILABLE,
)
from homeassistant.core import (
CALLBACK_TYPE,
Event,
EventStateChangedData,
HomeAssistant,
callback,
split_entity_id,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.target import TargetEntityChangeTracker, TargetSelection
from homeassistant.helpers.trigger import (
Trigger,
@@ -79,6 +92,7 @@ class ItemChangeListener(TargetEntityChangeTracker):
self._pending_listener_task: asyncio.Task[None] | None = None
self._unsubscribe_listeners: list[CALLBACK_TYPE] = []
self._state_change_unsub: CALLBACK_TYPE | None = None
@override
@callback
@@ -114,6 +128,31 @@ class ItemChangeListener(TargetEntityChangeTracker):
)
self._unsubscribe_listeners.append(unsub)
if self._state_change_unsub:
self._state_change_unsub()
self._state_change_unsub = None
if tracked_entities:
self._state_change_unsub = async_track_state_change_event(
self._hass, tracked_entities, self._async_todo_entity_state_changed
)
@callback
def _async_todo_entity_state_changed(
self, event: Event[EventStateChangedData]
) -> None:
"""Handle entities becoming available.
This is required so that when the config entry is reloaded,
we start listening on the newly created classes.
"""
new_state = event.data["new_state"]
old_state = event.data["old_state"]
if new_state is None or new_state.state == STATE_UNAVAILABLE:
return
if old_state is not None and old_state.state != STATE_UNAVAILABLE:
return
self._handle_entities_update(self._referenced_entities())
@override
@callback
def _unsubscribe(self) -> None:
@@ -122,6 +161,9 @@ class ItemChangeListener(TargetEntityChangeTracker):
if self._pending_listener_task:
self._pending_listener_task.cancel()
self._pending_listener_task = None
if self._state_change_unsub:
self._state_change_unsub()
self._state_change_unsub = None
for unsub in self._unsubscribe_listeners:
unsub()
+86 -2
View File
@@ -12,6 +12,7 @@ from homeassistant.components.todo import (
TodoListEntityFeature,
)
from homeassistant.components.todo.const import ATTR_ITEM, ATTR_STATUS, TodoServices
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_AREA_ID,
ATTR_DEVICE_ID,
@@ -31,11 +32,17 @@ from homeassistant.helpers import (
floor_registry as fr,
label_registry as lr,
)
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.setup import async_setup_component
from . import MockTodoListEntity, create_mock_platform
from . import TEST_DOMAIN, MockTodoListEntity, create_mock_platform
from tests.common import async_mock_service, mock_device_registry
from tests.common import (
MockPlatform,
async_mock_service,
mock_device_registry,
mock_platform,
)
from tests.components.common import assert_trigger_options_supported
TODO_ENTITY_ID1 = "todo.list_one"
@@ -474,6 +481,83 @@ async def test_new_entity_added_to_target_fires_triggers(
)
async def test_item_change_triggers_after_config_entry_reload(
hass: HomeAssistant,
service_calls: list[ServiceCall],
) -> None:
"""Test triggers still fire after the todo list's config entry is reloaded.
Reload recreates the entity object without changing the registry entry, so
the trigger must re-subscribe rather than keep the old object.
"""
await _setup_automation(hass, {CONF_ENTITY_ID: TODO_ENTITY_ID1})
config_entry = hass.config_entries.async_entries(TEST_DOMAIN)[0]
async def async_setup_entry_platform(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
async_add_entities(
[
_make_entity(
TODO_ENTITY_ID1,
unique_id="list_one",
items=[
TodoItem(
summary="existing_item",
uid="existing_id",
status=TodoItemStatus.NEEDS_ACTION,
)
],
),
_make_entity(TODO_ENTITY_ID2, unique_id="list_two"),
]
)
mock_platform(
hass,
f"{TEST_DOMAIN}.{DOMAIN}",
MockPlatform(async_setup_entry=async_setup_entry_platform),
)
assert await hass.config_entries.async_reload(config_entry.entry_id)
await hass.async_block_till_done()
await _add_item(hass, TODO_ENTITY_ID1, "item_after_reload")
_assert_service_calls(
service_calls,
[{"platform": "todo.item_added", "entity_id": TODO_ENTITY_ID1}],
)
item_id = service_calls[0].data["item_ids"][0]
service_calls.clear()
await _complete_item(hass, TODO_ENTITY_ID1, item_id)
_assert_service_calls(
service_calls,
[
{
"platform": "todo.item_completed",
"entity_id": TODO_ENTITY_ID1,
"item_ids": [item_id],
}
],
)
service_calls.clear()
await _remove_item(hass, TODO_ENTITY_ID1, item_id)
_assert_service_calls(
service_calls,
[
{
"platform": "todo.item_removed",
"entity_id": TODO_ENTITY_ID1,
"item_ids": [item_id],
}
],
)
async def test_trigger_skips_missing_entity(
hass: HomeAssistant,
service_calls: list[ServiceCall],