From f333afe08d83c083f8d504110d8627b89c6c8712 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 11 Oct 2023 05:49:52 -0400 Subject: [PATCH 01/21] move common functions to utils.py --- custom_components/adtpulse/__init__.py | 57 ++-------------- .../adtpulse/alarm_control_panel.py | 6 +- custom_components/adtpulse/binary_sensor.py | 6 +- custom_components/adtpulse/utils.py | 65 +++++++++++++++++++ 4 files changed, 75 insertions(+), 59 deletions(-) create mode 100644 custom_components/adtpulse/utils.py diff --git a/custom_components/adtpulse/__init__.py b/custom_components/adtpulse/__init__.py index 86a70fe..81f305c 100644 --- a/custom_components/adtpulse/__init__.py +++ b/custom_components/adtpulse/__init__.py @@ -18,22 +18,16 @@ CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import entity_registry from homeassistant.helpers.config_entry_flow import FlowResult from homeassistant.helpers.typing import ConfigType -from homeassistant.util import slugify from pyadtpulse import PyADTPulse from pyadtpulse.const import ( ADT_DEFAULT_KEEPALIVE_INTERVAL, ADT_DEFAULT_POLL_INTERVAL, ADT_DEFAULT_RELOGIN_INTERVAL, - STATE_OK, - STATE_ONLINE, ) -from pyadtpulse.site import ADTPulseSite -from pyadtpulse.zones import ADTPulseZoneData from .const import ( ADTPULSE_DOMAIN, @@ -49,52 +43,9 @@ SUPPORTED_PLATFORMS = ["alarm_control_panel", "binary_sensor"] -def get_gateway_unique_id(site: ADTPulseSite) -> str: - """Get unique ID for gateway.""" - return f"adt_pulse_gateway_{site.id}" - - -def get_alarm_unique_id(site: ADTPulseSite) -> str: - """Get unique ID for alarm.""" - return f"adt_pulse_alarm_{site.id}" - - -def zone_open(zone: ADTPulseZoneData) -> bool: - """Determine if a zone is opened.""" - return not zone.state == STATE_OK - - -def zone_trouble(zone: ADTPulseZoneData) -> bool: - """Determine if a zone is in trouble state.""" - return not zone.status == STATE_ONLINE - - -@callback -def migrate_entity_name( - hass: HomeAssistant, site: ADTPulseSite, platform_name: str, entity_uid: str -) -> None: - """Migrate old entity names.""" - registry = entity_registry.async_get(hass) - if registry is None: - return - # this seems backwards - entity_id = registry.async_get_entity_id( - platform_name, - ADTPULSE_DOMAIN, - entity_uid, - ) - if entity_id is not None: - # change has_entity_name to True and set name to None for devices - registry.async_update_entity(entity_id, has_entity_name=True, name=None) - # rename site name to site id for entities which have site name - slugified_site_name = slugify(site.name) - if slugified_site_name in entity_id: - registry.async_update_entity( - entity_id, new_entity_id=entity_id.replace(slugified_site_name, site.id) - ) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup( + hass: HomeAssistant, config: ConfigType # pylint: disable=unused-argument +) -> bool: """Start up the ADT Pulse HA integration. Args: diff --git a/custom_components/adtpulse/alarm_control_panel.py b/custom_components/adtpulse/alarm_control_panel.py index d0c1664..29d77d4 100644 --- a/custom_components/adtpulse/alarm_control_panel.py +++ b/custom_components/adtpulse/alarm_control_panel.py @@ -34,15 +34,15 @@ ) from pyadtpulse.site import ADTPulseSite -from . import ( +from .const import ADTPULSE_DATA_ATTRIBUTION, ADTPULSE_DOMAIN +from .coordinator import ADTPulseDataUpdateCoordinator +from .utils import ( get_alarm_unique_id, get_gateway_unique_id, migrate_entity_name, zone_open, zone_trouble, ) -from .const import ADTPULSE_DATA_ATTRIBUTION, ADTPULSE_DOMAIN -from .coordinator import ADTPulseDataUpdateCoordinator LOG = getLogger(__name__) diff --git a/custom_components/adtpulse/binary_sensor.py b/custom_components/adtpulse/binary_sensor.py index 1d88478..a96f410 100644 --- a/custom_components/adtpulse/binary_sensor.py +++ b/custom_components/adtpulse/binary_sensor.py @@ -25,15 +25,15 @@ from pyadtpulse.site import ADTPulseSite from pyadtpulse.zones import ADTPulseZoneData -from . import ( +from .const import ADTPULSE_DATA_ATTRIBUTION, ADTPULSE_DOMAIN +from .coordinator import ADTPulseDataUpdateCoordinator +from .utils import ( get_alarm_unique_id, get_gateway_unique_id, migrate_entity_name, zone_open, zone_trouble, ) -from .const import ADTPULSE_DATA_ATTRIBUTION, ADTPULSE_DOMAIN -from .coordinator import ADTPulseDataUpdateCoordinator LOG = getLogger(__name__) diff --git a/custom_components/adtpulse/utils.py b/custom_components/adtpulse/utils.py new file mode 100644 index 0000000..513f8e8 --- /dev/null +++ b/custom_components/adtpulse/utils.py @@ -0,0 +1,65 @@ +"""ADT Pulse utility functions.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry +from pyadtpulse.const import STATE_OK, STATE_ONLINE +from pyadtpulse.site import ADTPulseSite +from pyadtpulse.zones import ADTPulseZoneData + +from .const import ADTPULSE_DOMAIN + + +def migrate_entity_name( + hass: HomeAssistant, site: ADTPulseSite, platform_name: str, entity_uid: str +) -> None: + """Migrate old entity names.""" + registry = entity_registry.async_get(hass) + if registry is None: + return + # this seems backwards + entity_id = registry.async_get_entity_id( + platform_name, + ADTPULSE_DOMAIN, + entity_uid, + ) + if entity_id is not None: + # change has_entity_name to True and set name to None for devices + registry.async_update_entity(entity_id, has_entity_name=True, name=None) + # rename site name to site id for entities which have site name + slugified_site_name = slugify(site.name) + if slugified_site_name in entity_id: + registry.async_update_entity( + entity_id, new_entity_id=entity_id.replace(slugified_site_name, site.id) + ) + + +def get_gateway_unique_id(site: ADTPulseSite) -> str: + """Get entity unique id for the gateway.""" + return f"adt_pulse_gateway_{site.id}" + + +def get_alarm_unique_id(site: ADTPulseSite) -> str: + """Get entity unique ID for alarm.""" + return f"adt_pulse_alarm_{site.id}" + + +def zone_open(zone: ADTPulseZoneData) -> bool: + """Determine if a zone is opened.""" + return not zone.state == STATE_OK + + +def zone_trouble(zone: ADTPulseZoneData) -> bool: + """Determine if a zone is in trouble state.""" + return not zone.status == STATE_ONLINE + + +def system_can_be_armed(site: ADTPulseSite) -> bool: + """Determine is the system is able to be armed without being forced.""" + zones = site.zones_as_dict + if zones is None: + return False + for zone in zones: + if zone_open(zone) or zone_trouble(zone): + return False + return True From d738bde5097dcc2ba0dff4791a903506c8985b29 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 11 Oct 2023 14:50:42 -0400 Subject: [PATCH 02/21] add system_can_be_armed check to non-forced arm home/stay --- .../adtpulse/alarm_control_panel.py | 16 +++++++++++----- custom_components/adtpulse/utils.py | 1 + 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/custom_components/adtpulse/alarm_control_panel.py b/custom_components/adtpulse/alarm_control_panel.py index 29d77d4..7bb7943 100644 --- a/custom_components/adtpulse/alarm_control_panel.py +++ b/custom_components/adtpulse/alarm_control_panel.py @@ -40,8 +40,7 @@ get_alarm_unique_id, get_gateway_unique_id, migrate_entity_name, - zone_open, - zone_trouble, + system_can_be_armed, ) LOG = getLogger(__name__) @@ -138,9 +137,8 @@ def supported_features(self) -> AlarmControlPanelEntityFeature | 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 + if not system_can_be_armed(self._site): + return retval return ( AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS @@ -190,12 +188,20 @@ async def async_alarm_disarm(self, code: str | None = None) -> None: async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" + if not system_can_be_armed(self._site): + raise HomeAssistantError( + "Pulse system cannot be armed due to tripped zone" " - use force arm" + ) await self._perform_alarm_action( self._site.async_arm_home(), STATE_ALARM_ARMED_HOME ) async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" + if not system_can_be_armed(self._site): + raise HomeAssistantError( + "Pulse system cannot be armed due to tripped zone" " - use force arm" + ) await self._perform_alarm_action( self._site.async_arm_away(), STATE_ALARM_ARMED_AWAY ) diff --git a/custom_components/adtpulse/utils.py b/custom_components/adtpulse/utils.py index 513f8e8..bd09c13 100644 --- a/custom_components/adtpulse/utils.py +++ b/custom_components/adtpulse/utils.py @@ -3,6 +3,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry +from homeassistant.util import slugify from pyadtpulse.const import STATE_OK, STATE_ONLINE from pyadtpulse.site import ADTPulseSite from pyadtpulse.zones import ADTPulseZoneData From 9ab3043315c0c02217b8f4fdca206d500f1afd16 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 11 Oct 2023 15:45:34 -0400 Subject: [PATCH 03/21] add force_arm and force_stay services --- .../adtpulse/alarm_control_panel.py | 25 ++++++++++++++++--- custom_components/adtpulse/services.yaml | 3 +++ custom_components/adtpulse/strings.json | 10 ++++++++ .../adtpulse/translations/en.json | 10 ++++++++ 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/custom_components/adtpulse/alarm_control_panel.py b/custom_components/adtpulse/alarm_control_panel.py index 7bb7943..5f19a26 100644 --- a/custom_components/adtpulse/alarm_control_panel.py +++ b/custom_components/adtpulse/alarm_control_panel.py @@ -21,7 +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.entity_platform import ( + AddEntitiesCallback, + async_get_current_platform, +) from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import as_local from pyadtpulse.alarm_panel import ( @@ -84,6 +87,13 @@ 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( @@ -190,7 +200,7 @@ async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" if not system_can_be_armed(self._site): raise HomeAssistantError( - "Pulse system cannot be armed due to tripped zone" " - use force arm" + "Pulse system cannot be armed due to tripped zone - use force arm" ) await self._perform_alarm_action( self._site.async_arm_home(), STATE_ALARM_ARMED_HOME @@ -200,7 +210,7 @@ async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" if not system_can_be_armed(self._site): raise HomeAssistantError( - "Pulse system cannot be armed due to tripped zone" " - use force arm" + "Pulse system cannot be armed due to tripped zone - use force arm" ) await self._perform_alarm_action( self._site.async_arm_away(), STATE_ALARM_ARMED_AWAY @@ -213,6 +223,15 @@ async def async_alarm_arm_custom_bypass(self, code: str | None = None) -> None: self._site.async_arm_away(force_arm=True), "force arm" ) + async def async_alarm_arm_force_stay(self) -> None: + """Send force arm stay command. + + 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 name(self) -> str | None: """Return the name of the alarm.""" diff --git a/custom_components/adtpulse/services.yaml b/custom_components/adtpulse/services.yaml index e69de29..930581a 100644 --- a/custom_components/adtpulse/services.yaml +++ b/custom_components/adtpulse/services.yaml @@ -0,0 +1,3 @@ +force_stay: + +force_away: diff --git a/custom_components/adtpulse/strings.json b/custom_components/adtpulse/strings.json index 05aee81..e8912e1 100644 --- a/custom_components/adtpulse/strings.json +++ b/custom_components/adtpulse/strings.json @@ -38,5 +38,15 @@ "min_relogin":"Pulse re-login Interval must be greater than 20 minutes", "max_keepalive":"Pulse keepalive Interval must be less than 15 minutes" } + }, + "services": { + "force_stay": { + "name": "Alarm Force Stay", + "description": "Force arm Pulse in stay/home mode" + }, + "force_away": { + "name": "Alarm Force Stay", + "description": "Force arm Pulse in away mode. This is the same as arming custom bypass" + } } } diff --git a/custom_components/adtpulse/translations/en.json b/custom_components/adtpulse/translations/en.json index 05aee81..e8912e1 100644 --- a/custom_components/adtpulse/translations/en.json +++ b/custom_components/adtpulse/translations/en.json @@ -38,5 +38,15 @@ "min_relogin":"Pulse re-login Interval must be greater than 20 minutes", "max_keepalive":"Pulse keepalive Interval must be less than 15 minutes" } + }, + "services": { + "force_stay": { + "name": "Alarm Force Stay", + "description": "Force arm Pulse in stay/home mode" + }, + "force_away": { + "name": "Alarm Force Stay", + "description": "Force arm Pulse in away mode. This is the same as arming custom bypass" + } } } From e94f051f28ff10621935c8abb9c9fde92747876a Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 11 Oct 2023 16:05:16 -0400 Subject: [PATCH 04/21] add PLATFORM_SCHEMAS --- custom_components/adtpulse/alarm_control_panel.py | 2 ++ custom_components/adtpulse/binary_sensor.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/custom_components/adtpulse/alarm_control_panel.py b/custom_components/adtpulse/alarm_control_panel.py index 5f19a26..1d48cda 100644 --- a/custom_components/adtpulse/alarm_control_panel.py +++ b/custom_components/adtpulse/alarm_control_panel.py @@ -20,6 +20,7 @@ ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.config_validation import config_entry_only_config_schema from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, @@ -48,6 +49,7 @@ LOG = getLogger(__name__) +PLATFORM_SCHEMA = config_entry_only_config_schema ALARM_MAP = { ADT_ALARM_ARMING: STATE_ALARM_ARMING, ADT_ALARM_AWAY: STATE_ALARM_ARMED_AWAY, diff --git a/custom_components/adtpulse/binary_sensor.py b/custom_components/adtpulse/binary_sensor.py index a96f410..3fbed18 100644 --- a/custom_components/adtpulse/binary_sensor.py +++ b/custom_components/adtpulse/binary_sensor.py @@ -17,6 +17,7 @@ ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.config_validation import config_entry_only_config_schema from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -37,6 +38,8 @@ LOG = getLogger(__name__) +PLATFORM_SCHEMA = config_entry_only_config_schema + # please keep these alphabetized to make changes easier ADT_DEVICE_CLASS_TAG_MAP = { "co": BinarySensorDeviceClass.CO, From 9b95ad0caba0104c449d288e0b713317f6bc5f1c Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 11 Oct 2023 16:32:39 -0400 Subject: [PATCH 05/21] another attempt at setting PLATFORM_SCHEMA --- custom_components/adtpulse/alarm_control_panel.py | 3 ++- custom_components/adtpulse/binary_sensor.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/custom_components/adtpulse/alarm_control_panel.py b/custom_components/adtpulse/alarm_control_panel.py index 1d48cda..7a7cdd4 100644 --- a/custom_components/adtpulse/alarm_control_panel.py +++ b/custom_components/adtpulse/alarm_control_panel.py @@ -49,7 +49,8 @@ LOG = getLogger(__name__) -PLATFORM_SCHEMA = config_entry_only_config_schema +PLATFORM_SCHEMA = alarm.PLATFORM_SCHEMA.extend(config_entry_only_config_schema) + ALARM_MAP = { ADT_ALARM_ARMING: STATE_ALARM_ARMING, ADT_ALARM_AWAY: STATE_ALARM_ARMED_AWAY, diff --git a/custom_components/adtpulse/binary_sensor.py b/custom_components/adtpulse/binary_sensor.py index 3fbed18..545ebb2 100644 --- a/custom_components/adtpulse/binary_sensor.py +++ b/custom_components/adtpulse/binary_sensor.py @@ -12,6 +12,7 @@ from typing import Any, Mapping from homeassistant.components.binary_sensor import ( + PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) @@ -38,7 +39,7 @@ LOG = getLogger(__name__) -PLATFORM_SCHEMA = config_entry_only_config_schema +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(config_entry_only_config_schema) # please keep these alphabetized to make changes easier ADT_DEVICE_CLASS_TAG_MAP = { From fbb370206880ff7880c348ce3c465c3d8ef1a39b Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 11 Oct 2023 16:53:03 -0400 Subject: [PATCH 06/21] yet another attempt at CONFIG_SCHEMA --- custom_components/adtpulse/__init__.py | 3 +++ custom_components/adtpulse/alarm_control_panel.py | 3 --- custom_components/adtpulse/binary_sensor.py | 4 ---- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/custom_components/adtpulse/__init__.py b/custom_components/adtpulse/__init__.py index 81f305c..aa8ffa2 100644 --- a/custom_components/adtpulse/__init__.py +++ b/custom_components/adtpulse/__init__.py @@ -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 ( @@ -42,6 +43,8 @@ SUPPORTED_PLATFORMS = ["alarm_control_panel", "binary_sensor"] +CONFIG_SCHEMA = config_entry_only_config_schema + async def async_setup( hass: HomeAssistant, config: ConfigType # pylint: disable=unused-argument diff --git a/custom_components/adtpulse/alarm_control_panel.py b/custom_components/adtpulse/alarm_control_panel.py index 7a7cdd4..5f19a26 100644 --- a/custom_components/adtpulse/alarm_control_panel.py +++ b/custom_components/adtpulse/alarm_control_panel.py @@ -20,7 +20,6 @@ ) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.config_validation import config_entry_only_config_schema from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, @@ -49,8 +48,6 @@ LOG = getLogger(__name__) -PLATFORM_SCHEMA = alarm.PLATFORM_SCHEMA.extend(config_entry_only_config_schema) - ALARM_MAP = { ADT_ALARM_ARMING: STATE_ALARM_ARMING, ADT_ALARM_AWAY: STATE_ALARM_ARMED_AWAY, diff --git a/custom_components/adtpulse/binary_sensor.py b/custom_components/adtpulse/binary_sensor.py index 545ebb2..a96f410 100644 --- a/custom_components/adtpulse/binary_sensor.py +++ b/custom_components/adtpulse/binary_sensor.py @@ -12,13 +12,11 @@ from typing import Any, Mapping from homeassistant.components.binary_sensor import ( - PLATFORM_SCHEMA, BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.config_validation import config_entry_only_config_schema from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -39,8 +37,6 @@ LOG = getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(config_entry_only_config_schema) - # please keep these alphabetized to make changes easier ADT_DEVICE_CLASS_TAG_MAP = { "co": BinarySensorDeviceClass.CO, From f7925a95b33e73bcde3bd0380c1eeb1afc0061cc Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 11 Oct 2023 17:47:18 -0400 Subject: [PATCH 07/21] use ADTPulseEntity to set base attributes for entity classes --- .../adtpulse/alarm_control_panel.py | 24 ++--------- custom_components/adtpulse/base_entity.py | 43 ++++++++++++++++--- custom_components/adtpulse/binary_sensor.py | 38 +++------------- 3 files changed, 44 insertions(+), 61 deletions(-) diff --git a/custom_components/adtpulse/alarm_control_panel.py b/custom_components/adtpulse/alarm_control_panel.py index 5f19a26..607fefd 100644 --- a/custom_components/adtpulse/alarm_control_panel.py +++ b/custom_components/adtpulse/alarm_control_panel.py @@ -25,7 +25,6 @@ AddEntitiesCallback, async_get_current_platform, ) -from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import as_local from pyadtpulse.alarm_panel import ( ADT_ALARM_ARMING, @@ -37,7 +36,8 @@ ) 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, @@ -96,17 +96,13 @@ async def async_setup_entry( ) -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) @@ -127,11 +123,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.""" @@ -232,15 +223,6 @@ async def async_alarm_arm_force_stay(self) -> None: self._site.async_arm_home(force_arm=True), STATE_ALARM_ARMED_HOME ) - @property - def name(self) -> str | None: - """Return the name of the alarm.""" - return None - - @property - def has_entity_name(self) -> bool: - return True - @property def extra_state_attributes(self) -> dict: """Return the state attributes.""" diff --git a/custom_components/adtpulse/base_entity.py b/custom_components/adtpulse/base_entity.py index 59d3e25..d741d4c 100644 --- a/custom_components/adtpulse/base_entity.py +++ b/custom_components/adtpulse/base_entity.py @@ -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.""" @@ -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: @@ -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 is_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() diff --git a/custom_components/adtpulse/binary_sensor.py b/custom_components/adtpulse/binary_sensor.py index a96f410..67adb9c 100644 --- a/custom_components/adtpulse/binary_sensor.py +++ b/custom_components/adtpulse/binary_sensor.py @@ -20,12 +20,12 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import as_local from pyadtpulse.site import ADTPulseSite from pyadtpulse.zones import ADTPulseZoneData -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, @@ -103,9 +103,7 @@ async def async_setup_entry( async_add_entities(entities) -class ADTPulseZoneSensor( - CoordinatorEntity[ADTPulseDataUpdateCoordinator], BinarySensorEntity -): +class ADTPulseZoneSensor(ADTPulseEntity, BinarySensorEntity): """HASS zone binary sensor implementation for ADT Pulse.""" # zone = {'id': 'sensor-12', 'name': 'South Office Motion', @@ -167,7 +165,6 @@ def __init__( ) else: LOG.debug("%s: adding zone sensor for site %s", ADTPULSE_DOMAIN, site.id) - self._site = site self._zone_id = zone_id self._is_trouble_indicator = trouble_indicator self._my_zone = self._get_my_zone(site, zone_id) @@ -187,10 +184,6 @@ def name(self) -> str | None: return "Trouble" return None - @property - def has_entity_name(self) -> bool: - return True - @property def unique_id(self) -> str: """Return HA unique id.""" @@ -255,11 +248,6 @@ def device_info(self) -> DeviceInfo: manufacturer="ADT", ) - @property - def attribution(self) -> str: - """Return API data attribution.""" - return ADTPULSE_DATA_ATTRIBUTION - @callback def _handle_coordinator_update(self) -> None: LOG.debug( @@ -271,9 +259,7 @@ def _handle_coordinator_update(self) -> None: self.async_write_ha_state() -class ADTPulseGatewaySensor( - CoordinatorEntity[ADTPulseDataUpdateCoordinator], BinarySensorEntity -): +class ADTPulseGatewaySensor(ADTPulseEntity, BinarySensorEntity): """HASS Gateway Online Binary Sensor.""" def __init__(self, coordinator: ADTPulseDataUpdateCoordinator, site: ADTPulseSite): @@ -288,9 +274,8 @@ def __init__(self, coordinator: ADTPulseDataUpdateCoordinator, site: ADTPulseSit "%s: adding gateway status sensor for site %s", ADTPULSE_DOMAIN, site.name ) self._gateway = site.gateway - self._site = site self._device_class = BinarySensorDeviceClass.CONNECTIVITY - self._name = f"ADT Pulse Gateway Status - Site: {self._site.name}" + self._name = f"ADT Pulse Gateway Status - Site: {site.name}" super().__init__(coordinator, self._name) @property @@ -298,14 +283,6 @@ def is_on(self) -> bool: """Return if gateway is online.""" return self._gateway.is_online - @property - def name(self) -> str | None: - return None - - @property - def has_entity_name(self) -> bool: - return True - # FIXME: Gateways only support one site? @property def unique_id(self) -> str: @@ -318,11 +295,6 @@ def icon(self) -> str: return "mdi:lan-connect" return "mdi:lan-disconnect" - @property - def attribution(self) -> str | None: - """Return API data attribution.""" - return ADTPULSE_DATA_ATTRIBUTION - @property def extra_state_attributes(self) -> Mapping[str, Any]: """Return the device state attributes.""" From 5e7a15c263956232318b30158f50baa8817eace3 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Wed, 11 Oct 2023 17:47:51 -0400 Subject: [PATCH 08/21] remove redundant gateway attribute from gateway binary sensor --- custom_components/adtpulse/binary_sensor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/custom_components/adtpulse/binary_sensor.py b/custom_components/adtpulse/binary_sensor.py index 67adb9c..7f26837 100644 --- a/custom_components/adtpulse/binary_sensor.py +++ b/custom_components/adtpulse/binary_sensor.py @@ -273,7 +273,6 @@ def __init__(self, coordinator: ADTPulseDataUpdateCoordinator, site: ADTPulseSit LOG.debug( "%s: adding gateway status sensor for site %s", ADTPULSE_DOMAIN, site.name ) - self._gateway = site.gateway self._device_class = BinarySensorDeviceClass.CONNECTIVITY self._name = f"ADT Pulse Gateway Status - Site: {site.name}" super().__init__(coordinator, self._name) From 85032e63f14c1b2c2c503c4d6a1e173255339f10 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Thu, 12 Oct 2023 04:40:07 -0400 Subject: [PATCH 09/21] make alarm panel always available/fix property name in base entity --- custom_components/adtpulse/alarm_control_panel.py | 5 +++++ custom_components/adtpulse/base_entity.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/custom_components/adtpulse/alarm_control_panel.py b/custom_components/adtpulse/alarm_control_panel.py index 607fefd..0c239f9 100644 --- a/custom_components/adtpulse/alarm_control_panel.py +++ b/custom_components/adtpulse/alarm_control_panel.py @@ -251,6 +251,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( diff --git a/custom_components/adtpulse/base_entity.py b/custom_components/adtpulse/base_entity.py index d741d4c..64b16ab 100644 --- a/custom_components/adtpulse/base_entity.py +++ b/custom_components/adtpulse/base_entity.py @@ -60,7 +60,7 @@ def extra_state_attributes(self) -> Mapping[str, Any] | None: return self._attrs @property - def is_available(self) -> bool: + def available(self) -> bool: """Returns whether an entity is available. Generally false if gateway is offline.""" From 4ad752f6595b79150dfb03ac25f79ee697825088 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Thu, 12 Oct 2023 05:02:33 -0400 Subject: [PATCH 10/21] don't disable alarm states based upon open zones --- custom_components/adtpulse/alarm_control_panel.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/custom_components/adtpulse/alarm_control_panel.py b/custom_components/adtpulse/alarm_control_panel.py index 0c239f9..4a1b402 100644 --- a/custom_components/adtpulse/alarm_control_panel.py +++ b/custom_components/adtpulse/alarm_control_panel.py @@ -131,15 +131,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 - if not system_can_be_armed(self._site): - return retval return ( AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS From 87eba18fa06a7db2d1e475600443e4e965f7924e Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Thu, 12 Oct 2023 05:09:53 -0400 Subject: [PATCH 11/21] bump pyadtpulse/hacs version --- custom_components/adtpulse/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/adtpulse/manifest.json b/custom_components/adtpulse/manifest.json index 516e06a..b92e4c3 100644 --- a/custom_components/adtpulse/manifest.json +++ b/custom_components/adtpulse/manifest.json @@ -8,7 +8,7 @@ "iot_class": "cloud_push", "issue_tracker": "https://github.com/rlippmann/hass-adtpulse/issues", "requirements": [ - "pyadtpulse>=1.1.2" + "pyadtpulse>=1.1.3" ], - "version": "0.3.2" + "version": "0.3.3" } From a16b48c41e7f01b6299692b46954f2870e5813cf Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Thu, 12 Oct 2023 05:18:41 -0400 Subject: [PATCH 12/21] revert CONFIG_SCHEMA for now --- custom_components/adtpulse/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adtpulse/__init__.py b/custom_components/adtpulse/__init__.py index aa8ffa2..b422dbb 100644 --- a/custom_components/adtpulse/__init__.py +++ b/custom_components/adtpulse/__init__.py @@ -43,7 +43,7 @@ SUPPORTED_PLATFORMS = ["alarm_control_panel", "binary_sensor"] -CONFIG_SCHEMA = config_entry_only_config_schema +# CONFIG_SCHEMA = config_entry_only_config_schema async def async_setup( From df71fe73991203ccbe06c2d4afd79213f8c35b8f Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Thu, 12 Oct 2023 05:51:06 -0400 Subject: [PATCH 13/21] fix alarm services --- custom_components/adtpulse/services.yaml | 6 ++++++ custom_components/adtpulse/strings.json | 2 +- custom_components/adtpulse/translations/en.json | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/custom_components/adtpulse/services.yaml b/custom_components/adtpulse/services.yaml index 930581a..ec48cfa 100644 --- a/custom_components/adtpulse/services.yaml +++ b/custom_components/adtpulse/services.yaml @@ -1,3 +1,9 @@ force_stay: + target: + entity: + domain: alarm_control_panel force_away: + target: + entity: + domain: alarm_control_panel diff --git a/custom_components/adtpulse/strings.json b/custom_components/adtpulse/strings.json index e8912e1..b53963f 100644 --- a/custom_components/adtpulse/strings.json +++ b/custom_components/adtpulse/strings.json @@ -45,7 +45,7 @@ "description": "Force arm Pulse in stay/home mode" }, "force_away": { - "name": "Alarm Force Stay", + "name": "Alarm Force Away", "description": "Force arm Pulse in away mode. This is the same as arming custom bypass" } } diff --git a/custom_components/adtpulse/translations/en.json b/custom_components/adtpulse/translations/en.json index e8912e1..b53963f 100644 --- a/custom_components/adtpulse/translations/en.json +++ b/custom_components/adtpulse/translations/en.json @@ -45,7 +45,7 @@ "description": "Force arm Pulse in stay/home mode" }, "force_away": { - "name": "Alarm Force Stay", + "name": "Alarm Force Away", "description": "Force arm Pulse in away mode. This is the same as arming custom bypass" } } From ecd7d4fea9699cbe35828e3819f4337ee886e224 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Thu, 12 Oct 2023 05:51:29 -0400 Subject: [PATCH 14/21] util fix --- custom_components/adtpulse/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adtpulse/utils.py b/custom_components/adtpulse/utils.py index bd09c13..f5bc15a 100644 --- a/custom_components/adtpulse/utils.py +++ b/custom_components/adtpulse/utils.py @@ -60,7 +60,7 @@ def system_can_be_armed(site: ADTPulseSite) -> bool: zones = site.zones_as_dict if zones is None: return False - for zone in zones: + for zone in zones.values(): if zone_open(zone) or zone_trouble(zone): return False return True From ab25eb749356eeb20249c181b1fe21cb820aa2a3 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Thu, 12 Oct 2023 05:58:57 -0400 Subject: [PATCH 15/21] linting --- custom_components/adtpulse/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/adtpulse/utils.py b/custom_components/adtpulse/utils.py index f5bc15a..86f8eef 100644 --- a/custom_components/adtpulse/utils.py +++ b/custom_components/adtpulse/utils.py @@ -2,7 +2,7 @@ from __future__ import annotations from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry +from homeassistant.helpers import entity_registry as er from homeassistant.util import slugify from pyadtpulse.const import STATE_OK, STATE_ONLINE from pyadtpulse.site import ADTPulseSite @@ -15,7 +15,7 @@ def migrate_entity_name( hass: HomeAssistant, site: ADTPulseSite, platform_name: str, entity_uid: str ) -> None: """Migrate old entity names.""" - registry = entity_registry.async_get(hass) + registry = er.async_get(hass) if registry is None: return # this seems backwards @@ -47,12 +47,12 @@ def get_alarm_unique_id(site: ADTPulseSite) -> str: def zone_open(zone: ADTPulseZoneData) -> bool: """Determine if a zone is opened.""" - return not zone.state == STATE_OK + return zone.state != STATE_OK def zone_trouble(zone: ADTPulseZoneData) -> bool: """Determine if a zone is in trouble state.""" - return not zone.status == STATE_ONLINE + return zone.status != STATE_ONLINE def system_can_be_armed(site: ADTPulseSite) -> bool: From 75df9d9278d897f888fab2fd3077000deed7fbcc Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Thu, 12 Oct 2023 06:21:37 -0400 Subject: [PATCH 16/21] CONFIG_SCHEMA yet again --- custom_components/adtpulse/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/adtpulse/__init__.py b/custom_components/adtpulse/__init__.py index b422dbb..3a36c09 100644 --- a/custom_components/adtpulse/__init__.py +++ b/custom_components/adtpulse/__init__.py @@ -43,7 +43,7 @@ SUPPORTED_PLATFORMS = ["alarm_control_panel", "binary_sensor"] -# CONFIG_SCHEMA = config_entry_only_config_schema +CONFIG_SCHEMA = config_entry_only_config_schema(ADTPULSE_DOMAIN) async def async_setup( From 895917e66d16d443ad1f1850589a4532302c9791 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Thu, 12 Oct 2023 07:19:15 -0400 Subject: [PATCH 17/21] add relogin service --- custom_components/adtpulse/__init__.py | 7 ++++++- custom_components/adtpulse/services.yaml | 4 ++++ custom_components/adtpulse/strings.json | 4 ++++ custom_components/adtpulse/translations/en.json | 4 ++++ 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/custom_components/adtpulse/__init__.py b/custom_components/adtpulse/__init__.py index 3a36c09..417f78e 100644 --- a/custom_components/adtpulse/__init__.py +++ b/custom_components/adtpulse/__init__.py @@ -18,7 +18,7 @@ CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceRegistry 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 @@ -136,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 diff --git a/custom_components/adtpulse/services.yaml b/custom_components/adtpulse/services.yaml index ec48cfa..912ace7 100644 --- a/custom_components/adtpulse/services.yaml +++ b/custom_components/adtpulse/services.yaml @@ -7,3 +7,7 @@ force_away: target: entity: domain: alarm_control_panel + +quick_relogin: + name: "Relogin to Pulse" + description: "Performs a re-login to Pulse" diff --git a/custom_components/adtpulse/strings.json b/custom_components/adtpulse/strings.json index b53963f..102930f 100644 --- a/custom_components/adtpulse/strings.json +++ b/custom_components/adtpulse/strings.json @@ -47,6 +47,10 @@ "force_away": { "name": "Alarm Force Away", "description": "Force arm Pulse in away mode. This is the same as arming custom bypass" + }, + "quick_relogin": { + "name": "Relogin to Pulse", + "description": "Performs a re-login to Pulse" } } } diff --git a/custom_components/adtpulse/translations/en.json b/custom_components/adtpulse/translations/en.json index b53963f..102930f 100644 --- a/custom_components/adtpulse/translations/en.json +++ b/custom_components/adtpulse/translations/en.json @@ -47,6 +47,10 @@ "force_away": { "name": "Alarm Force Away", "description": "Force arm Pulse in away mode. This is the same as arming custom bypass" + }, + "quick_relogin": { + "name": "Relogin to Pulse", + "description": "Performs a re-login to Pulse" } } } From 8a0abb1d8d138d03485b00f10eb309ae2f1a333b Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Thu, 12 Oct 2023 08:24:13 -0400 Subject: [PATCH 18/21] code quality improvements --- custom_components/adtpulse/__init__.py | 2 +- custom_components/adtpulse/alarm_control_panel.py | 15 ++++++++------- custom_components/adtpulse/binary_sensor.py | 8 ++++---- custom_components/adtpulse/utils.py | 6 +++--- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/custom_components/adtpulse/__init__.py b/custom_components/adtpulse/__init__.py index 417f78e..ade21c2 100644 --- a/custom_components/adtpulse/__init__.py +++ b/custom_components/adtpulse/__init__.py @@ -18,7 +18,7 @@ CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import HomeAssistant, ServiceRegistry +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 diff --git a/custom_components/adtpulse/alarm_control_panel.py b/custom_components/adtpulse/alarm_control_panel.py index 4a1b402..2a74b86 100644 --- a/custom_components/adtpulse/alarm_control_panel.py +++ b/custom_components/adtpulse/alarm_control_panel.py @@ -66,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 @@ -183,9 +188,7 @@ async def async_alarm_disarm(self, code: str | None = None) -> None: async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" if not system_can_be_armed(self._site): - raise HomeAssistantError( - "Pulse system cannot be armed due to tripped zone - use force arm" - ) + raise HomeAssistantError(ARM_ERROR_MESSAGE) await self._perform_alarm_action( self._site.async_arm_home(), STATE_ALARM_ARMED_HOME ) @@ -193,9 +196,7 @@ async def async_alarm_arm_home(self, code: str | None = None) -> None: async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" if not system_can_be_armed(self._site): - raise HomeAssistantError( - "Pulse system cannot be armed due to tripped zone - use force arm" - ) + raise HomeAssistantError(ARM_ERROR_MESSAGE) await self._perform_alarm_action( self._site.async_arm_away(), STATE_ALARM_ARMED_AWAY ) @@ -204,7 +205,7 @@ 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 ) async def async_alarm_arm_force_stay(self) -> None: diff --git a/custom_components/adtpulse/binary_sensor.py b/custom_components/adtpulse/binary_sensor.py index 7f26837..8c7444f 100644 --- a/custom_components/adtpulse/binary_sensor.py +++ b/custom_components/adtpulse/binary_sensor.py @@ -31,8 +31,8 @@ get_alarm_unique_id, get_gateway_unique_id, migrate_entity_name, - zone_open, - zone_trouble, + zone_is_in_trouble, + zone_is_open, ) LOG = getLogger(__name__) @@ -212,8 +212,8 @@ def is_on(self) -> bool: """Return True if the binary sensor is on.""" # sensor is considered tripped if the state is anything but OK if self._is_trouble_indicator: - return zone_trouble(self._my_zone) - return zone_open(self._my_zone) + return zone_is_in_trouble(self._my_zone) + return zone_is_open(self._my_zone) @property def device_class(self) -> BinarySensorDeviceClass: diff --git a/custom_components/adtpulse/utils.py b/custom_components/adtpulse/utils.py index 86f8eef..6530bda 100644 --- a/custom_components/adtpulse/utils.py +++ b/custom_components/adtpulse/utils.py @@ -45,12 +45,12 @@ def get_alarm_unique_id(site: ADTPulseSite) -> str: return f"adt_pulse_alarm_{site.id}" -def zone_open(zone: ADTPulseZoneData) -> bool: +def zone_is_open(zone: ADTPulseZoneData) -> bool: """Determine if a zone is opened.""" return zone.state != STATE_OK -def zone_trouble(zone: ADTPulseZoneData) -> bool: +def zone_is_in_trouble(zone: ADTPulseZoneData) -> bool: """Determine if a zone is in trouble state.""" return zone.status != STATE_ONLINE @@ -61,6 +61,6 @@ def system_can_be_armed(site: ADTPulseSite) -> bool: if zones is None: return False for zone in zones.values(): - if zone_open(zone) or zone_trouble(zone): + if zone_is_open(zone) or zone_is_in_trouble(zone): return False return True From ee93e4f1993c6f0e36edee7de0f7fdf1e3a819fc Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Thu, 12 Oct 2023 08:59:11 -0400 Subject: [PATCH 19/21] change arm/disarmed assumed state if gateway offline --- custom_components/adtpulse/alarm_control_panel.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_components/adtpulse/alarm_control_panel.py b/custom_components/adtpulse/alarm_control_panel.py index 2a74b86..12df88d 100644 --- a/custom_components/adtpulse/alarm_control_panel.py +++ b/custom_components/adtpulse/alarm_control_panel.py @@ -166,7 +166,9 @@ async def _perform_alarm_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 From d8a1269c9eac8928bed8f20f8d4e2b8207ce696f Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Thu, 12 Oct 2023 09:26:48 -0400 Subject: [PATCH 20/21] use _check_if_system_armable before alarm arm calls --- .../adtpulse/alarm_control_panel.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/custom_components/adtpulse/alarm_control_panel.py b/custom_components/adtpulse/alarm_control_panel.py index 12df88d..ef5e815 100644 --- a/custom_components/adtpulse/alarm_control_panel.py +++ b/custom_components/adtpulse/alarm_control_panel.py @@ -163,6 +163,8 @@ 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 @@ -187,18 +189,24 @@ 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.""" - if not system_can_be_armed(self._site): - raise HomeAssistantError(ARM_ERROR_MESSAGE) await self._perform_alarm_action( self._site.async_arm_home(), STATE_ALARM_ARMED_HOME ) async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" - if not system_can_be_armed(self._site): - raise HomeAssistantError(ARM_ERROR_MESSAGE) await self._perform_alarm_action( self._site.async_arm_away(), STATE_ALARM_ARMED_AWAY ) From a7eb94f30d1d12f2b29ff1d45b4e1a3ece8bc537 Mon Sep 17 00:00:00 2001 From: Robert Lippmann Date: Thu, 12 Oct 2023 09:35:43 -0400 Subject: [PATCH 21/21] update changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a47c036..8bcddf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: