mirror of
https://github.com/home-assistant/core.git
synced 2026-10-07 06:50:41 -04:00
Fix entity removal leaks in matter (#183864)
This commit is contained in:
@@ -112,7 +112,6 @@ class MatterEntity(Entity):
|
||||
self._endpoint = endpoint
|
||||
self._entity_info = entity_info
|
||||
self.entity_description = entity_info.entity_description
|
||||
self._unsubscribes: list[Callable] = []
|
||||
# for fast lookups we create a mapping to the attribute paths
|
||||
self._attributes_map: dict[type, str] = {}
|
||||
# The server info is set when the client connects to the server.
|
||||
@@ -213,7 +212,7 @@ class MatterEntity(Entity):
|
||||
continue
|
||||
self._attributes_map[attr_cls] = attr_path
|
||||
sub_paths.append(attr_path)
|
||||
self._unsubscribes.append(
|
||||
self.async_on_remove(
|
||||
self.matter_client.subscribe_events(
|
||||
callback=self._on_matter_event,
|
||||
event_filter=EventType.ATTRIBUTE_UPDATED,
|
||||
@@ -222,7 +221,7 @@ class MatterEntity(Entity):
|
||||
)
|
||||
)
|
||||
# subscribe to node (availability changes)
|
||||
self._unsubscribes.append(
|
||||
self.async_on_remove(
|
||||
self.matter_client.subscribe_events(
|
||||
callback=self._on_matter_event,
|
||||
event_filter=EventType.NODE_UPDATED,
|
||||
@@ -240,7 +239,7 @@ class MatterEntity(Entity):
|
||||
)
|
||||
if reachable_attr_path not in sub_paths:
|
||||
sub_paths.append(reachable_attr_path)
|
||||
self._unsubscribes.append(
|
||||
self.async_on_remove(
|
||||
self.matter_client.subscribe_events(
|
||||
callback=self._on_matter_event,
|
||||
event_filter=EventType.ATTRIBUTE_UPDATED,
|
||||
@@ -259,7 +258,7 @@ class MatterEntity(Entity):
|
||||
)
|
||||
if parent_reachable_attr_path not in sub_paths:
|
||||
sub_paths.append(parent_reachable_attr_path)
|
||||
self._unsubscribes.append(
|
||||
self.async_on_remove(
|
||||
self.matter_client.subscribe_events(
|
||||
callback=self._on_matter_event,
|
||||
event_filter=EventType.ATTRIBUTE_UPDATED,
|
||||
@@ -268,7 +267,7 @@ class MatterEntity(Entity):
|
||||
)
|
||||
)
|
||||
# subscribe to FeatureMap attribute (as that can dynamically change)
|
||||
self._unsubscribes.append(
|
||||
self.async_on_remove(
|
||||
self.matter_client.subscribe_events(
|
||||
callback=self._on_featuremap_update,
|
||||
event_filter=EventType.ATTRIBUTE_UPDATED,
|
||||
|
||||
@@ -97,7 +97,7 @@ class MatterEventEntity(MatterEntity, EventEntity):
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# subscribe to NodeEvent events
|
||||
self._unsubscribes.append(
|
||||
self.async_on_remove(
|
||||
self.matter_client.subscribe_events(
|
||||
callback=self._on_matter_node_event,
|
||||
event_filter=EventType.NODE_EVENT,
|
||||
|
||||
@@ -93,7 +93,7 @@ class MatterLock(MatterEntity, LockEntity):
|
||||
"""Subscribe to events."""
|
||||
await super().async_added_to_hass()
|
||||
# subscribe to NodeEvent events
|
||||
self._unsubscribes.append(
|
||||
self.async_on_remove(
|
||||
self.matter_client.subscribe_events(
|
||||
callback=self._on_matter_node_event,
|
||||
event_filter=EventType.NODE_EVENT,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Test Matter entity behavior."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from matter_server.client.models.node import MatterNode
|
||||
@@ -581,3 +582,64 @@ async def test_composed_entity_subscribes_to_parent_reachable_attribute(
|
||||
f"{_COMPOSED_PARENT_REACHABLE_ATTR_PATH!r}"
|
||||
" and event_filter=ATTRIBUTE_UPDATED, but none was found."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("node_fixture", "entity_id", "expected_event_filters"),
|
||||
[
|
||||
pytest.param(
|
||||
"mock_onoff_light",
|
||||
"light.mock_onoff_light",
|
||||
{EventType.ATTRIBUTE_UPDATED, EventType.NODE_UPDATED},
|
||||
id="base_entity",
|
||||
),
|
||||
pytest.param(
|
||||
"mock_generic_switch",
|
||||
"event.mock_generic_switch_button",
|
||||
{EventType.ATTRIBUTE_UPDATED, EventType.NODE_UPDATED, EventType.NODE_EVENT},
|
||||
id="event",
|
||||
),
|
||||
pytest.param(
|
||||
"mock_door_lock",
|
||||
"lock.mock_door_lock",
|
||||
{EventType.ATTRIBUTE_UPDATED, EventType.NODE_UPDATED, EventType.NODE_EVENT},
|
||||
id="lock",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_entity_removal_unsubscribes_events(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
matter_client: MagicMock,
|
||||
node_fixture: str,
|
||||
entity_id: str,
|
||||
expected_event_filters: set[EventType],
|
||||
) -> None:
|
||||
"""Test removing an entity unsubscribes all its Matter client subscriptions."""
|
||||
subscriptions: list[tuple[dict[str, Any], MagicMock]] = []
|
||||
|
||||
def _subscribe_events(**kwargs: Any) -> MagicMock:
|
||||
unsubscribe = MagicMock()
|
||||
subscriptions.append((kwargs, unsubscribe))
|
||||
return unsubscribe
|
||||
|
||||
matter_client.subscribe_events.side_effect = _subscribe_events
|
||||
await setup_integration_with_node_fixture(hass, node_fixture, matter_client)
|
||||
|
||||
entity_subscriptions = [
|
||||
(kwargs["event_filter"], unsubscribe)
|
||||
for kwargs, unsubscribe in subscriptions
|
||||
# Only bound entity methods have an entity_id; other callbacks are skipped.
|
||||
if getattr(getattr(kwargs["callback"], "__self__", None), "entity_id", None)
|
||||
== entity_id
|
||||
]
|
||||
assert {event_filter for event_filter, _ in entity_subscriptions} == (
|
||||
expected_event_filters
|
||||
)
|
||||
assert not any(unsubscribe.called for _, unsubscribe in entity_subscriptions)
|
||||
|
||||
entity_registry.async_remove(entity_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(entity_id) is None
|
||||
assert all(unsubscribe.called for _, unsubscribe in entity_subscriptions)
|
||||
|
||||
Reference in New Issue
Block a user