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

Commit

Permalink
Merge pull request #4 from rlippmann/0.3.3
Browse files Browse the repository at this point in the history
0.3.3
  • Loading branch information
rlippmann authored Oct 12, 2023
2 parents f333afe + a7eb94f commit 636e4d4
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 88 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## 0.3.3 (2023-10-12)

* bump pyadtpulse to 1.1.3. This should fix alarm not updating issue
* add force stay and force away services
* add relogin service
* refactor code to use base entity. This should cause most entities to become unavailable if the gateway goes offline
* disallow invalid alarm state changes
* revert alarm card functionality. All states will be available, but exceptions will be thrown if an invalid state is requested.

## 0.3.2 (2023-10-08)

Alarm control panel updates:
Expand Down
8 changes: 8 additions & 0 deletions custom_components/adtpulse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.config_entry_flow import FlowResult
from homeassistant.helpers.config_validation import config_entry_only_config_schema
from homeassistant.helpers.typing import ConfigType
from pyadtpulse import PyADTPulse
from pyadtpulse.const import (
Expand All @@ -42,6 +43,8 @@

SUPPORTED_PLATFORMS = ["alarm_control_panel", "binary_sensor"]

CONFIG_SCHEMA = config_entry_only_config_schema(ADTPULSE_DOMAIN)


async def async_setup(
hass: HomeAssistant, config: ConfigType # pylint: disable=unused-argument
Expand Down Expand Up @@ -133,6 +136,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.stop)
)
entry.async_on_unload(entry.add_update_listener(options_listener))

async def handle_relogin(dummy: str) -> None: # pylint: disable=unused-argument
await service.async_quick_relogin()

hass.services.async_register(ADTPULSE_DOMAIN, "quick_relogin", handle_relogin)
return True


Expand Down
82 changes: 49 additions & 33 deletions custom_components/adtpulse/alarm_control_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.entity_platform import (
AddEntitiesCallback,
async_get_current_platform,
)
from homeassistant.util import as_local
from pyadtpulse.alarm_panel import (
ADT_ALARM_ARMING,
Expand All @@ -34,14 +36,14 @@
)
from pyadtpulse.site import ADTPulseSite

from .const import ADTPULSE_DATA_ATTRIBUTION, ADTPULSE_DOMAIN
from .base_entity import ADTPulseEntity
from .const import ADTPULSE_DOMAIN
from .coordinator import ADTPulseDataUpdateCoordinator
from .utils import (
get_alarm_unique_id,
get_gateway_unique_id,
migrate_entity_name,
zone_open,
zone_trouble,
system_can_be_armed,
)

LOG = getLogger(__name__)
Expand All @@ -64,6 +66,11 @@
ADT_ALARM_UNKNOWN: "mdi:shield-bug",
}

FORCE_ARM = "force arm"
ARM_ERROR_MESSAGE = (
f"Pulse system cannot be armed due to opened/tripped zone - use {FORCE_ARM}"
)


async def async_setup_entry(
hass: HomeAssistant, config: ConfigEntry, async_add_entities: AddEntitiesCallback
Expand All @@ -85,19 +92,22 @@ async def async_setup_entry(
alarm_devices = [ADTPulseAlarm(coordinator, site)]

async_add_entities(alarm_devices)
platform = async_get_current_platform()
platform.async_register_entity_service(
"force_stay", {}, "async_alarm_arm_force_stay"
)
platform.async_register_entity_service(
"force_away", {}, "async_alarm_arm_custom_bypass"
)


class ADTPulseAlarm(
CoordinatorEntity[ADTPulseDataUpdateCoordinator], alarm.AlarmControlPanelEntity
):
class ADTPulseAlarm(ADTPulseEntity, alarm.AlarmControlPanelEntity):
"""An alarm_control_panel implementation for ADT Pulse."""

def __init__(self, coordinator: ADTPulseDataUpdateCoordinator, site: ADTPulseSite):
"""Initialize the alarm control panel."""
LOG.debug("%s: adding alarm control panel for %s", ADTPULSE_DOMAIN, site.id)
self._name = f"ADT Alarm Panel - Site {site.id}"
self._site = site
self._alarm = site.alarm_control_panel
self._assumed_state: str | None = None
super().__init__(coordinator, self._name)

Expand All @@ -118,11 +128,6 @@ def state(self) -> str:
def assumed_state(self) -> bool:
return self._assumed_state is None

@property
def attribution(self) -> str | None:
"""Return API data attribution."""
return ADTPULSE_DATA_ATTRIBUTION

@property
def icon(self) -> str:
"""Return the icon."""
Expand All @@ -131,16 +136,8 @@ def icon(self) -> str:
return ALARM_ICON_MAP[self._alarm.status]

@property
def supported_features(self) -> AlarmControlPanelEntityFeature | None:
def supported_features(self) -> AlarmControlPanelEntityFeature:
"""Return the list of supported features."""
if self.state != STATE_ALARM_DISARMED:
return None
retval = AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS
if self._site.zones_as_dict is None:
return retval
for zone in self._site.zones_as_dict.values():
if zone_open(zone) or zone_trouble(zone):
return retval
return (
AlarmControlPanelEntityFeature.ARM_AWAY
| AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS
Expand All @@ -166,10 +163,14 @@ async def _perform_alarm_action(
) -> None:
result = True
LOG.debug("%s: Setting Alarm to %s", ADTPULSE_DOMAIN, action)
if action != STATE_ALARM_DISARMED:
await self._check_if_system_armable(action)
if self.state == action:
LOG.warning("Attempting to set alarm to same state, ignoring")
return
if action == STATE_ALARM_DISARMED:
if not self._gateway.is_online:
self._assumed_state = action
elif action == STATE_ALARM_DISARMED:
self._assumed_state = STATE_ALARM_DISARMING
else:
self._assumed_state = STATE_ALARM_ARMING
Expand All @@ -188,6 +189,16 @@ async def async_alarm_disarm(self, code: str | None = None) -> None:
self._site.async_disarm(), STATE_ALARM_DISARMED
)

async def _check_if_system_armable(self, new_state: str) -> None:
"""Checks if we can arm the system, raises exceptions if not."""
if self.state != STATE_ALARM_DISARMED:
raise HomeAssistantError(
f"Cannot set alarm to {new_state} "
f"because currently set to {self.state}"
)
if not new_state == FORCE_ARM and not system_can_be_armed(self._site):
raise HomeAssistantError(ARM_ERROR_MESSAGE)

async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
await self._perform_alarm_action(
Expand All @@ -204,17 +215,17 @@ async def async_alarm_arm_away(self, code: str | None = None) -> None:
async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None:
"""Send force arm command."""
await self._perform_alarm_action(
self._site.async_arm_away(force_arm=True), "force arm"
self._site.async_arm_away(force_arm=True), FORCE_ARM
)

@property
def name(self) -> str | None:
"""Return the name of the alarm."""
return None
async def async_alarm_arm_force_stay(self) -> None:
"""Send force arm stay command.
@property
def has_entity_name(self) -> bool:
return True
This type of arming isn't implemented in HA, but we put it in anyway for
use as a service call."""
await self._perform_alarm_action(
self._site.async_arm_home(force_arm=True), STATE_ALARM_ARMED_HOME
)

@property
def extra_state_attributes(self) -> dict:
Expand Down Expand Up @@ -244,6 +255,11 @@ def code_format(self) -> None:
"""
return None

@property
def available(self) -> bool:
"""Alarm panel is always available even if gateway isn't."""
return True

@callback
def _handle_coordinator_update(self) -> None:
LOG.debug(
Expand Down
43 changes: 36 additions & 7 deletions custom_components/adtpulse/base_entity.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
"""ADT Pulse Entity Base class."""
from __future__ import annotations

from logging import getLogger
from typing import Any, Mapping

from homeassistant.core import callback
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import LOG
from .const import ADTPULSE_DATA_ATTRIBUTION
from .coordinator import ADTPulseDataUpdateCoordinator

LOG = getLogger(__name__)


class ADTPulseEntity(CoordinatorEntity[ADTPulseDataUpdateCoordinator]):
"""Base Entity class for ADT Pulse devices."""
Expand All @@ -19,14 +24,26 @@ def __init__(self, coordinator: ADTPulseDataUpdateCoordinator, name: str):
name (str): entity name
"""
self._name = name

# save references to commonly used objects
self._pulse_connection = coordinator.adtpulse
self._site = self._pulse_connection.site
self._gateway = self._site.gateway
self._alarm = self._site.alarm_control_panel
self._attrs: dict = {}
super().__init__(coordinator)

# Base level properties that can be overridden by subclasses
@property
def name(self) -> str | None:
"""Return the display name for this sensor.
Should generally be none since using has_entity_name."""
return None

@property
def name(self) -> str:
"""Return the display name for this sensor."""
return self._name
def has_entity_name(self) -> bool:
"""Returns has_entity_name. Should generally be true."""
return True

@property
def icon(self) -> str:
Expand All @@ -38,13 +55,25 @@ def icon(self) -> str:
return "mdi:gauge"

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

@property
def available(self) -> bool:
"""Returns whether an entity is available.
Generally false if gateway is offline."""
return self._gateway.is_online

@property
def attribution(self) -> str:
"""Return API data attribution."""
return ADTPULSE_DATA_ATTRIBUTION

@callback
def _handle_coordinator_update(self) -> None:
"""Call update method."""
LOG.debug(f"Scheduling update ADT Pulse entity {self._name}")
LOG.debug("Scheduling update ADT Pulse entity %s", self._name)
# inform HASS that ADT Pulse data for this entity has been updated
self.async_write_ha_state()
Loading

0 comments on commit 636e4d4

Please sign in to comment.