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

0.3 #2

Merged
merged 53 commits into from
Oct 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
96701e4
updates for pyadtpulse 1.1
rlippmann Sep 10, 2023
608c433
isort
rlippmann Sep 10, 2023
d5dc71b
add device_info
rlippmann Sep 10, 2023
bb14fc4
general cleanup
rlippmann Sep 10, 2023
43fd554
add extra_state_attributes to gateway
rlippmann Sep 11, 2023
df3801f
add gateway update times
rlippmann Sep 11, 2023
7905615
add reauth flow
rlippmann Sep 20, 2023
da31cf0
add options flow
rlippmann Sep 20, 2023
181955d
add option handling to __init__
rlippmann Sep 20, 2023
2f00308
fix typos/formatting/isort
rlippmann Sep 20, 2023
2355124
update strings.json and en.json for options flow, remove unneed entries
rlippmann Sep 20, 2023
4229546
fix type in en.json
rlippmann Sep 20, 2023
541182a
add via_device
rlippmann Sep 28, 2023
49d7a12
move async_step_import to __init__.py
rlippmann Sep 28, 2023
992bf76
black, isort
rlippmann Sep 28, 2023
c539d13
remove type coercion for intervals in async_setup_entry
rlippmann Sep 28, 2023
d51c683
update strings.json/en.json
rlippmann Sep 28, 2023
7636d90
move get unique ids to init
rlippmann Sep 28, 2023
7b85395
remove typo in en.json and strings.json
rlippmann Sep 28, 2023
4d01327
fix device info for gateway
rlippmann Sep 28, 2023
0ccd3b7
add device/manufacturer names
rlippmann Sep 28, 2023
1a1c409
rework config flow schemas
rlippmann Sep 28, 2023
476c061
fix device syntax errors
rlippmann Sep 28, 2023
7be025e
pre-commit hook changes
rlippmann Sep 28, 2023
3f3f5ce
update pre-commit hook versions
rlippmann Sep 28, 2023
94a3843
use as_local for timestamps
rlippmann Oct 1, 2023
713be12
pylint fixes
rlippmann Oct 1, 2023
ba3c196
rework options and config flow yet again
rlippmann Oct 1, 2023
026482e
rework config/options flow yet again
rlippmann Oct 2, 2023
684b1c0
don't check site in validate_input if login result is false
rlippmann Oct 2, 2023
466c8a0
populate reauth form with original data
rlippmann Oct 2, 2023
2fa0c86
fix options flow error detection
rlippmann Oct 2, 2023
45491bb
change ipv4 attributes to strings
rlippmann Oct 2, 2023
ae2a6a3
change alarm panel name to use site id instead of name
rlippmann Oct 2, 2023
38e0f7b
version bump
rlippmann Oct 2, 2023
8a71d19
update readme
rlippmann Oct 2, 2023
ae3d2dd
add more debug logging to gateway update
rlippmann Oct 2, 2023
03b8fb5
change gateway name to use site.id rather than name
rlippmann Oct 2, 2023
d70d2ea
fix poll interval not updating
rlippmann Oct 3, 2023
4f9f82c
add migrate entities
rlippmann Oct 4, 2023
8f14731
lint fix
rlippmann Oct 4, 2023
456d955
add codeowners, change github to rlippmann, bump pyadtpulse to 1.1.2
rlippmann Oct 7, 2023
c90867c
add changelog
rlippmann Oct 7, 2023
1c772d9
update hacs validation workflow
rlippmann Oct 7, 2023
d06e122
Update manifest.json
rlippmann Oct 7, 2023
c263c4d
Update hacs.json
rlippmann Oct 7, 2023
7c9aa68
Update hacs.json
rlippmann Oct 7, 2023
8b7e4d5
Update hacs.json
rlippmann Oct 7, 2023
da1659a
Update hacs.json
rlippmann Oct 7, 2023
71eb9fd
Update hacs.json
rlippmann Oct 7, 2023
9492ec2
Update hacs.json
rlippmann Oct 7, 2023
aa6eab5
Update hacs.json
rlippmann Oct 7, 2023
bcf1813
Update hassfest.yaml
rlippmann Oct 7, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 10 additions & 25 deletions .github/workflows/hacs.yaml
Original file line number Diff line number Diff line change
@@ -1,32 +1,17 @@
# see https://github.com/KTibow/ha-blueprint
name: "HACS Validation And Formatting"
name: HACS Action

on:
push:
pull_request:
schedule:
- cron: '0 0 * * *'
- cron: "0 0 * * *"

jobs:
ci:
runs-on: ubuntu-latest
hacs:
name: HACS Action
runs-on: "ubuntu-latest"
steps:
- uses: actions/checkout@v3
name: Download repo
- name: HACS Action
uses: "hacs/action@main"
with:
fetch-depth: 0
- uses: actions/setup-python@v3
name: Setup Python
- uses: actions/cache@v3
name: Cache
with:
path: |
~/.cache/pip
key: custom-component-ci
- uses: hacs/action@main
with:
CATEGORY: integration
ignore: brands wheels
- uses: KTibow/ha-blueprint@stable
name: CI
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ignore: brands wheels
category: "integration"
2 changes: 1 addition & 1 deletion .github/workflows/hassfest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ jobs:
validate:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v3"
- uses: "actions/checkout@v4"
- uses: home-assistant/actions/hassfest@master
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ repos:
- id: trailing-whitespace
- id: end-of-file-fixer
- repo: https://github.com/psf/black
rev: 23.3.0
rev: 23.9.1
hooks:
- id: black
args: [--config=pyproject.toml]
- repo: https://github.com/hadialqattan/pycln
rev: v2.1.3
rev: v2.2.2
hooks:
- id: pycln
args: [--config=pyproject.toml]
Expand All @@ -21,10 +21,10 @@ repos:
files: "\\.(py)$"
args: [--settings-path=pyproject.toml]
- repo: https://github.com/dosisod/refurb
rev: v1.15.0
rev: v1.21.0
hooks:
- id: refurb
- repo: https://github.com/asottile/pyupgrade
rev: v3.3.1
rev: v3.13.0
hooks:
- id: pyupgrade
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## 0.3.0 (2023-10-07)

* bump pyadtpulse to 1.1.2
* add options flow for poll interval, relogin interval, keepalive interval
* add reauth config flow
* change code owner to rlippmann
* add gateway and alarm devices
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,28 @@ To enable ADT Pulse, add the following integration like any other integration in

![ADT Form Data](https://github.com/rsnodgrass/hass-adtpulse/blob/master/docs/adt_form_data.jpg?raw=true)


## Options

This integration supports the following options:

* `poll interval`: How often to poll ADT Pulse for updates (in seconds) - default 0.75
* `keepalive interval`: How often to keep the connection alive (in minutes) - default 5
* `relogin interval`: How often to re-authenticate with ADT Pulse (in minutes) - default 120

`poll interval` will determine how quickly Home Assistant will receive updates from ADT Pulse. The Pulse website does this in the background multiple times per second, so setting the poll interval less than a second should be fine. Of course, this will generate more network traffic from your Home Assistant instance to the internet.

`keepalive interval` will determine how often a background call to ADT pulse to keep the connection alive will be made. This is performed by the ADT site to automatically log out after a set time period if the user is inactive. The default of 5 minutes should be fine, but it can be increased if needed, probably to no more than 10 minutes. The minimum value is 1 minute, the maximum is 15 minutes.

`relogin interval` will determine how often a background call to ADT pulse will be made to re-authenticate with ADT Pulse. The ADT servers stop responding automatically after a set time period, even if the user is still active. This attempts to work around this issue. The default of 120 minutes should be fine, but it can be changed if needed, probably to no more than 180 minutes. The minimum value is 20 minutes. Frequently re-authenticating with ADT Pulse more than the default is probably not a good idea, but hasn't been tested.

## Devices

The integration provides the following devices:
* `Alarm Panel`
* `Gateway`
* `Sensors for each zone`: These include 2 entities, one for the sensor status (i.e. Open, Closed, etc). This sensor is named binary_sensor.{zone_name}. The other entity is for a trouble code (i.e. low battery, tamper, etc). Trouble sensors are named binary_sensor.trouble_sensor_{zone name}

## Lovelace

#### Sensors
Expand Down
172 changes: 157 additions & 15 deletions custom_components/adtpulse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,83 @@
"""
from __future__ import annotations

from asyncio import TimeoutError, gather
from logging import getLogger
from asyncio import gather
from typing import Any

from aiohttp.client_exceptions import ClientConnectionError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.const import (
CONF_DEVICE_ID,
CONF_HOST,
CONF_PASSWORD,
CONF_SCAN_INTERVAL,
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.core import HomeAssistant, callback
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 .const import ADTPULSE_DOMAIN, CONF_FINGERPRINT, CONF_HOSTNAME, LOG
from pyadtpulse.const import (
ADT_DEFAULT_KEEPALIVE_INTERVAL,
ADT_DEFAULT_POLL_INTERVAL,
ADT_DEFAULT_RELOGIN_INTERVAL,
)
from pyadtpulse.site import ADTPulseSite

from .const import (
ADTPULSE_DOMAIN,
CONF_FINGERPRINT,
CONF_HOSTNAME,
CONF_KEEPALIVE_INTERVAL,
CONF_RELOGIN_INTERVAL,
)
from .coordinator import ADTPulseDataUpdateCoordinator

LOG = getLogger(__name__)

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}"


@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:
"""Start up the ADT Pulse HA integration.

Expand All @@ -34,38 +95,68 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True


async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
"""Import a config entry from configuration.yaml."""
new_config = {**import_config}
if self.hass.data[CONF_HOST] is not None:
new_config.update({CONF_HOSTNAME: self.hass.data[CONF_HOST]})
new_config.pop(CONF_HOST)
if self.hass.data[CONF_DEVICE_ID] is not None:
new_config.update({CONF_FINGERPRINT: self.hass.data[CONF_DEVICE_ID]})
new_config.pop(CONF_DEVICE_ID)
return await self.async_step_user(new_config)


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Initialize the ADTPulse integration."""
username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
fingerprint = entry.data[CONF_FINGERPRINT]
username = entry.data.get(CONF_USERNAME)
password = entry.data.get(CONF_PASSWORD)
fingerprint = entry.data.get(CONF_FINGERPRINT)
poll_interval = entry.options.get(CONF_SCAN_INTERVAL)
keepalive = entry.options.get(CONF_KEEPALIVE_INTERVAL)
relogin = entry.options.get(CONF_RELOGIN_INTERVAL)
# share reference to the service with other components/platforms
# running within HASS

host = entry.data[CONF_HOSTNAME]
if host:
LOG.debug(f"Using ADT Pulse API host {host}")
LOG.debug("Using ADT Pulse API host %s", host)
if username is None or password is None or fingerprint is None:
raise ConfigEntryAuthFailed("Null value for username, password, or fingerprint")
service = PyADTPulse(
username, password, fingerprint, service_host=host, do_login=False
username,
password,
fingerprint,
service_host=host,
do_login=False,
keepalive_interval=keepalive,
relogin_interval=relogin,
)

hass.data[ADTPULSE_DOMAIN][entry.entry_id] = service
try:
if not await service.async_login():
LOG.error(f"{ADTPULSE_DOMAIN} could not log in as user {username}")
LOG.error("%s could not log in as user %s", ADTPULSE_DOMAIN, username)
raise ConfigEntryAuthFailed(
f"{ADTPULSE_DOMAIN} could not login using supplied credentials"
)
except (ClientConnectionError, TimeoutError) as ex:
LOG.error(f"Unable to connect to ADT Pulse: {ex}")
LOG.error("Unable to connect to ADT Pulse: %s", ex)
raise ConfigEntryNotReady(
f"{ADTPULSE_DOMAIN} could not log in due to a protocol error"
)
) from ex

if service.sites is None:
LOG.error(f"{ADTPULSE_DOMAIN} could not retrieve any sites")
LOG.error("%s could not retrieve any sites", ADTPULSE_DOMAIN)
raise ConfigEntryNotReady(f"{ADTPULSE_DOMAIN} could not retrieve any sites")

try:
service.site.gateway.poll_interval = poll_interval
except ValueError as ex:
LOG.warning(
"Could not set poll interval to %f seconds: %s",
poll_interval,
ex,
)
coordinator = ADTPulseDataUpdateCoordinator(hass, service)
hass.data.setdefault(ADTPULSE_DOMAIN, {})
hass.data[ADTPULSE_DOMAIN][entry.entry_id] = coordinator
Expand All @@ -77,9 +168,60 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.stop)
)
entry.async_on_unload(entry.add_update_listener(options_listener))
return True


async def options_listener(hass: HomeAssistant, entry: ConfigEntry):
"""Handle options update."""
new_poll = entry.options.get(CONF_SCAN_INTERVAL)
new_relogin = entry.options.get(CONF_RELOGIN_INTERVAL)
new_keepalive = entry.options.get(CONF_KEEPALIVE_INTERVAL)
coordinator: ADTPulseDataUpdateCoordinator = hass.data[ADTPULSE_DOMAIN][
entry.entry_id
]
pulse_service = coordinator.adtpulse

if new_poll is not None and new_poll != "":
LOG.info("Setting new poll interval to %f seconds", new_poll)
else:
new_poll = ADT_DEFAULT_POLL_INTERVAL
LOG.info("Re-setting poll interval to default %f seconds", new_poll)
try:
pulse_service.site.gateway.poll_interval = new_poll
coordinator.async_set_updated_data(None)
except ValueError as ex:
LOG.warning(
"Could not set poll interval to %f seconds: %s",
new_poll,
ex,
)

if new_relogin is None or new_relogin == "":
new_relogin = ADT_DEFAULT_RELOGIN_INTERVAL
LOG.info("Re-setting relogin interval to default %d seconds", new_relogin)
else:
LOG.info("Setting new relogin interval to %d seconds", new_relogin)

if new_keepalive is None or new_keepalive == "":
new_keepalive = ADT_DEFAULT_KEEPALIVE_INTERVAL
LOG.info("Re-setting keepalive interval to default %d seconds", new_keepalive)
else:
LOG.info("Setting new keepalive interval to %d seconds", new_keepalive)

try:
pulse_service.keepalive_interval = new_keepalive
except ValueError as ex:
LOG.warning(
"Could not set keepalive interval to %d seconds: %s", new_keepalive, ex
)

try:
pulse_service.relogin_interval = new_relogin
except ValueError as ex:
LOG.warning("Could not set relogin interval to %d seconds: %s", new_relogin, ex)


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
# This is called when an entry/configured device is to be removed. The class
Expand Down
Loading
Loading