Home Assistant Unofficial Reference 2024.12.1
alarm_control_panel.py
Go to the documentation of this file.
1 """Support for manual alarms."""
2 
3 from __future__ import annotations
4 
5 import datetime
6 from typing import Any
7 
8 import voluptuous as vol
9 
11  PLATFORM_SCHEMA as ALARM_CONTROL_PANEL_PLATFORM_SCHEMA,
12  AlarmControlPanelEntity,
13  AlarmControlPanelEntityFeature,
14  AlarmControlPanelState,
15  CodeFormat,
16 )
17 from homeassistant.const import (
18  CONF_ARMING_TIME,
19  CONF_CODE,
20  CONF_DELAY_TIME,
21  CONF_DISARM_AFTER_TRIGGER,
22  CONF_NAME,
23  CONF_TRIGGER_TIME,
24  CONF_UNIQUE_ID,
25 )
26 from homeassistant.core import HomeAssistant, callback
27 from homeassistant.exceptions import ServiceValidationError
29 from homeassistant.helpers.entity_platform import AddEntitiesCallback
30 from homeassistant.helpers.event import async_track_point_in_time
31 from homeassistant.helpers.restore_state import RestoreEntity
32 from homeassistant.helpers.template import Template
33 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
34 import homeassistant.util.dt as dt_util
35 
36 DOMAIN = "manual"
37 
38 CONF_ARMING_STATES = "arming_states"
39 CONF_CODE_TEMPLATE = "code_template"
40 CONF_CODE_ARM_REQUIRED = "code_arm_required"
41 
42 CONF_ALARM_ARMED_AWAY = "armed_away"
43 CONF_ALARM_ARMED_CUSTOM_BYPASS = "armed_custom_bypass"
44 CONF_ALARM_ARMED_HOME = "armed_home"
45 CONF_ALARM_ARMED_NIGHT = "armed_night"
46 CONF_ALARM_ARMED_VACATION = "armed_vacation"
47 CONF_ALARM_ARMING = "arming"
48 CONF_ALARM_DISARMED = "disarmed"
49 CONF_ALARM_PENDING = "pending"
50 CONF_ALARM_TRIGGERED = "triggered"
51 
52 DEFAULT_ALARM_NAME = "HA Alarm"
53 DEFAULT_DELAY_TIME = datetime.timedelta(seconds=60)
54 DEFAULT_ARMING_TIME = datetime.timedelta(seconds=60)
55 DEFAULT_TRIGGER_TIME = datetime.timedelta(seconds=120)
56 DEFAULT_DISARM_AFTER_TRIGGER = False
57 
58 SUPPORTED_STATES = [
59  AlarmControlPanelState.DISARMED,
60  AlarmControlPanelState.ARMED_AWAY,
61  AlarmControlPanelState.ARMED_HOME,
62  AlarmControlPanelState.ARMED_NIGHT,
63  AlarmControlPanelState.ARMED_VACATION,
64  AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
65  AlarmControlPanelState.TRIGGERED,
66 ]
67 
68 SUPPORTED_PRETRIGGER_STATES = [
69  state for state in SUPPORTED_STATES if state != AlarmControlPanelState.TRIGGERED
70 ]
71 
72 SUPPORTED_ARMING_STATES = [
73  state
74  for state in SUPPORTED_STATES
75  if state
76  not in (
77  AlarmControlPanelState.DISARMED,
78  AlarmControlPanelState.TRIGGERED,
79  )
80 ]
81 
82 SUPPORTED_ARMING_STATE_TO_FEATURE = {
83  AlarmControlPanelState.ARMED_AWAY: AlarmControlPanelEntityFeature.ARM_AWAY,
84  AlarmControlPanelState.ARMED_HOME: AlarmControlPanelEntityFeature.ARM_HOME,
85  AlarmControlPanelState.ARMED_NIGHT: AlarmControlPanelEntityFeature.ARM_NIGHT,
86  AlarmControlPanelState.ARMED_VACATION: AlarmControlPanelEntityFeature.ARM_VACATION,
87  AlarmControlPanelState.ARMED_CUSTOM_BYPASS: AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS,
88 }
89 
90 ATTR_PREVIOUS_STATE = "previous_state"
91 ATTR_NEXT_STATE = "next_state"
92 
93 
95  config: dict[AlarmControlPanelState | str, Any],
96 ) -> dict[str, Any]:
97  """Validate the state."""
98  state: AlarmControlPanelState
99  for state in SUPPORTED_PRETRIGGER_STATES:
100  if CONF_DELAY_TIME not in config[state]:
101  config[state] = config[state] | {CONF_DELAY_TIME: config[CONF_DELAY_TIME]}
102  if CONF_TRIGGER_TIME not in config[state]:
103  config[state] = config[state] | {
104  CONF_TRIGGER_TIME: config[CONF_TRIGGER_TIME]
105  }
106  for state in SUPPORTED_ARMING_STATES:
107  if CONF_ARMING_TIME not in config[state]:
108  config[state] = config[state] | {CONF_ARMING_TIME: config[CONF_ARMING_TIME]}
109 
110  return config
111 
112 
113 def _state_schema(state: str) -> vol.Schema:
114  """Validate the state."""
115  schema = {}
116  if state in SUPPORTED_PRETRIGGER_STATES:
117  schema[vol.Optional(CONF_DELAY_TIME)] = vol.All(
118  cv.time_period, cv.positive_timedelta
119  )
120  schema[vol.Optional(CONF_TRIGGER_TIME)] = vol.All(
121  cv.time_period, cv.positive_timedelta
122  )
123  if state in SUPPORTED_ARMING_STATES:
124  schema[vol.Optional(CONF_ARMING_TIME)] = vol.All(
125  cv.time_period, cv.positive_timedelta
126  )
127  return vol.Schema(schema)
128 
129 
130 PLATFORM_SCHEMA = vol.Schema(
131  vol.All(
132  ALARM_CONTROL_PANEL_PLATFORM_SCHEMA.extend(
133  {
134  vol.Optional(CONF_NAME, default=DEFAULT_ALARM_NAME): cv.string,
135  vol.Optional(CONF_UNIQUE_ID): cv.string,
136  vol.Exclusive(CONF_CODE, "code validation"): cv.string,
137  vol.Exclusive(CONF_CODE_TEMPLATE, "code validation"): cv.template,
138  vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean,
139  vol.Optional(CONF_DELAY_TIME, default=DEFAULT_DELAY_TIME): vol.All(
140  cv.time_period, cv.positive_timedelta
141  ),
142  vol.Optional(CONF_ARMING_TIME, default=DEFAULT_ARMING_TIME): vol.All(
143  cv.time_period, cv.positive_timedelta
144  ),
145  vol.Optional(CONF_TRIGGER_TIME, default=DEFAULT_TRIGGER_TIME): vol.All(
146  cv.time_period, cv.positive_timedelta
147  ),
148  vol.Optional(
149  CONF_DISARM_AFTER_TRIGGER, default=DEFAULT_DISARM_AFTER_TRIGGER
150  ): cv.boolean,
151  vol.Optional(
152  CONF_ARMING_STATES, default=SUPPORTED_ARMING_STATES
153  ): vol.All(cv.ensure_list, [vol.In(SUPPORTED_ARMING_STATES)]),
154  vol.Optional(CONF_ALARM_ARMED_AWAY, default={}): _state_schema(
155  AlarmControlPanelState.ARMED_AWAY
156  ),
157  vol.Optional(CONF_ALARM_ARMED_HOME, default={}): _state_schema(
158  AlarmControlPanelState.ARMED_HOME
159  ),
160  vol.Optional(CONF_ALARM_ARMED_NIGHT, default={}): _state_schema(
161  AlarmControlPanelState.ARMED_NIGHT
162  ),
163  vol.Optional(CONF_ALARM_ARMED_VACATION, default={}): _state_schema(
164  AlarmControlPanelState.ARMED_VACATION
165  ),
166  vol.Optional(CONF_ALARM_ARMED_CUSTOM_BYPASS, default={}): _state_schema(
167  AlarmControlPanelState.ARMED_CUSTOM_BYPASS
168  ),
169  vol.Optional(CONF_ALARM_DISARMED, default={}): _state_schema(
170  AlarmControlPanelState.DISARMED
171  ),
172  vol.Optional(CONF_ALARM_TRIGGERED, default={}): _state_schema(
173  AlarmControlPanelState.TRIGGERED
174  ),
175  },
176  ),
177  _state_validator,
178  )
179 )
180 
181 
183  hass: HomeAssistant,
184  config: ConfigType,
185  async_add_entities: AddEntitiesCallback,
186  discovery_info: DiscoveryInfoType | None = None,
187 ) -> None:
188  """Set up the manual alarm platform."""
190  [
191  ManualAlarm(
192  hass,
193  config[CONF_NAME],
194  config.get(CONF_UNIQUE_ID),
195  config.get(CONF_CODE),
196  config.get(CONF_CODE_TEMPLATE),
197  config[CONF_CODE_ARM_REQUIRED],
198  config[CONF_DISARM_AFTER_TRIGGER],
199  config,
200  )
201  ]
202  )
203 
204 
206  """Representation of an alarm status.
207 
208  When armed, will be arming for 'arming_time', after that armed.
209  When triggered, will be pending for the triggering state's 'delay_time'.
210  After that will be triggered for 'trigger_time', after that we return to
211  the previous state or disarm if `disarm_after_trigger` is true.
212  A trigger_time of zero disables the alarm_trigger service.
213  """
214 
215  _attr_should_poll = False
216 
217  def __init__(
218  self,
219  hass: HomeAssistant,
220  name: str,
221  unique_id: str | None,
222  code: str | None,
223  code_template: Template | None,
224  code_arm_required: bool,
225  disarm_after_trigger: bool,
226  config: dict[str, Any],
227  ) -> None:
228  """Init the manual alarm panel."""
229  self._state_state: AlarmControlPanelState = AlarmControlPanelState.DISARMED
230  self._hass_hass = hass
231  self._attr_name_attr_name = name
232  self._attr_unique_id_attr_unique_id = unique_id
233  self._code_code = code_template or code or None
234  self._attr_code_arm_required_attr_code_arm_required = code_arm_required
235  self._disarm_after_trigger_disarm_after_trigger = disarm_after_trigger
236  self._previous_state_previous_state: AlarmControlPanelState = self._state_state
237  self._state_ts_state_ts: datetime.datetime = dt_util.utcnow()
238 
239  self._delay_time_by_state: dict[AlarmControlPanelState, Any] = {
240  state: config[state][CONF_DELAY_TIME]
241  for state in SUPPORTED_PRETRIGGER_STATES
242  }
243  self._trigger_time_by_state: dict[AlarmControlPanelState, Any] = {
244  state: config[state][CONF_TRIGGER_TIME]
245  for state in SUPPORTED_PRETRIGGER_STATES
246  }
247  self._arming_time_by_state: dict[AlarmControlPanelState, Any] = {
248  state: config[state][CONF_ARMING_TIME] for state in SUPPORTED_ARMING_STATES
249  }
250 
251  self._attr_supported_features_attr_supported_features = AlarmControlPanelEntityFeature.TRIGGER
252  for arming_state in config.get(CONF_ARMING_STATES, SUPPORTED_ARMING_STATES):
253  self._attr_supported_features_attr_supported_features |= SUPPORTED_ARMING_STATE_TO_FEATURE[
254  arming_state
255  ]
256 
257  @property
258  def alarm_state(self) -> AlarmControlPanelState:
259  """Return the state of the device."""
260  if self._state_state == AlarmControlPanelState.TRIGGERED:
261  if self._within_pending_time_within_pending_time(self._state_state):
262  return AlarmControlPanelState.PENDING
263  trigger_time: datetime.timedelta = self._trigger_time_by_state[
264  self._previous_state_previous_state
265  ]
266  if (
267  self._state_ts_state_ts + self._pending_time_pending_time(self._state_state) + trigger_time
268  ) < dt_util.utcnow():
269  if self._disarm_after_trigger_disarm_after_trigger:
270  return AlarmControlPanelState.DISARMED
271  self._state_state = self._previous_state_previous_state
272  return self._state_state
273 
274  if self._state_state in SUPPORTED_ARMING_STATES and self._within_arming_time_within_arming_time(
275  self._state_state
276  ):
277  return AlarmControlPanelState.ARMING
278 
279  return self._state_state
280 
281  @property
282  def _active_state(self) -> AlarmControlPanelState:
283  """Get the current state."""
284  if self.statestatestatestate in (
285  AlarmControlPanelState.PENDING,
286  AlarmControlPanelState.ARMING,
287  ):
288  return self._previous_state_previous_state
289  return self._state_state
290 
291  def _arming_time(self, state: AlarmControlPanelState) -> datetime.timedelta:
292  """Get the arming time."""
293  arming_time: datetime.timedelta = self._arming_time_by_state[state]
294  return arming_time
295 
296  def _pending_time(self, state: AlarmControlPanelState) -> datetime.timedelta:
297  """Get the pending time."""
298  delay_time: datetime.timedelta = self._delay_time_by_state[self._previous_state_previous_state]
299  return delay_time
300 
301  def _within_arming_time(self, state: AlarmControlPanelState) -> bool:
302  """Get if the action is in the arming time window."""
303  return self._state_ts_state_ts + self._arming_time_arming_time(state) > dt_util.utcnow()
304 
305  def _within_pending_time(self, state: AlarmControlPanelState) -> bool:
306  """Get if the action is in the pending time window."""
307  return self._state_ts_state_ts + self._pending_time_pending_time(state) > dt_util.utcnow()
308 
309  @property
310  def code_format(self) -> CodeFormat | None:
311  """Return one or more digits/characters."""
312  if self._code_code is None:
313  return None
314  if isinstance(self._code_code, str) and self._code_code.isdigit():
315  return CodeFormat.NUMBER
316  return CodeFormat.TEXT
317 
318  async def async_alarm_disarm(self, code: str | None = None) -> None:
319  """Send disarm command."""
320  self._async_validate_code_async_validate_code(code, AlarmControlPanelState.DISARMED)
321  self._state_state = AlarmControlPanelState.DISARMED
322  self._state_ts_state_ts = dt_util.utcnow()
323  self.async_write_ha_stateasync_write_ha_state()
324 
325  async def async_alarm_arm_home(self, code: str | None = None) -> None:
326  """Send arm home command."""
327  self._async_validate_code_async_validate_code(code, AlarmControlPanelState.ARMED_HOME)
328  self._async_update_state_async_update_state(AlarmControlPanelState.ARMED_HOME)
329 
330  async def async_alarm_arm_away(self, code: str | None = None) -> None:
331  """Send arm away command."""
332  self._async_validate_code_async_validate_code(code, AlarmControlPanelState.ARMED_AWAY)
333  self._async_update_state_async_update_state(AlarmControlPanelState.ARMED_AWAY)
334 
335  async def async_alarm_arm_night(self, code: str | None = None) -> None:
336  """Send arm night command."""
337  self._async_validate_code_async_validate_code(code, AlarmControlPanelState.ARMED_NIGHT)
338  self._async_update_state_async_update_state(AlarmControlPanelState.ARMED_NIGHT)
339 
340  async def async_alarm_arm_vacation(self, code: str | None = None) -> None:
341  """Send arm vacation command."""
342  self._async_validate_code_async_validate_code(code, AlarmControlPanelState.ARMED_VACATION)
343  self._async_update_state_async_update_state(AlarmControlPanelState.ARMED_VACATION)
344 
345  async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None:
346  """Send arm custom bypass command."""
347  self._async_validate_code_async_validate_code(code, AlarmControlPanelState.ARMED_CUSTOM_BYPASS)
348  self._async_update_state_async_update_state(AlarmControlPanelState.ARMED_CUSTOM_BYPASS)
349 
350  async def async_alarm_trigger(self, code: str | None = None) -> None:
351  """Send alarm trigger command.
352 
353  No code needed, a trigger time of zero for the current state
354  disables the alarm.
355  """
356  if not self._trigger_time_by_state[self._active_state_active_state]:
357  return
358  self._async_update_state_async_update_state(AlarmControlPanelState.TRIGGERED)
359 
360  def _async_update_state(self, state: AlarmControlPanelState) -> None:
361  """Update the state."""
362  if self._state_state == state:
363  return
364 
365  self._previous_state_previous_state = self._state_state
366  self._state_state = state
367  self._state_ts_state_ts = dt_util.utcnow()
368  self.async_write_ha_stateasync_write_ha_state()
369  self._async_set_state_update_events_async_set_state_update_events()
370 
372  state = self._state_state
373  if state == AlarmControlPanelState.TRIGGERED:
374  pending_time = self._pending_time_pending_time(state)
376  self._hass_hass, self.async_scheduled_updateasync_scheduled_update, self._state_ts_state_ts + pending_time
377  )
378 
379  trigger_time = self._trigger_time_by_state[self._previous_state_previous_state]
381  self._hass_hass,
382  self.async_scheduled_updateasync_scheduled_update,
383  self._state_ts_state_ts + pending_time + trigger_time,
384  )
385  elif state in SUPPORTED_ARMING_STATES:
386  arming_time = self._arming_time_arming_time(state)
387  if arming_time:
389  self._hass_hass,
390  self.async_scheduled_updateasync_scheduled_update,
391  self._state_ts_state_ts + arming_time,
392  )
393 
394  def _async_validate_code(self, code: str | None, state: str) -> None:
395  """Validate given code."""
396  if (
397  state != AlarmControlPanelState.DISARMED and not self.code_arm_requiredcode_arm_required
398  ) or self._code_code is None:
399  return
400 
401  if isinstance(self._code_code, str):
402  alarm_code = self._code_code
403  else:
404  alarm_code = self._code_code.async_render(
405  parse_result=False, from_state=self._state_state, to_state=state
406  )
407 
408  if not alarm_code or code == alarm_code:
409  return
410 
412  "Invalid alarm code provided",
413  translation_domain=DOMAIN,
414  translation_key="invalid_code",
415  )
416 
417  @property
418  def extra_state_attributes(self) -> dict[str, Any]:
419  """Return the state attributes."""
420  if self.statestatestatestate in (
421  AlarmControlPanelState.PENDING,
422  AlarmControlPanelState.ARMING,
423  ):
424  prev_state: str | None = self._previous_state_previous_state
425  state: str | None = self._state_state
426  elif self.statestatestatestate == AlarmControlPanelState.TRIGGERED:
427  prev_state = self._previous_state_previous_state
428  state = None
429  else:
430  prev_state = None
431  state = None
432  return {ATTR_PREVIOUS_STATE: prev_state, ATTR_NEXT_STATE: state}
433 
434  @callback
435  def async_scheduled_update(self, now: datetime.datetime) -> None:
436  """Update state at a scheduled point in time."""
437  self.async_write_ha_stateasync_write_ha_state()
438 
439  async def async_added_to_hass(self) -> None:
440  """Run when entity about to be added to hass."""
441  await super().async_added_to_hass()
442  if state := await self.async_get_last_stateasync_get_last_state():
443  self._state_ts_state_ts = state.last_updated
444  if next_state := state.attributes.get(ATTR_NEXT_STATE):
445  # If in arming or pending state we record the transition,
446  # not the current state
447  self._state_state = AlarmControlPanelState(next_state)
448  else:
449  self._state_state = AlarmControlPanelState(state.state)
450 
451  if prev_state := state.attributes.get(ATTR_PREVIOUS_STATE):
452  self._previous_state_previous_state = prev_state
453  self._async_set_state_update_events_async_set_state_update_events()
None __init__(self, HomeAssistant hass, str name, str|None unique_id, str|None code, Template|None code_template, bool code_arm_required, bool disarm_after_trigger, dict[str, Any] config)
datetime.timedelta _pending_time(self, AlarmControlPanelState state)
datetime.timedelta _arming_time(self, AlarmControlPanelState state)
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
dict[str, Any] _state_validator(dict[AlarmControlPanelState|str, Any] config)
CALLBACK_TYPE async_track_point_in_time(HomeAssistant hass, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action, datetime point_in_time)
Definition: event.py:1462