Skip to content

Commit

Permalink
Fix state for Matter Locks (including optional door sensor) (#121665)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelveldt authored and frenck committed Jul 10, 2024
1 parent 9c83af3 commit 2151086
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 27 deletions.
16 changes: 16 additions & 0 deletions homeassistant/components/matter/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,20 @@ def _update_from_device(self) -> None:
required_attributes=(clusters.BooleanState.Attributes.StateValue,),
device_type=(device_types.RainSensor,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="LockDoorStateSensor",
device_class=BinarySensorDeviceClass.DOOR,
# pylint: disable=unnecessary-lambda
measurement_to_ha=lambda x: {
clusters.DoorLock.Enums.DoorStateEnum.kDoorOpen: True,
clusters.DoorLock.Enums.DoorStateEnum.kDoorJammed: True,
clusters.DoorLock.Enums.DoorStateEnum.kDoorForcedOpen: True,
clusters.DoorLock.Enums.DoorStateEnum.kDoorClosed: False,
}.get(x),
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.DoorLock.Attributes.DoorState,),
),
]
73 changes: 46 additions & 27 deletions homeassistant/components/matter/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import asyncio
from typing import Any

from chip.clusters import Objects as clusters
Expand Down Expand Up @@ -38,6 +39,7 @@ class MatterLock(MatterEntity, LockEntity):
"""Representation of a Matter lock."""

features: int | None = None
_optimistic_timer: asyncio.TimerHandle | None = None

@property
def code_format(self) -> str | None:
Expand Down Expand Up @@ -90,9 +92,15 @@ async def send_device_command(

async def async_lock(self, **kwargs: Any) -> None:
"""Lock the lock with pin if needed."""
# optimistically signal locking to state machine
self._attr_is_locking = True
self.async_write_ha_state()
if not self._attr_is_locked:
# optimistically signal locking to state machine
self._attr_is_locking = True
self.async_write_ha_state()
# the lock should acknowledge the command with an attribute update
# but bad things may happen, so guard against it with a timer.
self._optimistic_timer = self.hass.loop.call_later(
5, self._reset_optimistic_state
)
code: str | None = kwargs.get(ATTR_CODE)
code_bytes = code.encode() if code else None
await self.send_device_command(
Expand All @@ -101,9 +109,15 @@ async def async_lock(self, **kwargs: Any) -> None:

async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the lock with pin if needed."""
# optimistically signal unlocking to state machine
self._attr_is_unlocking = True
self.async_write_ha_state()
if self._attr_is_locked:
# optimistically signal unlocking to state machine
self._attr_is_unlocking = True
self.async_write_ha_state()
# the lock should acknowledge the command with an attribute update
# but bad things may happen, so guard against it with a timer.
self._optimistic_timer = self.hass.loop.call_later(
5, self._reset_optimistic_state
)
code: str | None = kwargs.get(ATTR_CODE)
code_bytes = code.encode() if code else None
if self.supports_unbolt:
Expand All @@ -120,9 +134,14 @@ async def async_unlock(self, **kwargs: Any) -> None:

async def async_open(self, **kwargs: Any) -> None:
"""Open the door latch."""
# optimistically signal unlocking to state machine
self._attr_is_unlocking = True
# optimistically signal opening to state machine
self._attr_is_opening = True
self.async_write_ha_state()
# the lock should acknowledge the command with an attribute update
# but bad things may happen, so guard against it with a timer.
self._optimistic_timer = self.hass.loop.call_later(
5, self._reset_optimistic_state
)
code: str | None = kwargs.get(ATTR_CODE)
code_bytes = code.encode() if code else None
await self.send_device_command(
Expand All @@ -145,38 +164,38 @@ def _update_from_device(self) -> None:
)

# always reset the optimisically (un)locking state on state update
self._attr_is_locking = False
self._attr_is_unlocking = False
self._reset_optimistic_state(write_state=False)

LOGGER.debug("Lock state: %s for %s", lock_state, self.entity_id)

if lock_state is clusters.DoorLock.Enums.DlLockState.kUnlatched:
self._attr_is_locked = False
self._attr_is_open = True
if lock_state is clusters.DoorLock.Enums.DlLockState.kLocked:
self._attr_is_locked = True
self._attr_is_open = False
elif lock_state in (
clusters.DoorLock.Enums.DlLockState.kUnlocked,
clusters.DoorLock.Enums.DlLockState.kUnlatched,
clusters.DoorLock.Enums.DlLockState.kNotFullyLocked,
):
self._attr_is_locked = False
self._attr_is_open = False
else:
# According to the matter docs a null state can happen during device startup.
# Treat any other state as unknown.
# NOTE: A null state can happen during device startup.
self._attr_is_locked = None
self._attr_is_open = None

if self.supports_door_position_sensor:
door_state = self.get_matter_attribute_value(
clusters.DoorLock.Attributes.DoorState
)

assert door_state is not None

LOGGER.debug("Door state: %s for %s", door_state, self.entity_id)

self._attr_is_jammed = (
door_state is clusters.DoorLock.Enums.DoorStateEnum.kDoorJammed
)
self._attr_is_open = (
door_state is clusters.DoorLock.Enums.DoorStateEnum.kDoorOpen
)
@callback
def _reset_optimistic_state(self, write_state: bool = True) -> None:
if self._optimistic_timer and not self._optimistic_timer.cancelled():
self._optimistic_timer.cancel()
self._optimistic_timer = None
self._attr_is_locking = False
self._attr_is_unlocking = False
self._attr_is_opening = False
if write_state:
self.async_write_ha_state()


DISCOVERY_SCHEMAS = [
Expand Down

0 comments on commit 2151086

Please sign in to comment.