Home Assistant Unofficial Reference 2024.12.1
alarm_control_panel.py
Go to the documentation of this file.
1 """Support for Template alarm control panels."""
2 
3 from __future__ import annotations
4 
5 from enum import Enum
6 import logging
7 from typing import Any
8 
9 import voluptuous as vol
10 
12  ENTITY_ID_FORMAT,
13  PLATFORM_SCHEMA as ALARM_CONTROL_PANEL_PLATFORM_SCHEMA,
14  AlarmControlPanelEntity,
15  AlarmControlPanelEntityFeature,
16  AlarmControlPanelState,
17  CodeFormat,
18 )
19 from homeassistant.config_entries import ConfigEntry
20 from homeassistant.const import (
21  ATTR_CODE,
22  CONF_DEVICE_ID,
23  CONF_NAME,
24  CONF_UNIQUE_ID,
25  CONF_VALUE_TEMPLATE,
26  STATE_UNAVAILABLE,
27  STATE_UNKNOWN,
28 )
29 from homeassistant.core import HomeAssistant, callback
30 from homeassistant.exceptions import TemplateError
31 from homeassistant.helpers import selector
33 from homeassistant.helpers.device import async_device_info_to_link_from_device_id
34 from homeassistant.helpers.entity import async_generate_entity_id
35 from homeassistant.helpers.entity_platform import AddEntitiesCallback
36 from homeassistant.helpers.restore_state import RestoreEntity
37 from homeassistant.helpers.script import Script
38 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
39 from homeassistant.util import slugify
40 
41 from .const import DOMAIN
42 from .template_entity import TemplateEntity, rewrite_common_legacy_to_modern_conf
43 
44 _LOGGER = logging.getLogger(__name__)
45 _VALID_STATES = [
46  AlarmControlPanelState.ARMED_AWAY,
47  AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
48  AlarmControlPanelState.ARMED_HOME,
49  AlarmControlPanelState.ARMED_NIGHT,
50  AlarmControlPanelState.ARMED_VACATION,
51  AlarmControlPanelState.ARMING,
52  AlarmControlPanelState.DISARMED,
53  AlarmControlPanelState.PENDING,
54  AlarmControlPanelState.TRIGGERED,
55  STATE_UNAVAILABLE,
56 ]
57 
58 CONF_ARM_AWAY_ACTION = "arm_away"
59 CONF_ARM_CUSTOM_BYPASS_ACTION = "arm_custom_bypass"
60 CONF_ARM_HOME_ACTION = "arm_home"
61 CONF_ARM_NIGHT_ACTION = "arm_night"
62 CONF_ARM_VACATION_ACTION = "arm_vacation"
63 CONF_DISARM_ACTION = "disarm"
64 CONF_TRIGGER_ACTION = "trigger"
65 CONF_ALARM_CONTROL_PANELS = "panels"
66 CONF_CODE_ARM_REQUIRED = "code_arm_required"
67 CONF_CODE_FORMAT = "code_format"
68 
69 
70 class TemplateCodeFormat(Enum):
71  """Class to represent different code formats."""
72 
73  no_code = None
74  number = CodeFormat.NUMBER
75  text = CodeFormat.TEXT
76 
77 
78 ALARM_CONTROL_PANEL_SCHEMA = vol.Schema(
79  {
80  vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
81  vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA,
82  vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA,
83  vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA,
84  vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA,
85  vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA,
86  vol.Optional(CONF_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA,
87  vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA,
88  vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean,
89  vol.Optional(CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name): cv.enum(
90  TemplateCodeFormat
91  ),
92  vol.Optional(CONF_NAME): cv.string,
93  vol.Optional(CONF_UNIQUE_ID): cv.string,
94  }
95 )
96 
97 PLATFORM_SCHEMA = ALARM_CONTROL_PANEL_PLATFORM_SCHEMA.extend(
98  {
99  vol.Required(CONF_ALARM_CONTROL_PANELS): cv.schema_with_slug_keys(
100  ALARM_CONTROL_PANEL_SCHEMA
101  ),
102  }
103 )
104 
105 ALARM_CONTROL_PANEL_CONFIG_SCHEMA = vol.Schema(
106  {
107  vol.Required(CONF_NAME): cv.template,
108  vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
109  vol.Optional(CONF_DISARM_ACTION): cv.SCRIPT_SCHEMA,
110  vol.Optional(CONF_ARM_AWAY_ACTION): cv.SCRIPT_SCHEMA,
111  vol.Optional(CONF_ARM_CUSTOM_BYPASS_ACTION): cv.SCRIPT_SCHEMA,
112  vol.Optional(CONF_ARM_HOME_ACTION): cv.SCRIPT_SCHEMA,
113  vol.Optional(CONF_ARM_NIGHT_ACTION): cv.SCRIPT_SCHEMA,
114  vol.Optional(CONF_ARM_VACATION_ACTION): cv.SCRIPT_SCHEMA,
115  vol.Optional(CONF_TRIGGER_ACTION): cv.SCRIPT_SCHEMA,
116  vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean,
117  vol.Optional(CONF_CODE_FORMAT, default=TemplateCodeFormat.number.name): cv.enum(
118  TemplateCodeFormat
119  ),
120  vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
121  }
122 )
123 
124 
126  hass: HomeAssistant, config: dict[str, Any]
127 ) -> list[AlarmControlPanelTemplate]:
128  """Create Template Alarm Control Panels."""
129  alarm_control_panels = []
130 
131  for object_id, entity_config in config[CONF_ALARM_CONTROL_PANELS].items():
132  entity_config = rewrite_common_legacy_to_modern_conf(hass, entity_config)
133  unique_id = entity_config.get(CONF_UNIQUE_ID)
134 
135  alarm_control_panels.append(
137  hass,
138  object_id,
139  entity_config,
140  unique_id,
141  )
142  )
143 
144  return alarm_control_panels
145 
146 
148  hass: HomeAssistant,
149  config_entry: ConfigEntry,
150  async_add_entities: AddEntitiesCallback,
151 ) -> None:
152  """Initialize config entry."""
153  _options = dict(config_entry.options)
154  _options.pop("template_type")
155  validated_config = ALARM_CONTROL_PANEL_CONFIG_SCHEMA(_options)
157  [
159  hass,
160  slugify(_options[CONF_NAME]),
161  validated_config,
162  config_entry.entry_id,
163  )
164  ]
165  )
166 
167 
169  hass: HomeAssistant,
170  config: ConfigType,
171  async_add_entities: AddEntitiesCallback,
172  discovery_info: DiscoveryInfoType | None = None,
173 ) -> None:
174  """Set up the Template Alarm Control Panels."""
175  async_add_entities(await _async_create_entities(hass, config))
176 
177 
179  """Representation of a templated Alarm Control Panel."""
180 
181  _attr_should_poll = False
182 
183  def __init__(
184  self,
185  hass: HomeAssistant,
186  object_id: str,
187  config: dict,
188  unique_id: str | None,
189  ) -> None:
190  """Initialize the panel."""
191  super().__init__(
192  hass, config=config, fallback_name=object_id, unique_id=unique_id
193  )
195  ENTITY_ID_FORMAT, object_id, hass=hass
196  )
197  name = self._attr_name_attr_name
198  assert name is not None
199  self._template_template = config.get(CONF_VALUE_TEMPLATE)
200  self._disarm_script_disarm_script = None
201  self._attr_code_arm_required: bool = config[CONF_CODE_ARM_REQUIRED]
202  self._attr_code_format_attr_code_format = config[CONF_CODE_FORMAT].value
203  if (disarm_action := config.get(CONF_DISARM_ACTION)) is not None:
204  self._disarm_script_disarm_script = Script(hass, disarm_action, name, DOMAIN)
205  self._arm_away_script_arm_away_script = None
206  if (arm_away_action := config.get(CONF_ARM_AWAY_ACTION)) is not None:
207  self._arm_away_script_arm_away_script = Script(hass, arm_away_action, name, DOMAIN)
208  self._arm_home_script_arm_home_script = None
209  if (arm_home_action := config.get(CONF_ARM_HOME_ACTION)) is not None:
210  self._arm_home_script_arm_home_script = Script(hass, arm_home_action, name, DOMAIN)
211  self._arm_night_script_arm_night_script = None
212  if (arm_night_action := config.get(CONF_ARM_NIGHT_ACTION)) is not None:
213  self._arm_night_script_arm_night_script = Script(hass, arm_night_action, name, DOMAIN)
214  self._arm_vacation_script_arm_vacation_script = None
215  if (arm_vacation_action := config.get(CONF_ARM_VACATION_ACTION)) is not None:
216  self._arm_vacation_script_arm_vacation_script = Script(hass, arm_vacation_action, name, DOMAIN)
217  self._arm_custom_bypass_script_arm_custom_bypass_script = None
218  if (
219  arm_custom_bypass_action := config.get(CONF_ARM_CUSTOM_BYPASS_ACTION)
220  ) is not None:
221  self._arm_custom_bypass_script_arm_custom_bypass_script = Script(
222  hass, arm_custom_bypass_action, name, DOMAIN
223  )
224  self._trigger_script_trigger_script = None
225  if (trigger_action := config.get(CONF_TRIGGER_ACTION)) is not None:
226  self._trigger_script_trigger_script = Script(hass, trigger_action, name, DOMAIN)
227 
228  self._state_state: AlarmControlPanelState | None = None
230  hass,
231  config.get(CONF_DEVICE_ID),
232  )
233  supported_features = AlarmControlPanelEntityFeature(0)
234  if self._arm_night_script_arm_night_script is not None:
235  supported_features = (
236  supported_features | AlarmControlPanelEntityFeature.ARM_NIGHT
237  )
238 
239  if self._arm_home_script_arm_home_script is not None:
240  supported_features = (
241  supported_features | AlarmControlPanelEntityFeature.ARM_HOME
242  )
243 
244  if self._arm_away_script_arm_away_script is not None:
245  supported_features = (
246  supported_features | AlarmControlPanelEntityFeature.ARM_AWAY
247  )
248 
249  if self._arm_vacation_script_arm_vacation_script is not None:
250  supported_features = (
251  supported_features | AlarmControlPanelEntityFeature.ARM_VACATION
252  )
253 
254  if self._arm_custom_bypass_script_arm_custom_bypass_script is not None:
255  supported_features = (
256  supported_features | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS
257  )
258 
259  if self._trigger_script_trigger_script is not None:
260  supported_features = (
261  supported_features | AlarmControlPanelEntityFeature.TRIGGER
262  )
263  self._attr_supported_features_attr_supported_features = supported_features
264 
265  async def async_added_to_hass(self) -> None:
266  """Restore last state."""
267  await super().async_added_to_hass()
268  if (
269  (last_state := await self.async_get_last_stateasync_get_last_state()) is not None
270  and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
271  and last_state.state in _VALID_STATES
272  # The trigger might have fired already while we waited for stored data,
273  # then we should not restore state
274  and self._state_state is None
275  ):
276  self._state_state = AlarmControlPanelState(last_state.state)
277 
278  @property
279  def alarm_state(self) -> AlarmControlPanelState | None:
280  """Return the state of the device."""
281  return self._state_state
282 
283  @callback
284  def _update_state(self, result):
285  if isinstance(result, TemplateError):
286  self._state_state = None
287  return
288 
289  # Validate state
290  if result in _VALID_STATES:
291  self._state_state = result
292  _LOGGER.debug("Valid state - %s", result)
293  return
294 
295  _LOGGER.error(
296  "Received invalid alarm panel state: %s for entity %s. Expected: %s",
297  result,
298  self.entity_identity_identity_identity_id,
299  ", ".join(_VALID_STATES),
300  )
301  self._state_state = None
302 
303  @callback
304  def _async_setup_templates(self) -> None:
305  """Set up templates."""
306  if self._template_template:
307  self.add_template_attributeadd_template_attribute(
308  "_state", self._template_template, None, self._update_state_update_state_update_state
309  )
310  super()._async_setup_templates()
311 
312  async def _async_alarm_arm(self, state, script, code):
313  """Arm the panel to specified state with supplied script."""
314  optimistic_set = False
315 
316  if self._template_template is None:
317  self._state_state = state
318  optimistic_set = True
319 
320  await self.async_run_scriptasync_run_script(
321  script, run_variables={ATTR_CODE: code}, context=self._context_context
322  )
323 
324  if optimistic_set:
325  self.async_write_ha_stateasync_write_ha_state()
326 
327  async def async_alarm_arm_away(self, code: str | None = None) -> None:
328  """Arm the panel to Away."""
329  await self._async_alarm_arm_async_alarm_arm(
330  AlarmControlPanelState.ARMED_AWAY,
331  script=self._arm_away_script_arm_away_script,
332  code=code,
333  )
334 
335  async def async_alarm_arm_home(self, code: str | None = None) -> None:
336  """Arm the panel to Home."""
337  await self._async_alarm_arm_async_alarm_arm(
338  AlarmControlPanelState.ARMED_HOME,
339  script=self._arm_home_script_arm_home_script,
340  code=code,
341  )
342 
343  async def async_alarm_arm_night(self, code: str | None = None) -> None:
344  """Arm the panel to Night."""
345  await self._async_alarm_arm_async_alarm_arm(
346  AlarmControlPanelState.ARMED_NIGHT,
347  script=self._arm_night_script_arm_night_script,
348  code=code,
349  )
350 
351  async def async_alarm_arm_vacation(self, code: str | None = None) -> None:
352  """Arm the panel to Vacation."""
353  await self._async_alarm_arm_async_alarm_arm(
354  AlarmControlPanelState.ARMED_VACATION,
355  script=self._arm_vacation_script_arm_vacation_script,
356  code=code,
357  )
358 
359  async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None:
360  """Arm the panel to Custom Bypass."""
361  await self._async_alarm_arm_async_alarm_arm(
362  AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
363  script=self._arm_custom_bypass_script_arm_custom_bypass_script,
364  code=code,
365  )
366 
367  async def async_alarm_disarm(self, code: str | None = None) -> None:
368  """Disarm the panel."""
369  await self._async_alarm_arm_async_alarm_arm(
370  AlarmControlPanelState.DISARMED, script=self._disarm_script_disarm_script, code=code
371  )
372 
373  async def async_alarm_trigger(self, code: str | None = None) -> None:
374  """Trigger the panel."""
375  await self._async_alarm_arm_async_alarm_arm(
376  AlarmControlPanelState.TRIGGERED,
377  script=self._trigger_script_trigger_script,
378  code=code,
379  )
None __init__(self, HomeAssistant hass, str object_id, dict config, str|None unique_id)
None async_run_script(self, Script script, *_VarsType|None run_variables=None, Context|None context=None)
None add_template_attribute(self, str attribute, Template template, Callable[[Any], Any]|None validator=None, Callable[[Any], None]|None on_update=None, bool none_on_template_error=False)
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
list[AlarmControlPanelTemplate] _async_create_entities(HomeAssistant hass, dict[str, Any] config)
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
dict[str, Any] rewrite_common_legacy_to_modern_conf(HomeAssistant hass, dict[str, Any] entity_cfg, dict[str, str]|None extra_legacy_fields=None)
dr.DeviceInfo|None async_device_info_to_link_from_device_id(HomeAssistant hass, str|None device_id)
Definition: device.py:44
str async_generate_entity_id(str entity_id_format, str|None name, Iterable[str]|None current_ids=None, HomeAssistant|None hass=None)
Definition: entity.py:119