Home Assistant Unofficial Reference 2024.12.1
alarm_control_panel.py
Go to the documentation of this file.
1 """Control a MQTT alarm."""
2 
3 from __future__ import annotations
4 
5 import logging
6 
7 import voluptuous as vol
8 
11  AlarmControlPanelEntityFeature,
12  AlarmControlPanelState,
13 )
14 from homeassistant.config_entries import ConfigEntry
15 from homeassistant.const import CONF_CODE, CONF_NAME, CONF_VALUE_TEMPLATE
16 from homeassistant.core import HomeAssistant, callback
18 from homeassistant.helpers.entity_platform import AddEntitiesCallback
19 from homeassistant.helpers.typing import ConfigType
20 
21 from . import subscription
22 from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA
23 from .const import (
24  CONF_COMMAND_TEMPLATE,
25  CONF_COMMAND_TOPIC,
26  CONF_RETAIN,
27  CONF_STATE_TOPIC,
28  CONF_SUPPORTED_FEATURES,
29  PAYLOAD_NONE,
30 )
31 from .entity import MqttEntity, async_setup_entity_entry_helper
32 from .models import MqttCommandTemplate, MqttValueTemplate, ReceiveMessage
33 from .schemas import MQTT_ENTITY_COMMON_SCHEMA
34 from .util import valid_publish_topic, valid_subscribe_topic
35 
36 _LOGGER = logging.getLogger(__name__)
37 
38 PARALLEL_UPDATES = 0
39 
40 _SUPPORTED_FEATURES = {
41  "arm_home": AlarmControlPanelEntityFeature.ARM_HOME,
42  "arm_away": AlarmControlPanelEntityFeature.ARM_AWAY,
43  "arm_night": AlarmControlPanelEntityFeature.ARM_NIGHT,
44  "arm_vacation": AlarmControlPanelEntityFeature.ARM_VACATION,
45  "arm_custom_bypass": AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS,
46  "trigger": AlarmControlPanelEntityFeature.TRIGGER,
47 }
48 
49 CONF_CODE_ARM_REQUIRED = "code_arm_required"
50 CONF_CODE_DISARM_REQUIRED = "code_disarm_required"
51 CONF_CODE_TRIGGER_REQUIRED = "code_trigger_required"
52 CONF_PAYLOAD_DISARM = "payload_disarm"
53 CONF_PAYLOAD_ARM_HOME = "payload_arm_home"
54 CONF_PAYLOAD_ARM_AWAY = "payload_arm_away"
55 CONF_PAYLOAD_ARM_NIGHT = "payload_arm_night"
56 CONF_PAYLOAD_ARM_VACATION = "payload_arm_vacation"
57 CONF_PAYLOAD_ARM_CUSTOM_BYPASS = "payload_arm_custom_bypass"
58 CONF_PAYLOAD_TRIGGER = "payload_trigger"
59 
60 MQTT_ALARM_ATTRIBUTES_BLOCKED = frozenset(
61  {
62  alarm.ATTR_CHANGED_BY,
63  alarm.ATTR_CODE_ARM_REQUIRED,
64  alarm.ATTR_CODE_FORMAT,
65  }
66 )
67 
68 DEFAULT_COMMAND_TEMPLATE = "{{action}}"
69 DEFAULT_ARM_NIGHT = "ARM_NIGHT"
70 DEFAULT_ARM_VACATION = "ARM_VACATION"
71 DEFAULT_ARM_AWAY = "ARM_AWAY"
72 DEFAULT_ARM_HOME = "ARM_HOME"
73 DEFAULT_ARM_CUSTOM_BYPASS = "ARM_CUSTOM_BYPASS"
74 DEFAULT_DISARM = "DISARM"
75 DEFAULT_TRIGGER = "TRIGGER"
76 DEFAULT_NAME = "MQTT Alarm"
77 
78 REMOTE_CODE = "REMOTE_CODE"
79 REMOTE_CODE_TEXT = "REMOTE_CODE_TEXT"
80 
81 PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend(
82  {
83  vol.Optional(CONF_SUPPORTED_FEATURES, default=list(_SUPPORTED_FEATURES)): [
84  vol.In(_SUPPORTED_FEATURES)
85  ],
86  vol.Optional(CONF_CODE): cv.string,
87  vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean,
88  vol.Optional(CONF_CODE_DISARM_REQUIRED, default=True): cv.boolean,
89  vol.Optional(CONF_CODE_TRIGGER_REQUIRED, default=True): cv.boolean,
90  vol.Optional(
91  CONF_COMMAND_TEMPLATE, default=DEFAULT_COMMAND_TEMPLATE
92  ): cv.template,
93  vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic,
94  vol.Optional(CONF_NAME): vol.Any(cv.string, None),
95  vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string,
96  vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string,
97  vol.Optional(CONF_PAYLOAD_ARM_NIGHT, default=DEFAULT_ARM_NIGHT): cv.string,
98  vol.Optional(
99  CONF_PAYLOAD_ARM_VACATION, default=DEFAULT_ARM_VACATION
100  ): cv.string,
101  vol.Optional(
102  CONF_PAYLOAD_ARM_CUSTOM_BYPASS, default=DEFAULT_ARM_CUSTOM_BYPASS
103  ): cv.string,
104  vol.Optional(CONF_PAYLOAD_DISARM, default=DEFAULT_DISARM): cv.string,
105  vol.Optional(CONF_PAYLOAD_TRIGGER, default=DEFAULT_TRIGGER): cv.string,
106  vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
107  vol.Required(CONF_STATE_TOPIC): valid_subscribe_topic,
108  vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
109  }
110 ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
111 
112 DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA)
113 
114 
116  hass: HomeAssistant,
117  config_entry: ConfigEntry,
118  async_add_entities: AddEntitiesCallback,
119 ) -> None:
120  """Set up MQTT alarm control panel through YAML and through MQTT discovery."""
122  hass,
123  config_entry,
124  MqttAlarm,
125  alarm.DOMAIN,
126  async_add_entities,
127  DISCOVERY_SCHEMA,
128  PLATFORM_SCHEMA_MODERN,
129  )
130 
131 
132 class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity):
133  """Representation of a MQTT alarm status."""
134 
135  _default_name = DEFAULT_NAME
136  _entity_id_format = alarm.ENTITY_ID_FORMAT
137  _attributes_extra_blocked = MQTT_ALARM_ATTRIBUTES_BLOCKED
138 
139  @staticmethod
140  def config_schema() -> vol.Schema:
141  """Return the config schema."""
142  return DISCOVERY_SCHEMA
143 
144  def _setup_from_config(self, config: ConfigType) -> None:
145  """(Re)Setup the entity."""
146  self._value_template_value_template = MqttValueTemplate(
147  config.get(CONF_VALUE_TEMPLATE),
148  entity=self,
149  ).async_render_with_possible_json_value
150  self._command_template_command_template = MqttCommandTemplate(
151  config[CONF_COMMAND_TEMPLATE], entity=self
152  ).async_render
153 
154  for feature in self._config_config[CONF_SUPPORTED_FEATURES]:
155  self._attr_supported_features |= _SUPPORTED_FEATURES[feature]
156 
157  if (code := self._config_config.get(CONF_CODE)) is None:
158  self._attr_code_format_attr_code_format = None
159  elif code == REMOTE_CODE or str(code).isdigit():
160  self._attr_code_format_attr_code_format = alarm.CodeFormat.NUMBER
161  else:
162  self._attr_code_format_attr_code_format = alarm.CodeFormat.TEXT
163  self._attr_code_arm_required_attr_code_arm_required = bool(self._config_config[CONF_CODE_ARM_REQUIRED])
164 
165  def _state_message_received(self, msg: ReceiveMessage) -> None:
166  """Run when new MQTT message has been received."""
167  payload = self._value_template_value_template(msg.payload)
168  if not payload.strip(): # No output from template, ignore
169  _LOGGER.debug(
170  "Ignoring empty payload '%s' after rendering for topic %s",
171  payload,
172  msg.topic,
173  )
174  return
175  if payload == PAYLOAD_NONE:
176  self._attr_alarm_state_attr_alarm_state = None
177  return
178  if payload not in (
179  AlarmControlPanelState.DISARMED,
180  AlarmControlPanelState.ARMED_HOME,
181  AlarmControlPanelState.ARMED_AWAY,
182  AlarmControlPanelState.ARMED_NIGHT,
183  AlarmControlPanelState.ARMED_VACATION,
184  AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
185  AlarmControlPanelState.PENDING,
186  AlarmControlPanelState.ARMING,
187  AlarmControlPanelState.DISARMING,
188  AlarmControlPanelState.TRIGGERED,
189  ):
190  _LOGGER.warning("Received unexpected payload: %s", msg.payload)
191  return
192  assert isinstance(payload, str)
193  self._attr_alarm_state_attr_alarm_state = AlarmControlPanelState(payload)
194 
195  @callback
196  def _prepare_subscribe_topics(self) -> None:
197  """(Re)Subscribe to topics."""
198  self.add_subscriptionadd_subscription(
199  CONF_STATE_TOPIC, self._state_message_received_state_message_received, {"_attr_alarm_state"}
200  )
201 
202  async def _subscribe_topics(self) -> None:
203  """(Re)Subscribe to topics."""
204  subscription.async_subscribe_topics_internal(self.hasshasshass, self._sub_state_sub_state)
205 
206  async def async_alarm_disarm(self, code: str | None = None) -> None:
207  """Send disarm command.
208 
209  This method is a coroutine.
210  """
211  code_required: bool = self._config_config[CONF_CODE_DISARM_REQUIRED]
212  if code_required and not self._validate_code_validate_code(code, "disarming"):
213  return
214  payload: str = self._config_config[CONF_PAYLOAD_DISARM]
215  await self._publish_publish(code, payload)
216 
217  async def async_alarm_arm_home(self, code: str | None = None) -> None:
218  """Send arm home command.
219 
220  This method is a coroutine.
221  """
222  code_required: bool = self._config_config[CONF_CODE_ARM_REQUIRED]
223  if code_required and not self._validate_code_validate_code(code, "arming home"):
224  return
225  action: str = self._config_config[CONF_PAYLOAD_ARM_HOME]
226  await self._publish_publish(code, action)
227 
228  async def async_alarm_arm_away(self, code: str | None = None) -> None:
229  """Send arm away command.
230 
231  This method is a coroutine.
232  """
233  code_required: bool = self._config_config[CONF_CODE_ARM_REQUIRED]
234  if code_required and not self._validate_code_validate_code(code, "arming away"):
235  return
236  action: str = self._config_config[CONF_PAYLOAD_ARM_AWAY]
237  await self._publish_publish(code, action)
238 
239  async def async_alarm_arm_night(self, code: str | None = None) -> None:
240  """Send arm night command.
241 
242  This method is a coroutine.
243  """
244  code_required: bool = self._config_config[CONF_CODE_ARM_REQUIRED]
245  if code_required and not self._validate_code_validate_code(code, "arming night"):
246  return
247  action: str = self._config_config[CONF_PAYLOAD_ARM_NIGHT]
248  await self._publish_publish(code, action)
249 
250  async def async_alarm_arm_vacation(self, code: str | None = None) -> None:
251  """Send arm vacation command.
252 
253  This method is a coroutine.
254  """
255  code_required: bool = self._config_config[CONF_CODE_ARM_REQUIRED]
256  if code_required and not self._validate_code_validate_code(code, "arming vacation"):
257  return
258  action: str = self._config_config[CONF_PAYLOAD_ARM_VACATION]
259  await self._publish_publish(code, action)
260 
261  async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None:
262  """Send arm custom bypass command.
263 
264  This method is a coroutine.
265  """
266  code_required: bool = self._config_config[CONF_CODE_ARM_REQUIRED]
267  if code_required and not self._validate_code_validate_code(code, "arming custom bypass"):
268  return
269  action: str = self._config_config[CONF_PAYLOAD_ARM_CUSTOM_BYPASS]
270  await self._publish_publish(code, action)
271 
272  async def async_alarm_trigger(self, code: str | None = None) -> None:
273  """Send trigger command.
274 
275  This method is a coroutine.
276  """
277  code_required: bool = self._config_config[CONF_CODE_TRIGGER_REQUIRED]
278  if code_required and not self._validate_code_validate_code(code, "triggering"):
279  return
280  action: str = self._config_config[CONF_PAYLOAD_TRIGGER]
281  await self._publish_publish(code, action)
282 
283  async def _publish(self, code: str | None, action: str) -> None:
284  """Publish via mqtt."""
285  variables = {"action": action, "code": code}
286  payload = self._command_template_command_template(None, variables=variables)
287  await self.async_publish_with_configasync_publish_with_config(self._config_config[CONF_COMMAND_TOPIC], payload)
288 
289  def _validate_code(self, code: str | None, state: str) -> bool:
290  """Validate given code."""
291  conf_code: str | None = self._config_config.get(CONF_CODE)
292  check = bool(
293  conf_code is None
294  or code == conf_code
295  or (conf_code == REMOTE_CODE and code)
296  or (conf_code == REMOTE_CODE_TEXT and code)
297  )
298  if not check:
299  _LOGGER.warning("Wrong code entered for %s", state)
300  return check
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
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
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