Home Assistant Unofficial Reference 2024.12.1
alarm_control_panel.py
Go to the documentation of this file.
1 """Each ElkM1 area will be created as a separate alarm_control_panel."""
2 
3 from __future__ import annotations
4 
5 from typing import Any
6 
7 from elkm1_lib.areas import Area
8 from elkm1_lib.const import AlarmState, ArmedStatus, ArmLevel, ArmUpState
9 from elkm1_lib.elements import Element
10 from elkm1_lib.elk import Elk
11 from elkm1_lib.keypads import Keypad
12 import voluptuous as vol
13 
15  ATTR_CHANGED_BY,
16  AlarmControlPanelEntity,
17  AlarmControlPanelEntityFeature,
18  AlarmControlPanelState,
19  CodeFormat,
20 )
21 from homeassistant.core import HomeAssistant
22 from homeassistant.helpers import entity_platform
24 from homeassistant.helpers.entity_platform import AddEntitiesCallback
25 from homeassistant.helpers.restore_state import RestoreEntity
26 from homeassistant.helpers.typing import VolDictType
27 
28 from . import ElkM1ConfigEntry
29 from .const import (
30  ATTR_CHANGED_BY_ID,
31  ATTR_CHANGED_BY_KEYPAD,
32  ATTR_CHANGED_BY_TIME,
33  ELK_USER_CODE_SERVICE_SCHEMA,
34 )
35 from .entity import ElkAttachedEntity, ElkEntity, create_elk_entities
36 from .models import ELKM1Data
37 
38 DISPLAY_MESSAGE_SERVICE_SCHEMA: VolDictType = {
39  vol.Optional("clear", default=2): vol.All(vol.Coerce(int), vol.In([0, 1, 2])),
40  vol.Optional("beep", default=False): cv.boolean,
41  vol.Optional("timeout", default=0): vol.All(
42  vol.Coerce(int), vol.Range(min=0, max=65535)
43  ),
44  vol.Optional("line1", default=""): cv.string,
45  vol.Optional("line2", default=""): cv.string,
46 }
47 
48 SERVICE_ALARM_DISPLAY_MESSAGE = "alarm_display_message"
49 SERVICE_ALARM_ARM_VACATION = "alarm_arm_vacation"
50 SERVICE_ALARM_ARM_HOME_INSTANT = "alarm_arm_home_instant"
51 SERVICE_ALARM_ARM_NIGHT_INSTANT = "alarm_arm_night_instant"
52 SERVICE_ALARM_BYPASS = "alarm_bypass"
53 SERVICE_ALARM_CLEAR_BYPASS = "alarm_clear_bypass"
54 
55 
57  hass: HomeAssistant,
58  config_entry: ElkM1ConfigEntry,
59  async_add_entities: AddEntitiesCallback,
60 ) -> None:
61  """Set up the ElkM1 alarm platform."""
62  elk_data = config_entry.runtime_data
63  elk = elk_data.elk
64  entities: list[ElkEntity] = []
65  create_elk_entities(elk_data, elk.areas, "area", ElkArea, entities)
66  async_add_entities(entities)
67 
68  platform = entity_platform.async_get_current_platform()
69 
70  platform.async_register_entity_service(
71  SERVICE_ALARM_ARM_VACATION,
72  ELK_USER_CODE_SERVICE_SCHEMA,
73  "async_alarm_arm_vacation",
74  )
75  platform.async_register_entity_service(
76  SERVICE_ALARM_ARM_HOME_INSTANT,
77  ELK_USER_CODE_SERVICE_SCHEMA,
78  "async_alarm_arm_home_instant",
79  )
80  platform.async_register_entity_service(
81  SERVICE_ALARM_ARM_NIGHT_INSTANT,
82  ELK_USER_CODE_SERVICE_SCHEMA,
83  "async_alarm_arm_night_instant",
84  )
85  platform.async_register_entity_service(
86  SERVICE_ALARM_DISPLAY_MESSAGE,
87  DISPLAY_MESSAGE_SERVICE_SCHEMA,
88  "async_display_message",
89  )
90  platform.async_register_entity_service(
91  SERVICE_ALARM_BYPASS,
92  ELK_USER_CODE_SERVICE_SCHEMA,
93  "async_bypass",
94  )
95  platform.async_register_entity_service(
96  SERVICE_ALARM_CLEAR_BYPASS,
97  ELK_USER_CODE_SERVICE_SCHEMA,
98  "async_clear_bypass",
99  )
100 
101 
103  """Representation of an Area / Partition within the ElkM1 alarm panel."""
104 
105  _attr_supported_features = (
106  AlarmControlPanelEntityFeature.ARM_HOME
107  | AlarmControlPanelEntityFeature.ARM_AWAY
108  | AlarmControlPanelEntityFeature.ARM_NIGHT
109  )
110  _element: Area
111 
112  def __init__(self, element: Element, elk: Elk, elk_data: ELKM1Data) -> None:
113  """Initialize Area as Alarm Control Panel."""
114  super().__init__(element, elk, elk_data)
115  self._elk_elk_elk = elk
116  self._changed_by_keypad_changed_by_keypad: str | None = None
117  self._changed_by_time_changed_by_time: str | None = None
118  self._changed_by_id_changed_by_id: int | None = None
119  self._changed_by_changed_by: str | None = None
120  self._state_state: AlarmControlPanelState | None = None
121 
122  async def async_added_to_hass(self) -> None:
123  """Register callback for ElkM1 changes."""
124  await super().async_added_to_hass()
125  if len(self._elk_elk_elk.areas.elements) == 1:
126  for keypad in self._elk_elk_elk.keypads:
127  keypad.add_callback(self._watch_keypad_watch_keypad)
128  self._element_element.add_callback(self._watch_area_watch_area)
129 
130  # We do not get changed_by back from resync.
131  if not (last_state := await self.async_get_last_stateasync_get_last_state()):
132  return
133 
134  if ATTR_CHANGED_BY_KEYPAD in last_state.attributes:
135  self._changed_by_keypad_changed_by_keypad = last_state.attributes[ATTR_CHANGED_BY_KEYPAD]
136  if ATTR_CHANGED_BY_TIME in last_state.attributes:
137  self._changed_by_time_changed_by_time = last_state.attributes[ATTR_CHANGED_BY_TIME]
138  if ATTR_CHANGED_BY_ID in last_state.attributes:
139  self._changed_by_id_changed_by_id = last_state.attributes[ATTR_CHANGED_BY_ID]
140  if ATTR_CHANGED_BY in last_state.attributes:
141  self._changed_by_changed_by = last_state.attributes[ATTR_CHANGED_BY]
142 
143  def _watch_keypad(self, keypad: Element, changeset: dict[str, Any]) -> None:
144  assert isinstance(keypad, Keypad)
145  if keypad.area != self._element_element.index:
146  return
147  if changeset.get("last_user") is not None:
148  self._changed_by_keypad_changed_by_keypad = keypad.name
149  self._changed_by_time_changed_by_time = keypad.last_user_time.isoformat()
150  self._changed_by_id_changed_by_id = keypad.last_user + 1
151  self._changed_by_changed_by = self._elk_elk_elk.users.username(keypad.last_user)
152  self.async_write_ha_stateasync_write_ha_state()
153 
154  def _watch_area(self, area: Element, changeset: dict[str, Any]) -> None:
155  if not (last_log := changeset.get("last_log")):
156  return
157  # user_number only set for arm/disarm logs
158  if (user_number := last_log.get("user_number")) is None:
159  return
160  self._changed_by_keypad_changed_by_keypad = None
161  self._changed_by_id_changed_by_id = user_number
162  self._changed_by_changed_by = self._elk_elk_elk.users.username(user_number - 1)
163  self._changed_by_time_changed_by_time = last_log["timestamp"]
164  self.async_write_ha_stateasync_write_ha_state()
165 
166  @property
167  def code_format(self) -> CodeFormat | None:
168  """Return the alarm code format."""
169  return CodeFormat.NUMBER
170 
171  @property
172  def alarm_state(self) -> AlarmControlPanelState | None:
173  """Return the state of the element."""
174  return self._state_state
175 
176  @property
177  def extra_state_attributes(self) -> dict[str, Any]:
178  """Attributes of the area."""
179  attrs = self.initial_attrsinitial_attrs()
180  elmt = self._element_element
181  attrs["is_exit"] = elmt.is_exit
182  attrs["timer1"] = elmt.timer1
183  attrs["timer2"] = elmt.timer2
184  if elmt.armed_status is not None:
185  attrs["armed_status"] = ArmedStatus(elmt.armed_status).name.lower()
186  if elmt.arm_up_state is not None:
187  attrs["arm_up_state"] = ArmUpState(elmt.arm_up_state).name.lower()
188  if elmt.alarm_state is not None:
189  attrs["alarm_state"] = AlarmState(elmt.alarm_state).name.lower()
190  attrs[ATTR_CHANGED_BY_KEYPAD] = self._changed_by_keypad_changed_by_keypad
191  attrs[ATTR_CHANGED_BY_TIME] = self._changed_by_time_changed_by_time
192  attrs[ATTR_CHANGED_BY_ID] = self._changed_by_id_changed_by_id
193  return attrs
194 
195  @property
196  def changed_by(self) -> str | None:
197  """Last change triggered by."""
198  return self._changed_by_changed_by
199 
200  def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
201  elk_state_to_hass_state = {
202  ArmedStatus.DISARMED: AlarmControlPanelState.DISARMED,
203  ArmedStatus.ARMED_AWAY: AlarmControlPanelState.ARMED_AWAY,
204  ArmedStatus.ARMED_STAY: AlarmControlPanelState.ARMED_HOME,
205  ArmedStatus.ARMED_STAY_INSTANT: AlarmControlPanelState.ARMED_HOME,
206  ArmedStatus.ARMED_TO_NIGHT: AlarmControlPanelState.ARMED_NIGHT,
207  ArmedStatus.ARMED_TO_NIGHT_INSTANT: AlarmControlPanelState.ARMED_NIGHT,
208  ArmedStatus.ARMED_TO_VACATION: AlarmControlPanelState.ARMED_AWAY,
209  }
210 
211  if self._element_element.alarm_state is None:
212  self._state_state = None
213  elif self._element_element.in_alarm_state():
214  # Area is in alarm state
215  self._state_state = AlarmControlPanelState.TRIGGERED
216  elif self._entry_exit_timer_is_running_entry_exit_timer_is_running():
217  self._state_state = (
218  AlarmControlPanelState.ARMING
219  if self._element_element.is_exit
220  else AlarmControlPanelState.PENDING
221  )
222  elif self._element_element.armed_status is not None:
223  self._state_state = elk_state_to_hass_state[self._element_element.armed_status]
224  else:
225  self._state_state = None
226 
227  def _entry_exit_timer_is_running(self) -> bool:
228  return self._element_element.timer1 > 0 or self._element_element.timer2 > 0
229 
230  async def async_alarm_disarm(self, code: str | None = None) -> None:
231  """Send disarm command."""
232  if code is not None:
233  self._element_element.disarm(int(code))
234 
235  async def async_alarm_arm_home(self, code: str | None = None) -> None:
236  """Send arm home command."""
237  if code is not None:
238  self._element_element.arm(ArmLevel.ARMED_STAY, int(code))
239 
240  async def async_alarm_arm_away(self, code: str | None = None) -> None:
241  """Send arm away command."""
242  if code is not None:
243  self._element_element.arm(ArmLevel.ARMED_AWAY, int(code))
244 
245  async def async_alarm_arm_night(self, code: str | None = None) -> None:
246  """Send arm night command."""
247  if code is not None:
248  self._element_element.arm(ArmLevel.ARMED_NIGHT, int(code))
249 
250  async def async_alarm_arm_home_instant(self, code: str | None = None) -> None:
251  """Send arm stay instant command."""
252  if code is not None:
253  self._element_element.arm(ArmLevel.ARMED_STAY_INSTANT, int(code))
254 
255  async def async_alarm_arm_night_instant(self, code: str | None = None) -> None:
256  """Send arm night instant command."""
257  if code is not None:
258  self._element_element.arm(ArmLevel.ARMED_NIGHT_INSTANT, int(code))
259 
260  async def async_alarm_arm_vacation(self, code: str | None = None) -> None:
261  """Send arm vacation command."""
262  if code is not None:
263  self._element_element.arm(ArmLevel.ARMED_VACATION, int(code))
264 
266  self, clear: int, beep: bool, timeout: int, line1: str, line2: str
267  ) -> None:
268  """Display a message on all keypads for the area."""
269  self._element_element.display_message(clear, beep, timeout, line1, line2)
270 
271  async def async_bypass(self, code: str | None = None) -> None:
272  """Bypass all zones in area."""
273  if code is not None:
274  self._element_element.bypass(int(code))
275 
276  async def async_clear_bypass(self, code: str | None = None) -> None:
277  """Clear bypass for all zones in area."""
278  if code is not None:
279  self._element_element.clear_bypass(int(code))
None _watch_keypad(self, Element keypad, dict[str, Any] changeset)
None async_display_message(self, int clear, bool beep, int timeout, str line1, str line2)
None __init__(self, Element element, Elk elk, ELKM1Data elk_data)
None _element_changed(self, Element element, dict[str, Any] changeset)
None _watch_area(self, Element area, dict[str, Any] changeset)
None async_setup_entry(HomeAssistant hass, ElkM1ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
list[ElkEntity]|None create_elk_entities(ELKM1Data elk_data, Iterable[Element] elk_elements, str element_type, Any class_, list[ElkEntity] entities)
Definition: entity.py:30