Home Assistant Unofficial Reference 2024.12.1
lock.py
Go to the documentation of this file.
1 """Support for MQTT locks."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 import logging
7 import re
8 from typing import Any
9 
10 import voluptuous as vol
11 
12 from homeassistant.components import lock
13 from homeassistant.components.lock import LockEntity, LockEntityFeature
14 from homeassistant.config_entries import ConfigEntry
15 from homeassistant.const import (
16  ATTR_CODE,
17  CONF_NAME,
18  CONF_OPTIMISTIC,
19  CONF_VALUE_TEMPLATE,
20 )
21 from homeassistant.core import HomeAssistant, callback
23 from homeassistant.helpers.entity_platform import AddEntitiesCallback
24 from homeassistant.helpers.service_info.mqtt import ReceivePayloadType
25 from homeassistant.helpers.typing import ConfigType, TemplateVarsType
26 
27 from . import subscription
28 from .config import MQTT_RW_SCHEMA
29 from .const import (
30  CONF_COMMAND_TEMPLATE,
31  CONF_COMMAND_TOPIC,
32  CONF_PAYLOAD_RESET,
33  CONF_STATE_OPEN,
34  CONF_STATE_OPENING,
35  CONF_STATE_TOPIC,
36 )
37 from .entity import MqttEntity, async_setup_entity_entry_helper
38 from .models import (
39  MqttCommandTemplate,
40  MqttValueTemplate,
41  PublishPayloadType,
42  ReceiveMessage,
43 )
44 from .schemas import MQTT_ENTITY_COMMON_SCHEMA
45 
46 _LOGGER = logging.getLogger(__name__)
47 
48 PARALLEL_UPDATES = 0
49 
50 CONF_CODE_FORMAT = "code_format"
51 
52 CONF_PAYLOAD_LOCK = "payload_lock"
53 CONF_PAYLOAD_UNLOCK = "payload_unlock"
54 CONF_PAYLOAD_OPEN = "payload_open"
55 
56 CONF_STATE_LOCKED = "state_locked"
57 CONF_STATE_LOCKING = "state_locking"
58 
59 CONF_STATE_UNLOCKED = "state_unlocked"
60 CONF_STATE_UNLOCKING = "state_unlocking"
61 CONF_STATE_JAMMED = "state_jammed"
62 
63 DEFAULT_NAME = "MQTT Lock"
64 DEFAULT_PAYLOAD_LOCK = "LOCK"
65 DEFAULT_PAYLOAD_UNLOCK = "UNLOCK"
66 DEFAULT_PAYLOAD_OPEN = "OPEN"
67 DEFAULT_PAYLOAD_RESET = "None"
68 DEFAULT_STATE_LOCKED = "LOCKED"
69 DEFAULT_STATE_LOCKING = "LOCKING"
70 DEFAULT_STATE_OPEN = "OPEN"
71 DEFAULT_STATE_OPENING = "OPENING"
72 DEFAULT_STATE_UNLOCKED = "UNLOCKED"
73 DEFAULT_STATE_UNLOCKING = "UNLOCKING"
74 DEFAULT_STATE_JAMMED = "JAMMED"
75 
76 MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset(
77  {
78  lock.ATTR_CHANGED_BY,
79  lock.ATTR_CODE_FORMAT,
80  }
81 )
82 
83 PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend(
84  {
85  vol.Optional(CONF_CODE_FORMAT): cv.is_regex,
86  vol.Optional(CONF_COMMAND_TEMPLATE): cv.template,
87  vol.Optional(CONF_NAME): vol.Any(cv.string, None),
88  vol.Optional(CONF_PAYLOAD_LOCK, default=DEFAULT_PAYLOAD_LOCK): cv.string,
89  vol.Optional(CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK): cv.string,
90  vol.Optional(CONF_PAYLOAD_OPEN): cv.string,
91  vol.Optional(CONF_PAYLOAD_RESET, default=DEFAULT_PAYLOAD_RESET): cv.string,
92  vol.Optional(CONF_STATE_JAMMED, default=DEFAULT_STATE_JAMMED): cv.string,
93  vol.Optional(CONF_STATE_LOCKED, default=DEFAULT_STATE_LOCKED): cv.string,
94  vol.Optional(CONF_STATE_LOCKING, default=DEFAULT_STATE_LOCKING): cv.string,
95  vol.Optional(CONF_STATE_OPEN, default=DEFAULT_STATE_OPEN): cv.string,
96  vol.Optional(CONF_STATE_OPENING, default=DEFAULT_STATE_OPENING): cv.string,
97  vol.Optional(CONF_STATE_UNLOCKED, default=DEFAULT_STATE_UNLOCKED): cv.string,
98  vol.Optional(CONF_STATE_UNLOCKING, default=DEFAULT_STATE_UNLOCKING): cv.string,
99  vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
100  }
101 ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
102 
103 DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA)
104 
105 STATE_CONFIG_KEYS = [
106  CONF_STATE_JAMMED,
107  CONF_STATE_LOCKED,
108  CONF_STATE_LOCKING,
109  CONF_STATE_OPEN,
110  CONF_STATE_OPENING,
111  CONF_STATE_UNLOCKED,
112  CONF_STATE_UNLOCKING,
113 ]
114 
115 
117  hass: HomeAssistant,
118  config_entry: ConfigEntry,
119  async_add_entities: AddEntitiesCallback,
120 ) -> None:
121  """Set up MQTT lock through YAML and through MQTT discovery."""
123  hass,
124  config_entry,
125  MqttLock,
126  lock.DOMAIN,
127  async_add_entities,
128  DISCOVERY_SCHEMA,
129  PLATFORM_SCHEMA_MODERN,
130  )
131 
132 
134  """Representation of a lock that can be toggled using MQTT."""
135 
136  _default_name = DEFAULT_NAME
137  _entity_id_format = lock.ENTITY_ID_FORMAT
138  _attributes_extra_blocked = MQTT_LOCK_ATTRIBUTES_BLOCKED
139 
140  _compiled_pattern: re.Pattern[Any] | None
141  _optimistic: bool
142  _valid_states: list[str]
143  _command_template: Callable[
144  [PublishPayloadType, TemplateVarsType], PublishPayloadType
145  ]
146  _value_template: Callable[[ReceivePayloadType], ReceivePayloadType]
147 
148  @staticmethod
149  def config_schema() -> vol.Schema:
150  """Return the config schema."""
151  return DISCOVERY_SCHEMA
152 
153  def _setup_from_config(self, config: ConfigType) -> None:
154  """(Re)Setup the entity."""
155  if (
156  optimistic := config[CONF_OPTIMISTIC]
157  or config.get(CONF_STATE_TOPIC) is None
158  ):
159  self._attr_is_locked_attr_is_locked = False
160  self._optimistic_optimistic = optimistic
161  self._attr_assumed_state_attr_assumed_state = bool(optimistic)
162 
163  self._compiled_pattern_compiled_pattern = config.get(CONF_CODE_FORMAT)
164  self._attr_code_format_attr_code_format = (
165  self._compiled_pattern_compiled_pattern.pattern if self._compiled_pattern_compiled_pattern else None
166  )
167 
168  self._command_template_command_template = MqttCommandTemplate(
169  config.get(CONF_COMMAND_TEMPLATE), entity=self
170  ).async_render
171 
172  self._value_template_value_template = MqttValueTemplate(
173  config.get(CONF_VALUE_TEMPLATE),
174  entity=self,
175  ).async_render_with_possible_json_value
176 
177  self._attr_supported_features_attr_supported_features = LockEntityFeature(0)
178  if CONF_PAYLOAD_OPEN in config:
179  self._attr_supported_features_attr_supported_features |= LockEntityFeature.OPEN
180 
181  self._valid_states_valid_states = [config[state] for state in STATE_CONFIG_KEYS]
182 
183  @callback
184  def _message_received(self, msg: ReceiveMessage) -> None:
185  """Handle new lock state messages."""
186  payload = self._value_template_value_template(msg.payload)
187  if not payload.strip(): # No output from template, ignore
188  _LOGGER.debug(
189  "Ignoring empty payload '%s' after rendering for topic %s",
190  payload,
191  msg.topic,
192  )
193  return
194  if payload == self._config_config[CONF_PAYLOAD_RESET]:
195  # Reset the state to `unknown`
196  self._attr_is_locked_attr_is_locked = None
197  elif payload in self._valid_states_valid_states:
198  self._attr_is_locked_attr_is_locked = payload == self._config_config[CONF_STATE_LOCKED]
199  self._attr_is_locking_attr_is_locking = payload == self._config_config[CONF_STATE_LOCKING]
200  self._attr_is_open_attr_is_open = payload == self._config_config[CONF_STATE_OPEN]
201  self._attr_is_opening_attr_is_opening = payload == self._config_config[CONF_STATE_OPENING]
202  self._attr_is_unlocking_attr_is_unlocking = payload == self._config_config[CONF_STATE_UNLOCKING]
203  self._attr_is_jammed_attr_is_jammed = payload == self._config_config[CONF_STATE_JAMMED]
204 
205  @callback
206  def _prepare_subscribe_topics(self) -> None:
207  """(Re)Subscribe to topics."""
208  self.add_subscriptionadd_subscription(
209  CONF_STATE_TOPIC,
210  self._message_received_message_received,
211  {
212  "_attr_is_jammed",
213  "_attr_is_locked",
214  "_attr_is_locking",
215  "_attr_is_open",
216  "_attr_is_opening",
217  "_attr_is_unlocking",
218  },
219  )
220 
221  async def _subscribe_topics(self) -> None:
222  """(Re)Subscribe to topics."""
223  subscription.async_subscribe_topics_internal(self.hasshasshass, self._sub_state_sub_state)
224 
225  async def async_lock(self, **kwargs: Any) -> None:
226  """Lock the device.
227 
228  This method is a coroutine.
229  """
230  tpl_vars: TemplateVarsType = {
231  ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None
232  }
233  payload = self._command_template_command_template(self._config_config[CONF_PAYLOAD_LOCK], tpl_vars)
234  await self.async_publish_with_configasync_publish_with_config(self._config_config[CONF_COMMAND_TOPIC], payload)
235  if self._optimistic_optimistic:
236  # Optimistically assume that the lock has changed state.
237  self._attr_is_locked_attr_is_locked = True
238  self.async_write_ha_stateasync_write_ha_state()
239 
240  async def async_unlock(self, **kwargs: Any) -> None:
241  """Unlock the device.
242 
243  This method is a coroutine.
244  """
245  tpl_vars: TemplateVarsType = {
246  ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None
247  }
248  payload = self._command_template_command_template(self._config_config[CONF_PAYLOAD_UNLOCK], tpl_vars)
249  await self.async_publish_with_configasync_publish_with_config(self._config_config[CONF_COMMAND_TOPIC], payload)
250  if self._optimistic_optimistic:
251  # Optimistically assume that the lock has changed state.
252  self._attr_is_locked_attr_is_locked = False
253  self.async_write_ha_stateasync_write_ha_state()
254 
255  async def async_open(self, **kwargs: Any) -> None:
256  """Open the door latch.
257 
258  This method is a coroutine.
259  """
260  tpl_vars: TemplateVarsType = {
261  ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None
262  }
263  payload = self._command_template_command_template(self._config_config[CONF_PAYLOAD_OPEN], tpl_vars)
264  await self.async_publish_with_configasync_publish_with_config(self._config_config[CONF_COMMAND_TOPIC], payload)
265  if self._optimistic_optimistic:
266  # Optimistically assume that the lock unlocks when opened.
267  self._attr_is_open_attr_is_open = True
268  self.async_write_ha_stateasync_write_ha_state()
None async_publish_with_config(self, str topic, PublishPayloadType payload)
Definition: entity.py:1377
bool add_subscription(self, str state_topic_config_key, Callable[[ReceiveMessage], None] msg_callback, set[str]|None tracked_attributes, bool disable_encoding=False)
Definition: entity.py:1484
None _message_received(self, ReceiveMessage msg)
Definition: lock.py:184
None async_open(self, **Any kwargs)
Definition: lock.py:255
None async_unlock(self, **Any kwargs)
Definition: lock.py:240
None async_lock(self, **Any kwargs)
Definition: lock.py:225
None _setup_from_config(self, ConfigType config)
Definition: lock.py:153
None async_setup_entity_entry_helper(HomeAssistant hass, ConfigEntry entry, type[MqttEntity]|None entity_class, str domain, AddEntitiesCallback async_add_entities, VolSchemaType discovery_schema, VolSchemaType platform_schema_modern, dict[str, type[MqttEntity]]|None schema_class_mapping=None)
Definition: entity.py:245
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: lock.py:120