Skip to content
This repository was archived by the owner on Feb 2, 2024. It is now read-only.

Commit 636e4d4

Browse files
authored
Merge pull request #4 from rlippmann/0.3.3
0.3.3
2 parents f333afe + a7eb94f commit 636e4d4

File tree

10 files changed

+163
-88
lines changed

10 files changed

+163
-88
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
## 0.3.3 (2023-10-12)
2+
3+
* bump pyadtpulse to 1.1.3. This should fix alarm not updating issue
4+
* add force stay and force away services
5+
* add relogin service
6+
* refactor code to use base entity. This should cause most entities to become unavailable if the gateway goes offline
7+
* disallow invalid alarm state changes
8+
* revert alarm card functionality. All states will be available, but exceptions will be thrown if an invalid state is requested.
9+
110
## 0.3.2 (2023-10-08)
211

312
Alarm control panel updates:

custom_components/adtpulse/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from homeassistant.core import HomeAssistant
2222
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
2323
from homeassistant.helpers.config_entry_flow import FlowResult
24+
from homeassistant.helpers.config_validation import config_entry_only_config_schema
2425
from homeassistant.helpers.typing import ConfigType
2526
from pyadtpulse import PyADTPulse
2627
from pyadtpulse.const import (
@@ -42,6 +43,8 @@
4243

4344
SUPPORTED_PLATFORMS = ["alarm_control_panel", "binary_sensor"]
4445

46+
CONFIG_SCHEMA = config_entry_only_config_schema(ADTPULSE_DOMAIN)
47+
4548

4649
async def async_setup(
4750
hass: HomeAssistant, config: ConfigType # pylint: disable=unused-argument
@@ -133,6 +136,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
133136
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.stop)
134137
)
135138
entry.async_on_unload(entry.add_update_listener(options_listener))
139+
140+
async def handle_relogin(dummy: str) -> None: # pylint: disable=unused-argument
141+
await service.async_quick_relogin()
142+
143+
hass.services.async_register(ADTPULSE_DOMAIN, "quick_relogin", handle_relogin)
136144
return True
137145

138146

custom_components/adtpulse/alarm_control_panel.py

Lines changed: 49 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
from homeassistant.core import HomeAssistant, callback
2222
from homeassistant.exceptions import HomeAssistantError
2323
from 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+
)
2628
from homeassistant.util import as_local
2729
from pyadtpulse.alarm_panel import (
2830
ADT_ALARM_ARMING,
@@ -34,14 +36,14 @@
3436
)
3537
from 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
3841
from .coordinator import ADTPulseDataUpdateCoordinator
3942
from .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

4749
LOG = getLogger(__name__)
@@ -64,6 +66,11 @@
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

6875
async 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(

custom_components/adtpulse/base_entity.py

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
"""ADT Pulse Entity Base class."""
22
from __future__ import annotations
33

4+
from logging import getLogger
5+
from typing import Any, Mapping
6+
47
from homeassistant.core import callback
58
from homeassistant.helpers.update_coordinator import CoordinatorEntity
69

7-
from .const import LOG
10+
from .const import ADTPULSE_DATA_ATTRIBUTION
811
from .coordinator import ADTPulseDataUpdateCoordinator
912

13+
LOG = getLogger(__name__)
14+
1015

1116
class ADTPulseEntity(CoordinatorEntity[ADTPulseDataUpdateCoordinator]):
1217
"""Base Entity class for ADT Pulse devices."""
@@ -19,14 +24,26 @@ def __init__(self, coordinator: ADTPulseDataUpdateCoordinator, name: str):
1924
name (str): entity name
2025
"""
2126
self._name = name
22-
27+
# save references to commonly used objects
28+
self._pulse_connection = coordinator.adtpulse
29+
self._site = self._pulse_connection.site
30+
self._gateway = self._site.gateway
31+
self._alarm = self._site.alarm_control_panel
2332
self._attrs: dict = {}
2433
super().__init__(coordinator)
2534

35+
# Base level properties that can be overridden by subclasses
36+
@property
37+
def name(self) -> str | None:
38+
"""Return the display name for this sensor.
39+
40+
Should generally be none since using has_entity_name."""
41+
return None
42+
2643
@property
27-
def name(self) -> str:
28-
"""Return the display name for this sensor."""
29-
return self._name
44+
def has_entity_name(self) -> bool:
45+
"""Returns has_entity_name. Should generally be true."""
46+
return True
3047

3148
@property
3249
def icon(self) -> str:
@@ -38,13 +55,25 @@ def icon(self) -> str:
3855
return "mdi:gauge"
3956

4057
@property
41-
def extra_state_attributes(self) -> dict:
58+
def extra_state_attributes(self) -> Mapping[str, Any] | None:
4259
"""Return the device state attributes."""
4360
return self._attrs
4461

62+
@property
63+
def available(self) -> bool:
64+
"""Returns whether an entity is available.
65+
66+
Generally false if gateway is offline."""
67+
return self._gateway.is_online
68+
69+
@property
70+
def attribution(self) -> str:
71+
"""Return API data attribution."""
72+
return ADTPULSE_DATA_ATTRIBUTION
73+
4574
@callback
4675
def _handle_coordinator_update(self) -> None:
4776
"""Call update method."""
48-
LOG.debug(f"Scheduling update ADT Pulse entity {self._name}")
77+
LOG.debug("Scheduling update ADT Pulse entity %s", self._name)
4978
# inform HASS that ADT Pulse data for this entity has been updated
5079
self.async_write_ha_state()

0 commit comments

Comments
 (0)