Home Assistant Unofficial Reference 2024.12.1
type_security_systems.py
Go to the documentation of this file.
1 """Class to hold all alarm control panel accessories."""
2 
3 import logging
4 from typing import Any
5 
6 from pyhap.const import CATEGORY_ALARM_SYSTEM
7 
9  DOMAIN as ALARM_CONTROL_PANEL_DOMAIN,
10  AlarmControlPanelEntityFeature,
11  AlarmControlPanelState,
12 )
13 from homeassistant.const import (
14  ATTR_CODE,
15  ATTR_ENTITY_ID,
16  ATTR_SUPPORTED_FEATURES,
17  SERVICE_ALARM_ARM_AWAY,
18  SERVICE_ALARM_ARM_HOME,
19  SERVICE_ALARM_ARM_NIGHT,
20  SERVICE_ALARM_DISARM,
21  STATE_UNAVAILABLE,
22  STATE_UNKNOWN,
23 )
24 from homeassistant.core import State, callback
25 
26 from .accessories import TYPES, HomeAccessory
27 from .const import (
28  CHAR_CURRENT_SECURITY_STATE,
29  CHAR_TARGET_SECURITY_STATE,
30  SERV_SECURITY_SYSTEM,
31 )
32 
33 _LOGGER = logging.getLogger(__name__)
34 
35 HK_ALARM_STAY_ARMED = 0
36 HK_ALARM_AWAY_ARMED = 1
37 HK_ALARM_NIGHT_ARMED = 2
38 HK_ALARM_DISARMED = 3
39 HK_ALARM_TRIGGERED = 4
40 
41 HASS_TO_HOMEKIT_CURRENT = {
42  AlarmControlPanelState.ARMED_HOME: HK_ALARM_STAY_ARMED,
43  AlarmControlPanelState.ARMED_VACATION: HK_ALARM_AWAY_ARMED,
44  AlarmControlPanelState.ARMED_AWAY: HK_ALARM_AWAY_ARMED,
45  AlarmControlPanelState.ARMED_NIGHT: HK_ALARM_NIGHT_ARMED,
46  AlarmControlPanelState.ARMING: HK_ALARM_DISARMED,
47  AlarmControlPanelState.DISARMED: HK_ALARM_DISARMED,
48  AlarmControlPanelState.TRIGGERED: HK_ALARM_TRIGGERED,
49 }
50 
51 HASS_TO_HOMEKIT_TARGET = {
52  AlarmControlPanelState.ARMED_HOME: HK_ALARM_STAY_ARMED,
53  AlarmControlPanelState.ARMED_VACATION: HK_ALARM_AWAY_ARMED,
54  AlarmControlPanelState.ARMED_AWAY: HK_ALARM_AWAY_ARMED,
55  AlarmControlPanelState.ARMED_NIGHT: HK_ALARM_NIGHT_ARMED,
56  AlarmControlPanelState.ARMING: HK_ALARM_AWAY_ARMED,
57  AlarmControlPanelState.DISARMED: HK_ALARM_DISARMED,
58 }
59 
60 HASS_TO_HOMEKIT_SERVICES = {
61  SERVICE_ALARM_ARM_HOME: HK_ALARM_STAY_ARMED,
62  SERVICE_ALARM_ARM_AWAY: HK_ALARM_AWAY_ARMED,
63  SERVICE_ALARM_ARM_NIGHT: HK_ALARM_NIGHT_ARMED,
64  SERVICE_ALARM_DISARM: HK_ALARM_DISARMED,
65 }
66 
67 HK_TO_SERVICE = {
68  HK_ALARM_AWAY_ARMED: SERVICE_ALARM_ARM_AWAY,
69  HK_ALARM_STAY_ARMED: SERVICE_ALARM_ARM_HOME,
70  HK_ALARM_NIGHT_ARMED: SERVICE_ALARM_ARM_NIGHT,
71  HK_ALARM_DISARMED: SERVICE_ALARM_DISARM,
72 }
73 
74 
75 @TYPES.register("SecuritySystem")
77  """Generate an SecuritySystem accessory for an alarm control panel."""
78 
79  def __init__(self, *args: Any) -> None:
80  """Initialize a SecuritySystem accessory object."""
81  super().__init__(*args, category=CATEGORY_ALARM_SYSTEM)
82  state = self.hasshass.states.get(self.entity_identity_id)
83  assert state
84  self._alarm_code_alarm_code = self.configconfig.get(ATTR_CODE)
85 
86  supported_states = state.attributes.get(
87  ATTR_SUPPORTED_FEATURES,
88  (
89  AlarmControlPanelEntityFeature.ARM_HOME
90  | AlarmControlPanelEntityFeature.ARM_VACATION
91  | AlarmControlPanelEntityFeature.ARM_AWAY
92  | AlarmControlPanelEntityFeature.ARM_NIGHT
93  | AlarmControlPanelEntityFeature.TRIGGER
94  ),
95  )
96 
97  serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM)
98  current_char = serv_alarm.get_characteristic(CHAR_CURRENT_SECURITY_STATE)
99  target_char = serv_alarm.get_characteristic(CHAR_TARGET_SECURITY_STATE)
100  default_current_states = current_char.properties.get("ValidValues")
101  default_target_services = target_char.properties.get("ValidValues")
102 
103  current_supported_states = [HK_ALARM_DISARMED, HK_ALARM_TRIGGERED]
104  target_supported_services = [HK_ALARM_DISARMED]
105 
106  if supported_states & AlarmControlPanelEntityFeature.ARM_HOME:
107  current_supported_states.append(HK_ALARM_STAY_ARMED)
108  target_supported_services.append(HK_ALARM_STAY_ARMED)
109 
110  if supported_states & (
111  AlarmControlPanelEntityFeature.ARM_AWAY
112  | AlarmControlPanelEntityFeature.ARM_VACATION
113  ):
114  current_supported_states.append(HK_ALARM_AWAY_ARMED)
115  target_supported_services.append(HK_ALARM_AWAY_ARMED)
116 
117  if supported_states & AlarmControlPanelEntityFeature.ARM_NIGHT:
118  current_supported_states.append(HK_ALARM_NIGHT_ARMED)
119  target_supported_services.append(HK_ALARM_NIGHT_ARMED)
120 
121  self.char_current_statechar_current_state = serv_alarm.configure_char(
122  CHAR_CURRENT_SECURITY_STATE,
123  value=HASS_TO_HOMEKIT_CURRENT[AlarmControlPanelState.DISARMED],
124  valid_values={
125  key: val
126  for key, val in default_current_states.items()
127  if val in current_supported_states
128  },
129  )
130  self.char_target_statechar_target_state = serv_alarm.configure_char(
131  CHAR_TARGET_SECURITY_STATE,
132  value=HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_DISARM],
133  valid_values={
134  key: val
135  for key, val in default_target_services.items()
136  if val in target_supported_services
137  },
138  setter_callback=self.set_security_stateset_security_state,
139  )
140 
141  # Set the state so it is in sync on initial
142  # GET to avoid an event storm after homekit startup
143  self.async_update_stateasync_update_stateasync_update_state(state)
144 
145  def set_security_state(self, value: int) -> None:
146  """Move security state to value if call came from HomeKit."""
147  _LOGGER.debug("%s: Set security state to %d", self.entity_identity_id, value)
148  service = HK_TO_SERVICE[value]
149  params = {ATTR_ENTITY_ID: self.entity_identity_id}
150  if self._alarm_code_alarm_code:
151  params[ATTR_CODE] = self._alarm_code_alarm_code
152  self.async_call_serviceasync_call_service(ALARM_CONTROL_PANEL_DOMAIN, service, params)
153 
154  @callback
155  def async_update_state(self, new_state: State) -> None:
156  """Update security state after state changed."""
157  hass_state: str | AlarmControlPanelState = new_state.state
158  if hass_state in {"None", STATE_UNKNOWN, STATE_UNAVAILABLE}:
159  # Bail out early for no state, unknown or unavailable
160  return
161  if hass_state is not None:
162  hass_state = AlarmControlPanelState(hass_state)
163  if (
164  hass_state
165  and (current_state := HASS_TO_HOMEKIT_CURRENT.get(hass_state)) is not None
166  ):
167  self.char_current_statechar_current_state.set_value(current_state)
168  _LOGGER.debug(
169  "%s: Updated current state to %s (%d)",
170  self.entity_identity_id,
171  hass_state,
172  current_state,
173  )
174  if (
175  hass_state
176  and (target_state := HASS_TO_HOMEKIT_TARGET.get(hass_state)) is not None
177  ):
178  self.char_target_statechar_target_state.set_value(target_state)
None async_call_service(self, str domain, str service, dict[str, Any]|None service_data, Any|None value=None)
Definition: accessories.py:609
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88