2121from homeassistant .core import HomeAssistant , callback
2222from homeassistant .exceptions import HomeAssistantError
2323from homeassistant .helpers .entity import DeviceInfo
24- from homeassistant .helpers .entity_platform import AddEntitiesCallback
25- from homeassistant .helpers .update_coordinator import CoordinatorEntity
24+ from homeassistant .helpers .entity_platform import (
25+ AddEntitiesCallback ,
26+ async_get_current_platform ,
27+ )
2628from homeassistant .util import as_local
2729from pyadtpulse .alarm_panel import (
2830 ADT_ALARM_ARMING ,
3436)
3537from pyadtpulse .site import ADTPulseSite
3638
37- from .const import ADTPULSE_DATA_ATTRIBUTION , ADTPULSE_DOMAIN
39+ from .base_entity import ADTPulseEntity
40+ from .const import ADTPULSE_DOMAIN
3841from .coordinator import ADTPulseDataUpdateCoordinator
3942from .utils import (
4043 get_alarm_unique_id ,
4144 get_gateway_unique_id ,
4245 migrate_entity_name ,
43- zone_open ,
44- zone_trouble ,
46+ system_can_be_armed ,
4547)
4648
4749LOG = getLogger (__name__ )
6466 ADT_ALARM_UNKNOWN : "mdi:shield-bug" ,
6567}
6668
69+ FORCE_ARM = "force arm"
70+ ARM_ERROR_MESSAGE = (
71+ f"Pulse system cannot be armed due to opened/tripped zone - use { FORCE_ARM } "
72+ )
73+
6774
6875async def async_setup_entry (
6976 hass : HomeAssistant , config : ConfigEntry , async_add_entities : AddEntitiesCallback
@@ -85,19 +92,22 @@ async def async_setup_entry(
8592 alarm_devices = [ADTPulseAlarm (coordinator , site )]
8693
8794 async_add_entities (alarm_devices )
95+ platform = async_get_current_platform ()
96+ platform .async_register_entity_service (
97+ "force_stay" , {}, "async_alarm_arm_force_stay"
98+ )
99+ platform .async_register_entity_service (
100+ "force_away" , {}, "async_alarm_arm_custom_bypass"
101+ )
88102
89103
90- class ADTPulseAlarm (
91- CoordinatorEntity [ADTPulseDataUpdateCoordinator ], alarm .AlarmControlPanelEntity
92- ):
104+ class ADTPulseAlarm (ADTPulseEntity , alarm .AlarmControlPanelEntity ):
93105 """An alarm_control_panel implementation for ADT Pulse."""
94106
95107 def __init__ (self , coordinator : ADTPulseDataUpdateCoordinator , site : ADTPulseSite ):
96108 """Initialize the alarm control panel."""
97109 LOG .debug ("%s: adding alarm control panel for %s" , ADTPULSE_DOMAIN , site .id )
98110 self ._name = f"ADT Alarm Panel - Site { site .id } "
99- self ._site = site
100- self ._alarm = site .alarm_control_panel
101111 self ._assumed_state : str | None = None
102112 super ().__init__ (coordinator , self ._name )
103113
@@ -118,11 +128,6 @@ def state(self) -> str:
118128 def assumed_state (self ) -> bool :
119129 return self ._assumed_state is None
120130
121- @property
122- def attribution (self ) -> str | None :
123- """Return API data attribution."""
124- return ADTPULSE_DATA_ATTRIBUTION
125-
126131 @property
127132 def icon (self ) -> str :
128133 """Return the icon."""
@@ -131,16 +136,8 @@ def icon(self) -> str:
131136 return ALARM_ICON_MAP [self ._alarm .status ]
132137
133138 @property
134- def supported_features (self ) -> AlarmControlPanelEntityFeature | None :
139+ def supported_features (self ) -> AlarmControlPanelEntityFeature :
135140 """Return the list of supported features."""
136- if self .state != STATE_ALARM_DISARMED :
137- return None
138- retval = AlarmControlPanelEntityFeature .ARM_CUSTOM_BYPASS
139- if self ._site .zones_as_dict is None :
140- return retval
141- for zone in self ._site .zones_as_dict .values ():
142- if zone_open (zone ) or zone_trouble (zone ):
143- return retval
144141 return (
145142 AlarmControlPanelEntityFeature .ARM_AWAY
146143 | AlarmControlPanelEntityFeature .ARM_CUSTOM_BYPASS
@@ -166,10 +163,14 @@ async def _perform_alarm_action(
166163 ) -> None :
167164 result = True
168165 LOG .debug ("%s: Setting Alarm to %s" , ADTPULSE_DOMAIN , action )
166+ if action != STATE_ALARM_DISARMED :
167+ await self ._check_if_system_armable (action )
169168 if self .state == action :
170169 LOG .warning ("Attempting to set alarm to same state, ignoring" )
171170 return
172- if action == STATE_ALARM_DISARMED :
171+ if not self ._gateway .is_online :
172+ self ._assumed_state = action
173+ elif action == STATE_ALARM_DISARMED :
173174 self ._assumed_state = STATE_ALARM_DISARMING
174175 else :
175176 self ._assumed_state = STATE_ALARM_ARMING
@@ -188,6 +189,16 @@ async def async_alarm_disarm(self, code: str | None = None) -> None:
188189 self ._site .async_disarm (), STATE_ALARM_DISARMED
189190 )
190191
192+ async def _check_if_system_armable (self , new_state : str ) -> None :
193+ """Checks if we can arm the system, raises exceptions if not."""
194+ if self .state != STATE_ALARM_DISARMED :
195+ raise HomeAssistantError (
196+ f"Cannot set alarm to { new_state } "
197+ f"because currently set to { self .state } "
198+ )
199+ if not new_state == FORCE_ARM and not system_can_be_armed (self ._site ):
200+ raise HomeAssistantError (ARM_ERROR_MESSAGE )
201+
191202 async def async_alarm_arm_home (self , code : str | None = None ) -> None :
192203 """Send arm home command."""
193204 await self ._perform_alarm_action (
@@ -204,17 +215,17 @@ async def async_alarm_arm_away(self, code: str | None = None) -> None:
204215 async def async_alarm_arm_custom_bypass (self , code : str | None = None ) -> None :
205216 """Send force arm command."""
206217 await self ._perform_alarm_action (
207- self ._site .async_arm_away (force_arm = True ), "force arm"
218+ self ._site .async_arm_away (force_arm = True ), FORCE_ARM
208219 )
209220
210- @property
211- def name (self ) -> str | None :
212- """Return the name of the alarm."""
213- return None
221+ async def async_alarm_arm_force_stay (self ) -> None :
222+ """Send force arm stay command.
214223
215- @property
216- def has_entity_name (self ) -> bool :
217- return True
224+ This type of arming isn't implemented in HA, but we put it in anyway for
225+ use as a service call."""
226+ await self ._perform_alarm_action (
227+ self ._site .async_arm_home (force_arm = True ), STATE_ALARM_ARMED_HOME
228+ )
218229
219230 @property
220231 def extra_state_attributes (self ) -> dict :
@@ -244,6 +255,11 @@ def code_format(self) -> None:
244255 """
245256 return None
246257
258+ @property
259+ def available (self ) -> bool :
260+ """Alarm panel is always available even if gateway isn't."""
261+ return True
262+
247263 @callback
248264 def _handle_coordinator_update (self ) -> None :
249265 LOG .debug (
0 commit comments