Compare commits

...

2 Commits

Author SHA1 Message Date
Stefan Agner
69949713ed Use Debouncer for Matter cover state writes
Replace ad-hoc delayed write logic with the shared Debouncer helper

to keep the 100ms coalescing behavior and centralized shutdown handling.
2026-02-27 21:27:45 +01:00
Stefan Agner
e243840558 Debounce Matter cover state writes for split attribute updates 2026-02-26 12:23:31 +01:00
2 changed files with 73 additions and 0 deletions

View File

@@ -20,6 +20,7 @@ from homeassistant.components.cover import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import LOGGER
@@ -29,6 +30,7 @@ from .models import MatterDiscoverySchema
# The MASK used for extracting bits 0 to 1 of the byte.
OPERATIONAL_STATUS_MASK = 0b11
WRITE_STATE_DEBOUNCE_SECONDS = 0.1
# map Matter window cover types to HA device class
TYPE_MAP = {
@@ -70,8 +72,20 @@ class MatterCoverEntityDescription(CoverEntityDescription, MatterEntityDescripti
class MatterCover(MatterEntity, CoverEntity):
"""Representation of a Matter Cover."""
_write_state_debouncer: Debouncer[None] | None = None
entity_description: MatterCoverEntityDescription
async def async_added_to_hass(self) -> None:
"""Run when entity has been added to Home Assistant."""
await super().async_added_to_hass()
self._write_state_debouncer = Debouncer(
self.hass,
LOGGER,
cooldown=WRITE_STATE_DEBOUNCE_SECONDS,
immediate=False,
function=self.async_write_ha_state,
)
@property
def is_closed(self) -> bool | None:
"""Return true if cover is closed, if there is no position report, return None."""
@@ -114,6 +128,21 @@ class MatterCover(MatterEntity, CoverEntity):
clusters.WindowCovering.Commands.GoToTiltPercentage((100 - position) * 100)
)
@callback
def _on_matter_event(self, event: Any, data: Any = None) -> None:
"""Handle updates from the device."""
self._attr_available = self._endpoint.node.available
self._update_from_device()
assert self._write_state_debouncer is not None
self._write_state_debouncer.async_schedule_call()
async def async_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from Home Assistant."""
await super().async_will_remove_from_hass()
if self._write_state_debouncer is not None:
self._write_state_debouncer.async_shutdown()
self._write_state_debouncer = None
@callback
def _update_from_device(self) -> None:
"""Update from device."""

View File

@@ -1,5 +1,6 @@
"""Test Matter covers."""
import asyncio
from math import floor
from unittest.mock import MagicMock, call
@@ -229,6 +230,49 @@ async def test_cover_position_aware_lift(
assert state.state == CoverState.CLOSED
@pytest.mark.parametrize(
("node_fixture", "entity_id"),
[
("mock_window_covering_pa_lift", "cover.longan_link_wncv_da01"),
],
)
async def test_cover_position_aware_lift_debounce(
hass: HomeAssistant,
matter_client: MagicMock,
matter_node: MatterNode,
entity_id: str,
) -> None:
"""Test cover state update is debounced for split attribute updates."""
set_node_attribute(matter_node, 1, 258, 14, 9900)
set_node_attribute(matter_node, 1, 258, 10, 0b001010)
await trigger_subscription_callback(hass, matter_client)
await asyncio.sleep(0.11)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state
assert state.state == CoverState.CLOSING
set_node_attribute(matter_node, 1, 258, 10, 0b000000)
await trigger_subscription_callback(hass, matter_client)
state = hass.states.get(entity_id)
assert state
assert state.state == CoverState.CLOSING
set_node_attribute(matter_node, 1, 258, 14, 10000)
await trigger_subscription_callback(hass, matter_client)
await asyncio.sleep(0.11)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state
assert state.state == CoverState.CLOSED
@pytest.mark.parametrize(
("node_fixture", "entity_id"),
[