Skip to content

Commit a1164f4

Browse files
authored
Merge pull request #12 from rlippmann/1.2.9-fixes
1.2.9
2 parents 6d67f93 + 305b76e commit a1164f4

File tree

7 files changed

+107
-46
lines changed

7 files changed

+107
-46
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
## 1.2.9 (2024-04-21)
2+
3+
* ignore query string in check_login_errors(). This should fix a bug where the task was logged out
4+
but not correctly being identified
5+
* remove unnecessary warning in alarm status check
6+
* add arm night
7+
* refactor update_alarm_from_etree()
8+
* bump to newer user agent
9+
* skip sync check if it will back off
10+
* fix linter issue in _initialize_sites
11+
112
## 1.2.8 (2024-03-07)
213

314
* add more detail to "invalid sync check" error logging

pyadtpulse/alarm_panel.py

Lines changed: 72 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
ADT_ALARM_UNKNOWN = "unknown"
2222
ADT_ALARM_ARMING = "arming"
2323
ADT_ALARM_DISARMING = "disarming"
24+
ADT_ALARM_NIGHT = "night"
2425

2526
ALARM_STATUSES = (
2627
ADT_ALARM_AWAY,
@@ -29,8 +30,16 @@
2930
ADT_ALARM_UNKNOWN,
3031
ADT_ALARM_ARMING,
3132
ADT_ALARM_DISARMING,
33+
ADT_ALARM_NIGHT,
3234
)
3335

36+
ALARM_POSSIBLE_STATUS_MAP = {
37+
"Disarmed": (ADT_ALARM_OFF, ADT_ALARM_ARMING),
38+
"Armed Away": (ADT_ALARM_AWAY, ADT_ALARM_DISARMING),
39+
"Armed Stay": (ADT_ALARM_HOME, ADT_ALARM_DISARMING),
40+
"Armed Night": (ADT_ALARM_NIGHT, ADT_ALARM_DISARMING),
41+
}
42+
3443
ADT_ARM_DISARM_TIMEOUT: float = 20
3544

3645

@@ -129,6 +138,16 @@ def is_disarming(self) -> bool:
129138
with self._state_lock:
130139
return self._status == ADT_ALARM_DISARMING
131140

141+
@property
142+
def is_armed_night(self) -> bool:
143+
"""Return if system is in night mode.
144+
145+
Returns:
146+
bool: True if system is in night mode
147+
"""
148+
with self._state_lock:
149+
return self._status == ADT_ALARM_NIGHT
150+
132151
@property
133152
def last_update(self) -> float:
134153
"""Return last update time.
@@ -198,7 +217,7 @@ async def _arm(
198217
if arm_result is not None:
199218
error_block = arm_result.find(".//div")
200219
if error_block is not None:
201-
error_text = arm_result.text_contents().replace(
220+
error_text = arm_result.text_content().replace(
202221
"Arm AnywayCancel\n\n", ""
203222
)
204223
LOG.warning(
@@ -240,6 +259,18 @@ def arm_away(self, connection: PulseConnection, force_arm: bool = False) -> bool
240259
"""
241260
return self._sync_set_alarm_mode(connection, ADT_ALARM_AWAY, force_arm)
242261

262+
@typechecked
263+
def arm_night(self, connection: PulseConnection, force_arm: bool = False) -> bool:
264+
"""Arm the alarm in Night mode.
265+
266+
Args:
267+
force_arm (bool, Optional): force system to arm
268+
269+
Returns:
270+
bool: True if arm succeeded
271+
"""
272+
return self._sync_set_alarm_mode(connection, ADT_ALARM_NIGHT, force_arm)
273+
243274
@typechecked
244275
def arm_home(self, connection: PulseConnection, force_arm: bool = False) -> bool:
245276
"""Arm the alarm in Home mode.
@@ -288,6 +319,19 @@ async def async_arm_home(
288319
"""
289320
return await self._arm(connection, ADT_ALARM_HOME, force_arm)
290321

322+
@typechecked
323+
async def async_arm_night(
324+
self, connection: PulseConnection, force_arm: bool = False
325+
) -> bool:
326+
"""Arm alarm night async.
327+
328+
Args:
329+
force_arm (bool, Optional): force system to arm
330+
Returns:
331+
bool: True if arm succeeded
332+
"""
333+
return await self._arm(connection, ADT_ALARM_NIGHT, force_arm)
334+
291335
@typechecked
292336
async def async_disarm(self, connection: PulseConnection) -> bool:
293337
"""Disarm alarm async.
@@ -313,51 +357,44 @@ def update_alarm_from_etree(self, summary_html_etree: html.HtmlElement) -> None:
313357
value = summary_html_etree.find(".//span[@class='p_boldNormalTextLarge']")
314358
sat_location = "security_button_0"
315359
with self._state_lock:
360+
status_found = False
361+
last_updated = int(time())
316362
if value is not None:
317363
text = value.text_content().lstrip().splitlines()[0]
318-
last_updated = int(time())
319-
320-
if text.startswith("Disarmed"):
321-
if (
322-
self._status != ADT_ALARM_ARMING
323-
or last_updated - self._last_arm_disarm > ADT_ARM_DISARM_TIMEOUT
324-
):
325-
self._status = ADT_ALARM_OFF
326-
self._last_arm_disarm = last_updated
327-
elif text.startswith("Armed Away"):
328-
if (
329-
self._status != ADT_ALARM_DISARMING
330-
or last_updated - self._last_arm_disarm > ADT_ARM_DISARM_TIMEOUT
331-
):
332-
self._status = ADT_ALARM_AWAY
333-
self._last_arm_disarm = last_updated
334-
elif text.startswith("Armed Stay"):
335-
if (
336-
self._status != ADT_ALARM_DISARMING
337-
or last_updated - self._last_arm_disarm > ADT_ARM_DISARM_TIMEOUT
338-
):
339-
self._status = ADT_ALARM_HOME
340-
self._last_arm_disarm = last_updated
341-
else:
364+
365+
for (
366+
current_status,
367+
possible_statuses,
368+
) in ALARM_POSSIBLE_STATUS_MAP.items():
369+
if text.startswith(current_status):
370+
status_found = True
371+
if (
372+
self._status != possible_statuses[1]
373+
or last_updated - self._last_arm_disarm
374+
> ADT_ARM_DISARM_TIMEOUT
375+
):
376+
self._status = possible_statuses[0]
377+
self._last_arm_disarm = last_updated
378+
break
379+
380+
if value is None or not status_found:
381+
if not text.startswith("Status Unavailable"):
342382
LOG.warning("Failed to get alarm status from '%s'", text)
343-
self._status = ADT_ALARM_UNKNOWN
344-
self._last_arm_disarm = last_updated
345-
return
346-
LOG.debug("Alarm status = %s", self._status)
383+
self._status = ADT_ALARM_UNKNOWN
384+
self._last_arm_disarm = last_updated
385+
return
386+
LOG.debug("Alarm status = %s", self._status)
347387
sat_string = f'.//input[@id="{sat_location}"]'
348388
sat_button = summary_html_etree.find(sat_string)
349389
if sat_button is not None and "onclick" in sat_button.attrib:
350390
on_click = sat_button.attrib["onclick"]
351391
match = re.search(r"sat=([a-z0-9\-]+)", on_click)
352392
if match:
353393
self._sat = match.group(1)
354-
elif len(self._sat) == 0:
355-
LOG.warning("No sat recorded and was unable extract sat.")
356-
357-
if len(self._sat) > 0:
358-
LOG.debug("Extracted sat = %s", self._sat)
394+
if not self._sat:
395+
LOG.warning("No sat recorded and was unable to extract sat.")
359396
else:
360-
LOG.warning("Unable to extract sat")
397+
LOG.debug("Extracted sat = %s", self._sat)
361398

362399
@typechecked
363400
def set_alarm_attributes(self, alarm_attributes: dict[str, str]) -> None:

pyadtpulse/const.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Constants for pyadtpulse."""
22

3-
__version__ = "1.2.8"
3+
__version__ = "1.2.9"
44

55
DEFAULT_API_HOST = "https://portal.adtpulse.com"
66
API_HOST_CA = "https://portal-ca.adtpulse.com" # Canada
@@ -31,12 +31,12 @@
3131
# than that
3232
ADT_DEFAULT_POLL_INTERVAL = 2.0
3333
ADT_GATEWAY_MAX_OFFLINE_POLL_INTERVAL = 600.0
34-
ADT_MAX_BACKOFF: float = 15.0 * 60.0
34+
ADT_MAX_BACKOFF: float = 5.0 * 60.0
3535
ADT_DEFAULT_HTTP_USER_AGENT = {
3636
"User-Agent": (
37-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
37+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
3838
"AppleWebKit/537.36 (KHTML, like Gecko) "
39-
"Chrome/100.0.4896.127 Safari/537.36 Edg/100.0.1185.44"
39+
"Chrome/122.0.0.0 Safari/537.36"
4040
)
4141
}
4242

pyadtpulse/pulse_connection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def determine_error_type():
120120
"""
121121
self._login_in_progress = False
122122
url = self._connection_properties.make_url(ADT_LOGIN_URI)
123-
if url == response_url_string:
123+
if response_url_string.startswith(url):
124124
error = tree.find(".//div[@id='warnMsgContents']")
125125
if error is not None:
126126
error_text = error.text_content()

pyadtpulse/pyadtpulse_async.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -171,13 +171,15 @@ async def _initialize_sites(self, tree: html.HtmlElement) -> None:
171171
"""
172172
# typically, ADT Pulse accounts have only a single site (premise/location)
173173
single_premise = tree.find(".//span[@id='p_singlePremise']")
174-
if single_premise is not None:
174+
if single_premise is not None and single_premise.text:
175175
site_name = single_premise.text
176176
start_time = 0.0
177177
if self._pulse_connection.detailed_debug_logging:
178178
start_time = time.time()
179-
# FIXME: this code works, but it doesn't pass the linter
180-
signout_link = str(tree.find(".//a[@class='p_signoutlink']").get("href"))
179+
temp = tree.find(".//a[@class='p_signoutlink']")
180+
signout_link = None
181+
if temp is not None:
182+
signout_link = str(temp.get("href"))
181183
if signout_link:
182184
m = re.search("networkid=(.+)&", signout_link)
183185
if m and m.group(1) and m.group(1):
@@ -272,9 +274,13 @@ def should_relogin(relogin_interval: int) -> bool:
272274
relogin_interval = self._pulse_properties.relogin_interval * 60
273275
try:
274276
await asyncio.sleep(self._pulse_properties.keepalive_interval * 60)
275-
if self._pulse_connection_status.retry_after > time.time():
277+
if (
278+
self._pulse_connection_status.retry_after > time.time()
279+
or self._pulse_connection_status.get_backoff().backoff_count
280+
> WARN_TRANSIENT_FAILURE_THRESHOLD
281+
):
276282
LOG.debug(
277-
"%s: Skipping actions because retry_after > now", task_name
283+
"%s: Skipping actions because query will backoff", task_name
278284
)
279285
continue
280286
if not self._pulse_connection.is_connected:

pyadtpulse/site.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ async def async_arm_away(self, force_arm: bool = False) -> bool:
7878
self._pulse_connection, force_arm=force_arm
7979
)
8080

81+
@typechecked
82+
async def async_arm_night(self, force_arm: bool = False) -> bool:
83+
"""Arm system away async."""
84+
return await self.alarm_control_panel.async_arm_night(
85+
self._pulse_connection, force_arm=force_arm
86+
)
87+
8188
async def async_disarm(self) -> bool:
8289
"""Disarm system async."""
8390
return await self.alarm_control_panel.async_disarm(self._pulse_connection)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "pyadtpulse"
3-
version = "1.2.8"
3+
version = "1.2.9"
44
description = "Python interface for ADT Pulse security systems"
55
authors = ["Ryan Snodgrass"]
66
maintainers = ["Robert Lippmann"]

0 commit comments

Comments
 (0)