Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Component to interface with locks that can be controlled remotely."""
2 
3 from __future__ import annotations
4 
5 from datetime import timedelta
6 from enum import IntFlag
7 import functools as ft
8 import logging
9 import re
10 from typing import TYPE_CHECKING, Any, final
11 
12 from propcache import cached_property
13 import voluptuous as vol
14 
15 from homeassistant.config_entries import ConfigEntry
16 from homeassistant.const import ( # noqa: F401
17  _DEPRECATED_STATE_JAMMED,
18  _DEPRECATED_STATE_LOCKED,
19  _DEPRECATED_STATE_LOCKING,
20  _DEPRECATED_STATE_UNLOCKED,
21  _DEPRECATED_STATE_UNLOCKING,
22  ATTR_CODE,
23  ATTR_CODE_FORMAT,
24  SERVICE_LOCK,
25  SERVICE_OPEN,
26  SERVICE_UNLOCK,
27  STATE_OPEN,
28  STATE_OPENING,
29 )
30 from homeassistant.core import HomeAssistant, callback
31 from homeassistant.exceptions import ServiceValidationError
34  DeprecatedConstantEnum,
35  all_with_deprecated_constants,
36  check_if_deprecated_constant,
37  dir_with_deprecated_constants,
38 )
39 from homeassistant.helpers.entity import Entity, EntityDescription
40 from homeassistant.helpers.entity_component import EntityComponent
41 from homeassistant.helpers.typing import ConfigType, StateType
42 from homeassistant.util.hass_dict import HassKey
43 
44 from .const import DOMAIN, LockState
45 
46 _LOGGER = logging.getLogger(__name__)
47 
48 DATA_COMPONENT: HassKey[EntityComponent[LockEntity]] = HassKey(DOMAIN)
49 ENTITY_ID_FORMAT = DOMAIN + ".{}"
50 PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
51 PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
52 SCAN_INTERVAL = timedelta(seconds=30)
53 
54 ATTR_CHANGED_BY = "changed_by"
55 CONF_DEFAULT_CODE = "default_code"
56 
57 MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
58 
59 LOCK_SERVICE_SCHEMA = cv.make_entity_service_schema(
60  {vol.Optional(ATTR_CODE): cv.string}
61 )
62 
63 
64 class LockEntityFeature(IntFlag):
65  """Supported features of the lock entity."""
66 
67  OPEN = 1
68 
69 
70 # The SUPPORT_OPEN constant is deprecated as of Home Assistant 2022.5.
71 # Please use the LockEntityFeature enum instead.
72 _DEPRECATED_SUPPORT_OPEN = DeprecatedConstantEnum(LockEntityFeature.OPEN, "2025.1")
73 
74 PROP_TO_ATTR = {"changed_by": ATTR_CHANGED_BY, "code_format": ATTR_CODE_FORMAT}
75 
76 # mypy: disallow-any-generics
77 
78 
79 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
80  """Track states and offer events for locks."""
81  component = hass.data[DATA_COMPONENT] = EntityComponent[LockEntity](
82  _LOGGER, DOMAIN, hass, SCAN_INTERVAL
83  )
84 
85  await component.async_setup(config)
86 
87  component.async_register_entity_service(
88  SERVICE_UNLOCK, LOCK_SERVICE_SCHEMA, "async_handle_unlock_service"
89  )
90  component.async_register_entity_service(
91  SERVICE_LOCK, LOCK_SERVICE_SCHEMA, "async_handle_lock_service"
92  )
93  component.async_register_entity_service(
94  SERVICE_OPEN,
95  LOCK_SERVICE_SCHEMA,
96  "async_handle_open_service",
97  [LockEntityFeature.OPEN],
98  )
99 
100  return True
101 
102 
103 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
104  """Set up a config entry."""
105  return await hass.data[DATA_COMPONENT].async_setup_entry(entry)
106 
107 
108 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
109  """Unload a config entry."""
110  return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
111 
112 
113 class LockEntityDescription(EntityDescription, frozen_or_thawed=True):
114  """A class that describes lock entities."""
115 
116 
117 CACHED_PROPERTIES_WITH_ATTR_ = {
118  "changed_by",
119  "code_format",
120  "is_locked",
121  "is_locking",
122  "is_unlocking",
123  "is_open",
124  "is_opening",
125  "is_jammed",
126  "supported_features",
127 }
128 
129 
130 class LockEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
131  """Base class for lock entities."""
132 
133  entity_description: LockEntityDescription
134  _attr_changed_by: str | None = None
135  _attr_code_format: str | None = None
136  _attr_is_locked: bool | None = None
137  _attr_is_locking: bool | None = None
138  _attr_is_open: bool | None = None
139  _attr_is_opening: bool | None = None
140  _attr_is_unlocking: bool | None = None
141  _attr_is_jammed: bool | None = None
142  _attr_state: None = None
143  _attr_supported_features: LockEntityFeature = LockEntityFeature(0)
144  _lock_option_default_code: str = ""
145  __code_format_cmp: re.Pattern[str] | None = None
146 
147  @final
148  @callback
149  def add_default_code(self, data: dict[Any, Any]) -> dict[Any, Any]:
150  """Add default lock code."""
151  code: str = data.pop(ATTR_CODE, "")
152  if not code:
153  code = self._lock_option_default_code_lock_option_default_code
154  if self.code_format_cmpcode_format_cmp and not self.code_format_cmpcode_format_cmp.match(code):
155  if TYPE_CHECKING:
156  assert self.code_formatcode_format
157  raise ServiceValidationError(
158  translation_domain=DOMAIN,
159  translation_key="add_default_code",
160  translation_placeholders={
161  "entity_id": self.entity_identity_id,
162  "code_format": self.code_formatcode_format,
163  },
164  )
165  if code:
166  data[ATTR_CODE] = code
167  return data
168 
169  @cached_property
170  def changed_by(self) -> str | None:
171  """Last change triggered by."""
172  return self._attr_changed_by
173 
174  @cached_property
175  def code_format(self) -> str | None:
176  """Regex for code format or None if no code is required."""
177  return self._attr_code_format
178 
179  @property
180  @final
181  def code_format_cmp(self) -> re.Pattern[str] | None:
182  """Return a compiled code_format."""
183  if self.code_formatcode_format is None:
184  self.__code_format_cmp__code_format_cmp = None
185  return None
186  if (
187  not self.__code_format_cmp__code_format_cmp
188  or self.code_formatcode_format != self.__code_format_cmp__code_format_cmp.pattern
189  ):
190  self.__code_format_cmp__code_format_cmp = re.compile(self.code_formatcode_format)
191  return self.__code_format_cmp__code_format_cmp
192 
193  @cached_property
194  def is_locked(self) -> bool | None:
195  """Return true if the lock is locked."""
196  return self._attr_is_locked
197 
198  @cached_property
199  def is_locking(self) -> bool | None:
200  """Return true if the lock is locking."""
201  return self._attr_is_locking
202 
203  @cached_property
204  def is_unlocking(self) -> bool | None:
205  """Return true if the lock is unlocking."""
206  return self._attr_is_unlocking
207 
208  @cached_property
209  def is_open(self) -> bool | None:
210  """Return true if the lock is open."""
211  return self._attr_is_open
212 
213  @cached_property
214  def is_opening(self) -> bool | None:
215  """Return true if the lock is opening."""
216  return self._attr_is_opening
217 
218  @cached_property
219  def is_jammed(self) -> bool | None:
220  """Return true if the lock is jammed (incomplete locking)."""
221  return self._attr_is_jammed
222 
223  @final
224  async def async_handle_lock_service(self, **kwargs: Any) -> None:
225  """Add default code and lock."""
226  await self.async_lockasync_lock(**self.add_default_codeadd_default_code(kwargs))
227 
228  def lock(self, **kwargs: Any) -> None:
229  """Lock the lock."""
230  raise NotImplementedError
231 
232  async def async_lock(self, **kwargs: Any) -> None:
233  """Lock the lock."""
234  await self.hasshass.async_add_executor_job(ft.partial(self.locklock, **kwargs))
235 
236  @final
237  async def async_handle_unlock_service(self, **kwargs: Any) -> None:
238  """Add default code and unlock."""
239  await self.async_unlockasync_unlock(**self.add_default_codeadd_default_code(kwargs))
240 
241  def unlock(self, **kwargs: Any) -> None:
242  """Unlock the lock."""
243  raise NotImplementedError
244 
245  async def async_unlock(self, **kwargs: Any) -> None:
246  """Unlock the lock."""
247  await self.hasshass.async_add_executor_job(ft.partial(self.unlockunlock, **kwargs))
248 
249  @final
250  async def async_handle_open_service(self, **kwargs: Any) -> None:
251  """Add default code and open."""
252  await self.async_openasync_open(**self.add_default_codeadd_default_code(kwargs))
253 
254  def open(self, **kwargs: Any) -> None:
255  """Open the door latch."""
256  raise NotImplementedError
257 
258  async def async_open(self, **kwargs: Any) -> None:
259  """Open the door latch."""
260  await self.hasshass.async_add_executor_job(ft.partial(self.openopen, **kwargs))
261 
262  @final
263  @property
264  def state_attributes(self) -> dict[str, StateType]:
265  """Return the state attributes."""
266  state_attr = {}
267  for prop, attr in PROP_TO_ATTR.items():
268  if (value := getattr(self, prop)) is not None:
269  state_attr[attr] = value
270  return state_attr
271 
272  @final
273  @property
274  def state(self) -> str | None:
275  """Return the state."""
276  if self.is_jammedis_jammed:
277  return LockState.JAMMED
278  if self.is_openingis_opening:
279  return LockState.OPENING
280  if self.is_lockingis_locking:
281  return LockState.LOCKING
282  if self.is_openis_open:
283  return LockState.OPEN
284  if self.is_unlockingis_unlocking:
285  return LockState.UNLOCKING
286  if (locked := self.is_lockedis_locked) is None:
287  return None
288  return LockState.LOCKED if locked else LockState.UNLOCKED
289 
290  @cached_property
291  def supported_features(self) -> LockEntityFeature:
292  """Return the list of supported features."""
293  features = self._attr_supported_features
294  if type(features) is int: # noqa: E721
295  new_features = LockEntityFeature(features)
296  self._report_deprecated_supported_features_values_report_deprecated_supported_features_values(new_features)
297  return new_features
298  return features
299 
300  async def async_internal_added_to_hass(self) -> None:
301  """Call when the sensor entity is added to hass."""
302  await super().async_internal_added_to_hass()
303  if not self.registry_entryregistry_entry:
304  return
305  self._async_read_entity_options_async_read_entity_options()
306 
307  @callback
308  def async_registry_entry_updated(self) -> None:
309  """Run when the entity registry entry has been updated."""
310  self._async_read_entity_options_async_read_entity_options()
311 
312  @callback
313  def _async_read_entity_options(self) -> None:
314  """Read entity options from entity registry.
315 
316  Called when the entity registry entry has been updated and before the lock is
317  added to the state machine.
318  """
319  assert self.registry_entryregistry_entry
320  if (lock_options := self.registry_entryregistry_entry.options.get(DOMAIN)) and (
321  custom_default_lock_code := lock_options.get(CONF_DEFAULT_CODE)
322  ):
323  if self.code_format_cmpcode_format_cmp and self.code_format_cmpcode_format_cmp.match(
324  custom_default_lock_code
325  ):
326  self._lock_option_default_code_lock_option_default_code = custom_default_lock_code
327  return
328 
329  self._lock_option_default_code_lock_option_default_code = ""
330 
331 
332 # These can be removed if no deprecated constant are in this module anymore
333 __getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals())
334 __dir__ = ft.partial(
335  dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
336 )
337 __all__ = all_with_deprecated_constants(globals())
LockEntityFeature supported_features(self)
Definition: __init__.py:291
dict[str, StateType] state_attributes(self)
Definition: __init__.py:264
None async_open(self, **Any kwargs)
Definition: __init__.py:258
None async_unlock(self, **Any kwargs)
Definition: __init__.py:245
None unlock(self, **Any kwargs)
Definition: __init__.py:241
None async_lock(self, **Any kwargs)
Definition: __init__.py:232
None open(self, **Any kwargs)
Definition: __init__.py:254
None async_handle_unlock_service(self, **Any kwargs)
Definition: __init__.py:237
None lock(self, **Any kwargs)
Definition: __init__.py:228
None async_handle_open_service(self, **Any kwargs)
Definition: __init__.py:250
dict[Any, Any] add_default_code(self, dict[Any, Any] data)
Definition: __init__.py:149
re.Pattern[str]|None code_format_cmp(self)
Definition: __init__.py:181
None async_handle_lock_service(self, **Any kwargs)
Definition: __init__.py:224
None _report_deprecated_supported_features_values(self, IntFlag replacement)
Definition: entity.py:1645
list[_T] match(self, BluetoothServiceInfoBleak service_info)
Definition: match.py:246
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:108
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:79
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:103
list[str] all_with_deprecated_constants(dict[str, Any] module_globals)
Definition: deprecation.py:356