Compare commits

...

1 Commits

Author SHA1 Message Date
Stefan Agner
e243840558 Debounce Matter cover state writes for split attribute updates 2026-02-26 12:23:31 +01:00
2 changed files with 72 additions and 0 deletions

View File

@@ -2,7 +2,9 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from enum import IntEnum
from math import floor
from typing import Any
@@ -21,6 +23,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.event import async_call_later
from .const import LOGGER
from .entity import MatterEntity, MatterEntityDescription
@@ -70,6 +73,7 @@ class MatterCoverEntityDescription(CoverEntityDescription, MatterEntityDescripti
class MatterCover(MatterEntity, CoverEntity):
"""Representation of a Matter Cover."""
_cancel_write_state: Callable[[], None] | None = None
entity_description: MatterCoverEntityDescription
@property
@@ -114,6 +118,30 @@ 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()
if self._cancel_write_state is not None:
self._cancel_write_state()
self._cancel_write_state = async_call_later(
self.hass, 0.1, self._async_write_state_later
)
@callback
def _async_write_state_later(self, now: datetime) -> None:
"""Write the Home Assistant state after debouncing attribute updates."""
self._cancel_write_state = None
self.async_write_ha_state()
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._cancel_write_state is not None:
self._cancel_write_state()
self._cancel_write_state = 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"),
[