Home Assistant Unofficial Reference 2024.12.1
alarm_control_panel.py
Go to the documentation of this file.
1 """Interfaces with TotalConnect alarm control panels."""
2 
3 from __future__ import annotations
4 
5 from total_connect_client import ArmingHelper
6 from total_connect_client.exceptions import BadResultCodeError, UsercodeInvalid
7 from total_connect_client.location import TotalConnectLocation
8 
10  AlarmControlPanelEntity,
11  AlarmControlPanelEntityFeature,
12  AlarmControlPanelState,
13  CodeFormat,
14 )
15 from homeassistant.config_entries import ConfigEntry
16 from homeassistant.core import HomeAssistant
17 from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
18 from homeassistant.helpers import entity_platform
19 from homeassistant.helpers.entity_platform import AddEntitiesCallback
20 
21 from .const import CODE_REQUIRED, DOMAIN
22 from .coordinator import TotalConnectDataUpdateCoordinator
23 from .entity import TotalConnectLocationEntity
24 
25 SERVICE_ALARM_ARM_AWAY_INSTANT = "arm_away_instant"
26 SERVICE_ALARM_ARM_HOME_INSTANT = "arm_home_instant"
27 
28 
30  hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
31 ) -> None:
32  """Set up TotalConnect alarm panels based on a config entry."""
33  coordinator: TotalConnectDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
34  code_required = entry.options.get(CODE_REQUIRED, False)
35 
37  TotalConnectAlarm(coordinator, location, partition_id, code_required)
38  for location in coordinator.client.locations.values()
39  for partition_id in location.partitions
40  )
41 
42  # Set up services
43  platform = entity_platform.async_get_current_platform()
44 
45  platform.async_register_entity_service(
46  SERVICE_ALARM_ARM_AWAY_INSTANT,
47  None,
48  "async_alarm_arm_away_instant",
49  )
50 
51  platform.async_register_entity_service(
52  SERVICE_ALARM_ARM_HOME_INSTANT,
53  None,
54  "async_alarm_arm_home_instant",
55  )
56 
57 
59  """Represent a TotalConnect alarm panel."""
60 
61  _attr_supported_features = (
62  AlarmControlPanelEntityFeature.ARM_HOME
63  | AlarmControlPanelEntityFeature.ARM_AWAY
64  | AlarmControlPanelEntityFeature.ARM_NIGHT
65  )
66 
67  def __init__(
68  self,
69  coordinator: TotalConnectDataUpdateCoordinator,
70  location: TotalConnectLocation,
71  partition_id: int,
72  require_code: bool,
73  ) -> None:
74  """Initialize the TotalConnect status."""
75  super().__init__(coordinator, location)
76  self._partition_id_partition_id = partition_id
77  self._partition_partition = self._location_location.partitions[partition_id]
78 
79  """
80  Set unique_id to location_id for partition 1 to avoid breaking change
81  for most users with new support for partitions.
82  Add _# for partition 2 and beyond.
83  """
84  if partition_id == 1:
85  self._attr_name_attr_name = None
86  self._attr_unique_id_attr_unique_id = str(location.location_id)
87  else:
88  self._attr_translation_key_attr_translation_key = "partition"
89  self._attr_translation_placeholders_attr_translation_placeholders = {"partition_id": str(partition_id)}
90  self._attr_unique_id_attr_unique_id = f"{location.location_id}_{partition_id}"
91 
92  self._attr_code_arm_required_attr_code_arm_required = require_code
93  if require_code:
94  self._attr_code_format_attr_code_format = CodeFormat.NUMBER
95 
96  @property
97  def alarm_state(self) -> AlarmControlPanelState | None:
98  """Return the state of the device."""
99  # State attributes can be removed in 2025.3
100  attr = {
101  "location_id": self._location_location.location_id,
102  "partition": self._partition_id_partition_id,
103  "ac_loss": self._location_location.ac_loss,
104  "low_battery": self._location_location.low_battery,
105  "cover_tampered": self._location_location.is_cover_tampered(),
106  "triggered_source": None,
107  "triggered_zone": None,
108  }
109 
110  if self._partition_id_partition_id == 1:
111  attr["location_name"] = self.devicedevice.name
112  else:
113  attr["location_name"] = f"{self.device.name} partition {self._partition_id}"
114 
115  state: AlarmControlPanelState | None = None
116  if self._partition_partition.arming_state.is_disarmed():
117  state = AlarmControlPanelState.DISARMED
118  elif self._partition_partition.arming_state.is_armed_night():
119  state = AlarmControlPanelState.ARMED_NIGHT
120  elif self._partition_partition.arming_state.is_armed_home():
121  state = AlarmControlPanelState.ARMED_HOME
122  elif self._partition_partition.arming_state.is_armed_away():
123  state = AlarmControlPanelState.ARMED_AWAY
124  elif self._partition_partition.arming_state.is_armed_custom_bypass():
125  state = AlarmControlPanelState.ARMED_CUSTOM_BYPASS
126  elif self._partition_partition.arming_state.is_arming():
127  state = AlarmControlPanelState.ARMING
128  elif self._partition_partition.arming_state.is_disarming():
129  state = AlarmControlPanelState.DISARMING
130  elif self._partition_partition.arming_state.is_triggered_police():
131  state = AlarmControlPanelState.TRIGGERED
132  attr["triggered_source"] = "Police/Medical"
133  elif self._partition_partition.arming_state.is_triggered_fire():
134  state = AlarmControlPanelState.TRIGGERED
135  attr["triggered_source"] = "Fire/Smoke"
136  elif self._partition_partition.arming_state.is_triggered_gas():
137  state = AlarmControlPanelState.TRIGGERED
138  attr["triggered_source"] = "Carbon Monoxide"
139 
140  self._attr_extra_state_attributes_attr_extra_state_attributes = attr
141 
142  return state
143 
144  async def async_alarm_disarm(self, code: str | None = None) -> None:
145  """Send disarm command."""
146  self._check_usercode_check_usercode(code)
147  try:
148  await self.hasshasshass.async_add_executor_job(self._disarm_disarm)
149  except UsercodeInvalid as error:
150  self.coordinator.config_entry.async_start_reauth(self.hasshasshass)
151  raise HomeAssistantError(
152  translation_domain=DOMAIN,
153  translation_key="disarm_invalid_code",
154  ) from error
155  except BadResultCodeError as error:
156  raise HomeAssistantError(
157  translation_domain=DOMAIN,
158  translation_key="disarm_failed",
159  translation_placeholders={"device": self.devicedevice.name},
160  ) from error
161  await self.coordinator.async_request_refresh()
162 
163  def _disarm(self) -> None:
164  """Disarm synchronous."""
165  ArmingHelper(self._partition_partition).disarm()
166 
167  async def async_alarm_arm_home(self, code: str | None = None) -> None:
168  """Send arm home command."""
169  self._check_usercode_check_usercode(code)
170  try:
171  await self.hasshasshass.async_add_executor_job(self._arm_home_arm_home)
172  except UsercodeInvalid as error:
173  self.coordinator.config_entry.async_start_reauth(self.hasshasshass)
174  raise HomeAssistantError(
175  translation_domain=DOMAIN,
176  translation_key="arm_home_invalid_code",
177  ) from error
178  except BadResultCodeError as error:
179  raise HomeAssistantError(
180  translation_domain=DOMAIN,
181  translation_key="arm_home_failed",
182  translation_placeholders={"device": self.devicedevice.name},
183  ) from error
184  await self.coordinator.async_request_refresh()
185 
186  def _arm_home(self) -> None:
187  """Arm home synchronous."""
188  ArmingHelper(self._partition_partition).arm_stay()
189 
190  async def async_alarm_arm_away(self, code: str | None = None) -> None:
191  """Send arm away command."""
192  self._check_usercode_check_usercode(code)
193  try:
194  await self.hasshasshass.async_add_executor_job(self._arm_away_arm_away)
195  except UsercodeInvalid as error:
196  self.coordinator.config_entry.async_start_reauth(self.hasshasshass)
197  raise HomeAssistantError(
198  translation_domain=DOMAIN,
199  translation_key="arm_away_invalid_code",
200  ) from error
201  except BadResultCodeError as error:
202  raise HomeAssistantError(
203  translation_domain=DOMAIN,
204  translation_key="arm_away_failed",
205  translation_placeholders={"device": self.devicedevice.name},
206  ) from error
207  await self.coordinator.async_request_refresh()
208 
209  def _arm_away(self) -> None:
210  """Arm away synchronous."""
211  ArmingHelper(self._partition_partition).arm_away()
212 
213  async def async_alarm_arm_night(self, code: str | None = None) -> None:
214  """Send arm night command."""
215  self._check_usercode_check_usercode(code)
216  try:
217  await self.hasshasshass.async_add_executor_job(self._arm_night_arm_night)
218  except UsercodeInvalid as error:
219  self.coordinator.config_entry.async_start_reauth(self.hasshasshass)
220  raise HomeAssistantError(
221  translation_domain=DOMAIN,
222  translation_key="arm_night_invalid_code",
223  ) from error
224  except BadResultCodeError as error:
225  raise HomeAssistantError(
226  translation_domain=DOMAIN,
227  translation_key="arm_night_failed",
228  translation_placeholders={"device": self.devicedevice.name},
229  ) from error
230  await self.coordinator.async_request_refresh()
231 
232  def _arm_night(self) -> None:
233  """Arm night synchronous."""
234  ArmingHelper(self._partition_partition).arm_stay_night()
235 
236  async def async_alarm_arm_home_instant(self) -> None:
237  """Send arm home instant command."""
238  try:
239  await self.hasshasshass.async_add_executor_job(self._arm_home_instant_arm_home_instant)
240  except UsercodeInvalid as error:
241  self.coordinator.config_entry.async_start_reauth(self.hasshasshass)
242  raise HomeAssistantError(
243  translation_domain=DOMAIN,
244  translation_key="arm_home_instant_invalid_code",
245  ) from error
246  except BadResultCodeError as error:
247  raise HomeAssistantError(
248  translation_domain=DOMAIN,
249  translation_key="arm_home_instant_failed",
250  translation_placeholders={"device": self.devicedevice.name},
251  ) from error
252  await self.coordinator.async_request_refresh()
253 
254  def _arm_home_instant(self):
255  """Arm home instant synchronous."""
256  ArmingHelper(self._partition_partition).arm_stay_instant()
257 
258  async def async_alarm_arm_away_instant(self) -> None:
259  """Send arm away instant command."""
260  try:
261  await self.hasshasshass.async_add_executor_job(self._arm_away_instant_arm_away_instant)
262  except UsercodeInvalid as error:
263  self.coordinator.config_entry.async_start_reauth(self.hasshasshass)
264  raise HomeAssistantError(
265  translation_domain=DOMAIN,
266  translation_key="arm_away_instant_invalid_code",
267  ) from error
268  except BadResultCodeError as error:
269  raise HomeAssistantError(
270  translation_domain=DOMAIN,
271  translation_key="arm_away_instant_failed",
272  translation_placeholders={"device": self.devicedevice.name},
273  ) from error
274  await self.coordinator.async_request_refresh()
275 
276  def _arm_away_instant(self):
277  """Arm away instant synchronous."""
278  ArmingHelper(self._partition_partition).arm_away_instant()
279 
280  def _check_usercode(self, code):
281  """Check if the run-time entered code matches configured code."""
282  if (
283  self._attr_code_arm_required_attr_code_arm_required
284  and self.coordinator.client.usercodes[self._location_location.location_id] != code
285  ):
287  translation_domain=DOMAIN, translation_key="invalid_pin"
288  )
None __init__(self, TotalConnectDataUpdateCoordinator coordinator, TotalConnectLocation location, int partition_id, bool require_code)
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)