Home Assistant Unofficial Reference 2024.12.1
lock.py
Go to the documentation of this file.
1 """Matter lock."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from typing import Any
7 
8 from chip.clusters import Objects as clusters
9 
11  LockEntity,
12  LockEntityDescription,
13  LockEntityFeature,
14 )
15 from homeassistant.config_entries import ConfigEntry
16 from homeassistant.const import ATTR_CODE, Platform
17 from homeassistant.core import HomeAssistant, callback
18 from homeassistant.helpers.entity_platform import AddEntitiesCallback
19 
20 from .const import LOGGER
21 from .entity import MatterEntity
22 from .helpers import get_matter
23 from .models import MatterDiscoverySchema
24 
25 DoorLockFeature = clusters.DoorLock.Bitmaps.Feature
26 
27 
29  hass: HomeAssistant,
30  config_entry: ConfigEntry,
31  async_add_entities: AddEntitiesCallback,
32 ) -> None:
33  """Set up Matter lock from Config Entry."""
34  matter = get_matter(hass)
35  matter.register_platform_handler(Platform.LOCK, async_add_entities)
36 
37 
39  """Representation of a Matter lock."""
40 
41  _feature_map: int | None = None
42  _optimistic_timer: asyncio.TimerHandle | None = None
43  _platform_translation_key = "lock"
44 
45  @property
46  def code_format(self) -> str | None:
47  """Regex for code format or None if no code is required."""
48  if self.get_matter_attribute_valueget_matter_attribute_value(
49  clusters.DoorLock.Attributes.RequirePINforRemoteOperation
50  ):
51  min_pincode_length = int(
52  self.get_matter_attribute_valueget_matter_attribute_value(
53  clusters.DoorLock.Attributes.MinPINCodeLength
54  )
55  )
56  max_pincode_length = int(
57  self.get_matter_attribute_valueget_matter_attribute_value(
58  clusters.DoorLock.Attributes.MaxPINCodeLength
59  )
60  )
61  return f"^\\d{{{min_pincode_length},{max_pincode_length}}}$"
62 
63  return None
64 
66  self,
67  command: clusters.ClusterCommand,
68  timed_request_timeout_ms: int = 1000,
69  ) -> None:
70  """Send a command to the device."""
71  await self.matter_clientmatter_client.send_device_command(
72  node_id=self._endpoint_endpoint.node.node_id,
73  endpoint_id=self._endpoint_endpoint.endpoint_id,
74  command=command,
75  timed_request_timeout_ms=timed_request_timeout_ms,
76  )
77 
78  async def async_lock(self, **kwargs: Any) -> None:
79  """Lock the lock with pin if needed."""
80  if not self._attr_is_locked_attr_is_locked:
81  # optimistically signal locking to state machine
82  self._attr_is_locking_attr_is_locking = True
83  self.async_write_ha_stateasync_write_ha_state()
84  # the lock should acknowledge the command with an attribute update
85  # but bad things may happen, so guard against it with a timer.
86  self._optimistic_timer_optimistic_timer = self.hasshass.loop.call_later(
87  30, self._reset_optimistic_state_reset_optimistic_state
88  )
89  code: str | None = kwargs.get(ATTR_CODE)
90  code_bytes = code.encode() if code else None
91  await self.send_device_commandsend_device_command(
92  command=clusters.DoorLock.Commands.LockDoor(code_bytes)
93  )
94 
95  async def async_unlock(self, **kwargs: Any) -> None:
96  """Unlock the lock with pin if needed."""
97  if self._attr_is_locked_attr_is_locked:
98  # optimistically signal unlocking to state machine
99  self._attr_is_unlocking_attr_is_unlocking = True
100  self.async_write_ha_stateasync_write_ha_state()
101  # the lock should acknowledge the command with an attribute update
102  # but bad things may happen, so guard against it with a timer.
103  self._optimistic_timer_optimistic_timer = self.hasshass.loop.call_later(
104  30, self._reset_optimistic_state_reset_optimistic_state
105  )
106  code: str | None = kwargs.get(ATTR_CODE)
107  code_bytes = code.encode() if code else None
108  if self._attr_supported_features_attr_supported_features & LockEntityFeature.OPEN:
109  # if the lock reports it has separate unbolt support,
110  # the unlock command should unbolt only on the unlock command
111  # and unlatch on the HA 'open' command.
112  await self.send_device_commandsend_device_command(
113  command=clusters.DoorLock.Commands.UnboltDoor(code_bytes)
114  )
115  else:
116  await self.send_device_commandsend_device_command(
117  command=clusters.DoorLock.Commands.UnlockDoor(code_bytes)
118  )
119 
120  async def async_open(self, **kwargs: Any) -> None:
121  """Open the door latch."""
122  # optimistically signal opening to state machine
123  self._attr_is_opening_attr_is_opening = True
124  self.async_write_ha_stateasync_write_ha_state()
125  # the lock should acknowledge the command with an attribute update
126  # but bad things may happen, so guard against it with a timer.
127  self._optimistic_timer_optimistic_timer = self.hasshass.loop.call_later(
128  30 if self._attr_is_locked_attr_is_locked else 5, self._reset_optimistic_state_reset_optimistic_state
129  )
130  code: str | None = kwargs.get(ATTR_CODE)
131  code_bytes = code.encode() if code else None
132  await self.send_device_commandsend_device_command(
133  command=clusters.DoorLock.Commands.UnlockDoor(code_bytes)
134  )
135 
136  @callback
137  def _update_from_device(self) -> None:
138  """Update the entity from the device."""
139  # always calculate the features as they can dynamically change
140  self._calculate_features_calculate_features()
141 
142  lock_state = self.get_matter_attribute_valueget_matter_attribute_value(
143  clusters.DoorLock.Attributes.LockState
144  )
145 
146  # always reset the optimisically (un)locking state on state update
147  self._reset_optimistic_state_reset_optimistic_state(write_state=False)
148 
149  LOGGER.debug("Lock state: %s for %s", lock_state, self.entity_identity_id)
150 
151  if lock_state == clusters.DoorLock.Enums.DlLockState.kUnlatched:
152  self._attr_is_locked_attr_is_locked = False
153  self._attr_is_open_attr_is_open = True
154  elif lock_state == clusters.DoorLock.Enums.DlLockState.kLocked:
155  self._attr_is_locked_attr_is_locked = True
156  self._attr_is_open_attr_is_open = False
157  elif lock_state in (
158  clusters.DoorLock.Enums.DlLockState.kUnlocked,
159  clusters.DoorLock.Enums.DlLockState.kNotFullyLocked,
160  ):
161  self._attr_is_locked_attr_is_locked = False
162  self._attr_is_open_attr_is_open = False
163  else:
164  # Treat any other state as unknown.
165  # NOTE: A null state can happen during device startup.
166  self._attr_is_locked_attr_is_locked = None
167  self._attr_is_open_attr_is_open = None
168 
169  @callback
170  def _reset_optimistic_state(self, write_state: bool = True) -> None:
171  if self._optimistic_timer_optimistic_timer and not self._optimistic_timer_optimistic_timer.cancelled():
172  self._optimistic_timer_optimistic_timer.cancel()
173  self._optimistic_timer_optimistic_timer = None
174  self._attr_is_locking_attr_is_locking = False
175  self._attr_is_unlocking_attr_is_unlocking = False
176  self._attr_is_opening_attr_is_opening = False
177  if write_state:
178  self.async_write_ha_stateasync_write_ha_state()
179 
180  @callback
182  self,
183  ) -> None:
184  """Calculate features for HA Lock platform from Matter FeatureMap."""
185  feature_map = int(
186  self.get_matter_attribute_valueget_matter_attribute_value(clusters.DoorLock.Attributes.FeatureMap)
187  )
188  # NOTE: the featuremap can dynamically change, so we need to update the
189  # supported features if the featuremap changes.
190  if self._feature_map_feature_map == feature_map:
191  return
192  self._feature_map_feature_map = feature_map
193  supported_features = LockEntityFeature(0)
194  # determine if lock supports optional open/unbolt feature
195  if bool(feature_map & DoorLockFeature.kUnbolt):
196  supported_features |= LockEntityFeature.OPEN
197  self._attr_supported_features_attr_supported_features = supported_features
198 
199 
200 DISCOVERY_SCHEMAS = [
202  platform=Platform.LOCK,
203  entity_description=LockEntityDescription(
204  key="MatterLock",
205  name=None,
206  ),
207  entity_class=MatterLock,
208  required_attributes=(clusters.DoorLock.Attributes.LockState,),
209  ),
210 ]
Any get_matter_attribute_value(self, type[ClusterAttributeDescriptor] attribute, bool null_as_none=True)
Definition: entity.py:206
None async_unlock(self, **Any kwargs)
Definition: lock.py:95
None send_device_command(self, clusters.ClusterCommand command, int timed_request_timeout_ms=1000)
Definition: lock.py:69
None _reset_optimistic_state(self, bool write_state=True)
Definition: lock.py:170
None async_open(self, **Any kwargs)
Definition: lock.py:120
None async_lock(self, **Any kwargs)
Definition: lock.py:78
MatterAdapter get_matter(HomeAssistant hass)
Definition: helpers.py:35
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: lock.py:32