pyg90alarm 1.10.1__tar.gz → 1.13.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/.github/workflows/main.yml +3 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/PKG-INFO +14 -2
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/README.rst +7 -0
- pyg90alarm-1.13.0/docs/requirements.txt +10 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm/alarm.py +195 -28
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm/base_cmd.py +38 -65
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm/const.py +29 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm/definitions/sensors.py +241 -1
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm/device_notifications.py +99 -73
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm/discovery.py +11 -43
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm/entities/sensor.py +20 -0
- pyg90alarm-1.13.0/src/pyg90alarm/history.py +218 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm/targeted_discovery.py +8 -43
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm.egg-info/PKG-INFO +14 -2
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/tests/test_alarm.py +153 -27
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/tests/test_base_commands.py +4 -5
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/tests/test_notifications.py +75 -26
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/tox.ini +16 -8
- pyg90alarm-1.10.1/docs/requirements.txt +0 -5
- pyg90alarm-1.10.1/src/pyg90alarm/history.py +0 -56
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/.github/CODEOWNERS +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/.gitignore +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/.pylintrc +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/.readthedocs.yaml +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/LICENSE +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/MANIFEST.in +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/docs/.DS_Store +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/docs/.gitignore +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/docs/api-docs.rst +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/docs/conf.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/docs/index.rst +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/docs/protocol.rst +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/pyproject.toml +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/setup.cfg +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/setup.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/sonar-project.properties +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm/__init__.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm/callback.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm/config.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm/definitions/__init__.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm/entities/__init__.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm/entities/device.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm/exceptions.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm/host_info.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm/host_status.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm/paginated_cmd.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm/paginated_result.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm/user_data_crc.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm.egg-info/SOURCES.txt +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm.egg-info/dependency_links.txt +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm.egg-info/requires.txt +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/src/pyg90alarm.egg-info/top_level.txt +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/tests/conftest.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/tests/device_mock.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/tests/test_callback.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/tests/test_discovery.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.13.0}/tests/test_paginated_commands.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyg90alarm
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.13.0
|
|
4
4
|
Summary: G90 Alarm system protocol
|
|
5
5
|
Home-page: https://github.com/hostcc/pyg90alarm
|
|
6
6
|
Author: Ilia Sotnikov
|
|
@@ -22,10 +22,15 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
22
22
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
23
23
|
Requires-Python: >=3.7, <4
|
|
24
24
|
Description-Content-Type: text/x-rst
|
|
25
|
+
License-File: LICENSE
|
|
25
26
|
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: check-manifest; extra == "dev"
|
|
26
28
|
Provides-Extra: test
|
|
29
|
+
Requires-Dist: coverage; extra == "test"
|
|
30
|
+
Requires-Dist: asynctest; extra == "test"
|
|
27
31
|
Provides-Extra: docs
|
|
28
|
-
|
|
32
|
+
Requires-Dist: sphinx; extra == "docs"
|
|
33
|
+
Requires-Dist: sphinx-rtd-theme; extra == "docs"
|
|
29
34
|
|
|
30
35
|
.. image:: https://github.com/hostcc/pyg90alarm/actions/workflows/main.yml/badge.svg?branch=master
|
|
31
36
|
:target: https://github.com/hostcc/pyg90alarm/tree/master
|
|
@@ -153,6 +158,13 @@ set the IP address allocation up.
|
|
|
153
158
|
status will not be reflected and those will always be reported as inactive,
|
|
154
159
|
since there is no way to read their state in a polled manner.
|
|
155
160
|
|
|
161
|
+
To work that limitation around the package now supports simulating device
|
|
162
|
+
notifications from periodically polling the history it records - the
|
|
163
|
+
simulation works only for the alerts, not notifications (e.g. notifications
|
|
164
|
+
include low battery events and alike). This also requires the particular
|
|
165
|
+
alert to be enabled in the mobile application, otherwise it won't be
|
|
166
|
+
recorded in the history.
|
|
167
|
+
|
|
156
168
|
Quick start
|
|
157
169
|
===========
|
|
158
170
|
|
|
@@ -124,6 +124,13 @@ set the IP address allocation up.
|
|
|
124
124
|
status will not be reflected and those will always be reported as inactive,
|
|
125
125
|
since there is no way to read their state in a polled manner.
|
|
126
126
|
|
|
127
|
+
To work that limitation around the package now supports simulating device
|
|
128
|
+
notifications from periodically polling the history it records - the
|
|
129
|
+
simulation works only for the alerts, not notifications (e.g. notifications
|
|
130
|
+
include low battery events and alike). This also requires the particular
|
|
131
|
+
alert to be enabled in the mobile application, otherwise it won't be
|
|
132
|
+
recorded in the history.
|
|
133
|
+
|
|
127
134
|
Quick start
|
|
128
135
|
===========
|
|
129
136
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
sphinx-rtd-theme
|
|
2
|
+
enum-tools[sphinx]
|
|
3
|
+
pygments>=2.15.0 # not directly required, pinned by Snyk to avoid a vulnerability
|
|
4
|
+
sphinx>=3.3.0 # not directly required, pinned by Snyk to avoid a vulnerability
|
|
5
|
+
setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability
|
|
6
|
+
certifi>=2023.7.22 # not directly required, pinned by Snyk to avoid a vulnerability
|
|
7
|
+
requests>=2.32.0 # not directly required, pinned by Snyk to avoid a vulnerability
|
|
8
|
+
jinja2>=3.1.4 # not directly required, pinned by Snyk to avoid a vulnerability
|
|
9
|
+
requests>=2.31.0 # not directly required, pinned by Snyk to avoid a vulnerability
|
|
10
|
+
idna>=3.7 # not directly required, pinned by Snyk to avoid a vulnerability
|
|
@@ -48,7 +48,7 @@ G90HostInfo(host_guid='<...>',
|
|
|
48
48
|
wifi_signal_level=100)
|
|
49
49
|
|
|
50
50
|
"""
|
|
51
|
-
|
|
51
|
+
import asyncio
|
|
52
52
|
import logging
|
|
53
53
|
from .const import (
|
|
54
54
|
G90Commands, REMOTE_PORT,
|
|
@@ -72,11 +72,14 @@ from .config import (G90AlertConfig, G90AlertConfigFlags)
|
|
|
72
72
|
from .history import G90History
|
|
73
73
|
from .user_data_crc import G90UserDataCRC
|
|
74
74
|
from .callback import G90Callback
|
|
75
|
+
from .exceptions import G90Error, G90TimeoutError
|
|
75
76
|
|
|
76
77
|
_LOGGER = logging.getLogger(__name__)
|
|
77
78
|
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
# pylint: disable=too-many-public-methods
|
|
81
|
+
class G90Alarm(G90DeviceNotifications):
|
|
82
|
+
|
|
80
83
|
"""
|
|
81
84
|
Allows to interact with G90 alarm panel.
|
|
82
85
|
|
|
@@ -92,9 +95,12 @@ class G90Alarm: # pylint: disable=too-many-public-methods
|
|
|
92
95
|
simulated to go into inactive state.
|
|
93
96
|
:type reset_occupancy_interval: int, optional
|
|
94
97
|
"""
|
|
95
|
-
# pylint: disable=too-many-instance-attributes
|
|
98
|
+
# pylint: disable=too-many-instance-attributes,too-many-arguments
|
|
96
99
|
def __init__(self, host, port=REMOTE_PORT,
|
|
97
|
-
reset_occupancy_interval=3
|
|
100
|
+
reset_occupancy_interval=3,
|
|
101
|
+
notifications_host='0.0.0.0',
|
|
102
|
+
notifications_port=NOTIFICATIONS_PORT):
|
|
103
|
+
super().__init__(host=notifications_host, port=notifications_port)
|
|
98
104
|
self._host = host
|
|
99
105
|
self._port = port
|
|
100
106
|
self._sensors = []
|
|
@@ -104,9 +110,12 @@ class G90Alarm: # pylint: disable=too-many-public-methods
|
|
|
104
110
|
self._armdisarm_cb = None
|
|
105
111
|
self._door_open_close_cb = None
|
|
106
112
|
self._alarm_cb = None
|
|
113
|
+
self._low_battery_cb = None
|
|
107
114
|
self._reset_occupancy_interval = reset_occupancy_interval
|
|
108
115
|
self._alert_config = None
|
|
109
116
|
self._sms_alert_when_armed = False
|
|
117
|
+
self._alert_simulation_task = None
|
|
118
|
+
self._alert_simulation_start_listener_back = False
|
|
110
119
|
|
|
111
120
|
async def command(self, code, data=None):
|
|
112
121
|
"""
|
|
@@ -400,10 +409,15 @@ class G90Alarm: # pylint: disable=too-many-public-methods
|
|
|
400
409
|
"""
|
|
401
410
|
res = self.paginated_result(G90Commands.GETHISTORY,
|
|
402
411
|
start, count)
|
|
403
|
-
history = [G90History(*x.data) async for x in res]
|
|
404
|
-
return history
|
|
405
412
|
|
|
406
|
-
|
|
413
|
+
# Sort the history entries from older to newer - device typically does
|
|
414
|
+
# that, but apparently that is not guaranteed
|
|
415
|
+
return sorted(
|
|
416
|
+
[G90History(*x.data) async for x in res],
|
|
417
|
+
key=lambda x: x.datetime, reverse=True
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
async def on_sensor_activity(self, idx, name, occupancy=True):
|
|
407
421
|
"""
|
|
408
422
|
Callback that invoked both for sensor notifications and door open/close
|
|
409
423
|
alerts, since the logic for both is same and could be reused. Please
|
|
@@ -420,6 +434,7 @@ class G90Alarm: # pylint: disable=too-many-public-methods
|
|
|
420
434
|
alerts (only for `door` type sensors, if door open/close alerts are
|
|
421
435
|
enabled)
|
|
422
436
|
"""
|
|
437
|
+
_LOGGER.debug('on_sensor_activity: %s %s %s', idx, name, occupancy)
|
|
423
438
|
sensor = await self.find_sensor(idx, name)
|
|
424
439
|
if sensor:
|
|
425
440
|
_LOGGER.debug('Setting occupancy to %s (previously %s)',
|
|
@@ -477,19 +492,21 @@ class G90Alarm: # pylint: disable=too-many-public-methods
|
|
|
477
492
|
def sensor_callback(self, value):
|
|
478
493
|
self._sensor_cb = value
|
|
479
494
|
|
|
480
|
-
async def
|
|
495
|
+
async def on_door_open_close(self, event_id, zone_name, is_open):
|
|
481
496
|
"""
|
|
482
497
|
Callback that invoked when door open/close alert comes from the alarm
|
|
483
498
|
panel. Please note the callback is for internal use by the class.
|
|
484
499
|
|
|
485
|
-
.. seealso:: `method`:
|
|
500
|
+
.. seealso:: `method`:on_sensor_activity for arguments
|
|
486
501
|
"""
|
|
487
502
|
# Same internal callback is reused both for door open/close alerts and
|
|
488
503
|
# sensor notifications. The former adds reporting when a door is
|
|
489
504
|
# closed, since the notifications aren't sent for such events
|
|
490
|
-
await self.
|
|
505
|
+
await self.on_sensor_activity(event_id, zone_name, is_open)
|
|
491
506
|
# Invoke user specified callback if any
|
|
492
|
-
G90Callback.invoke(
|
|
507
|
+
G90Callback.invoke(
|
|
508
|
+
self._door_open_close_cb, event_id, zone_name, is_open
|
|
509
|
+
)
|
|
493
510
|
|
|
494
511
|
@property
|
|
495
512
|
def door_open_close_callback(self):
|
|
@@ -509,7 +526,7 @@ class G90Alarm: # pylint: disable=too-many-public-methods
|
|
|
509
526
|
"""
|
|
510
527
|
self._door_open_close_cb = value
|
|
511
528
|
|
|
512
|
-
async def
|
|
529
|
+
async def on_armdisarm(self, state):
|
|
513
530
|
"""
|
|
514
531
|
Callback that invoked when the device is armed or disarmed. Please note
|
|
515
532
|
the callback is for internal use by the class.
|
|
@@ -545,7 +562,7 @@ class G90Alarm: # pylint: disable=too-many-public-methods
|
|
|
545
562
|
def armdisarm_callback(self, value):
|
|
546
563
|
self._armdisarm_cb = value
|
|
547
564
|
|
|
548
|
-
async def
|
|
565
|
+
async def on_alarm(self, event_id, zone_name):
|
|
549
566
|
"""
|
|
550
567
|
Callback that invoked when alarm is triggered. Fires alarm callback if
|
|
551
568
|
set by the user with `:property:G90Alarm.alarm_callback`.
|
|
@@ -554,14 +571,20 @@ class G90Alarm: # pylint: disable=too-many-public-methods
|
|
|
554
571
|
:param int: Index of the sensor triggered alarm
|
|
555
572
|
:param str: Sensor name
|
|
556
573
|
"""
|
|
557
|
-
sensor = await self.find_sensor(
|
|
574
|
+
sensor = await self.find_sensor(event_id, zone_name)
|
|
558
575
|
# The callback is still delivered to the caller even if the sensor
|
|
559
|
-
# isn't found, only `extra_data` is skipped. That is to
|
|
576
|
+
# isn't found, only `extra_data` is skipped. That is to ensure the
|
|
560
577
|
# important callback isn't filtered
|
|
561
578
|
extra_data = sensor.extra_data if sensor else None
|
|
579
|
+
# Invoke the sensor activity callback to set the sensor occupancy if
|
|
580
|
+
# sensor is known, but only if that isn't already set - it helps when
|
|
581
|
+
# device notifications on triggerring sensor's activity aren't receveid
|
|
582
|
+
# by a reason
|
|
583
|
+
if sensor and not sensor.occupancy:
|
|
584
|
+
await self.on_sensor_activity(event_id, zone_name, True)
|
|
562
585
|
|
|
563
586
|
G90Callback.invoke(
|
|
564
|
-
self._alarm_cb,
|
|
587
|
+
self._alarm_cb, event_id, zone_name, extra_data
|
|
565
588
|
)
|
|
566
589
|
|
|
567
590
|
@property
|
|
@@ -581,27 +604,49 @@ class G90Alarm: # pylint: disable=too-many-public-methods
|
|
|
581
604
|
def alarm_callback(self, value):
|
|
582
605
|
self._alarm_cb = value
|
|
583
606
|
|
|
584
|
-
async def
|
|
585
|
-
|
|
586
|
-
|
|
607
|
+
async def on_low_battery(self, event_id, zone_name):
|
|
608
|
+
"""
|
|
609
|
+
Callback that invoked when the sensor reports on low battery. Fires
|
|
610
|
+
corresponding callback if set by the user with
|
|
611
|
+
`:property:G90Alarm.on_low_battery_callback`.
|
|
612
|
+
Please note the callback is for internal use by the class.
|
|
613
|
+
|
|
614
|
+
:param int: Index of the sensor triggered alarm
|
|
615
|
+
:param str: Sensor name
|
|
616
|
+
"""
|
|
617
|
+
sensor = await self.find_sensor(event_id, zone_name)
|
|
618
|
+
if sensor:
|
|
619
|
+
# Invoke per-sensor callback if provided
|
|
620
|
+
G90Callback.invoke(sensor.low_battery_callback)
|
|
621
|
+
|
|
622
|
+
G90Callback.invoke(self._low_battery_cb, event_id, zone_name)
|
|
623
|
+
|
|
624
|
+
@property
|
|
625
|
+
def low_battery_callback(self):
|
|
626
|
+
"""
|
|
627
|
+
Get or set low battery callback, the callback is invoked when sensor
|
|
628
|
+
the condition is reported by a sensor.
|
|
629
|
+
|
|
630
|
+
:type: .. py:function:: ()(idx, name)
|
|
631
|
+
"""
|
|
632
|
+
return self._low_battery_cb
|
|
633
|
+
|
|
634
|
+
@low_battery_callback.setter
|
|
635
|
+
def low_battery_callback(self, value):
|
|
636
|
+
self._low_battery_cb = value
|
|
637
|
+
|
|
638
|
+
async def listen_device_notifications(self):
|
|
587
639
|
"""
|
|
588
640
|
Starts internal listener for device notifications/alerts.
|
|
589
641
|
|
|
590
642
|
"""
|
|
591
|
-
self.
|
|
592
|
-
host=host, port=port,
|
|
593
|
-
sensor_cb=self._internal_sensor_cb,
|
|
594
|
-
door_open_close_cb=self._internal_door_open_close_cb,
|
|
595
|
-
armdisarm_cb=self._internal_armdisarm_cb,
|
|
596
|
-
alarm_cb=self._internal_alarm_cb)
|
|
597
|
-
await self._notifications.listen()
|
|
643
|
+
await self.listen()
|
|
598
644
|
|
|
599
645
|
def close_device_notifications(self):
|
|
600
646
|
"""
|
|
601
647
|
Closes the listener for device notifications/alerts.
|
|
602
648
|
"""
|
|
603
|
-
|
|
604
|
-
self._notifications.close()
|
|
649
|
+
self.close()
|
|
605
650
|
|
|
606
651
|
async def arm_away(self):
|
|
607
652
|
"""
|
|
@@ -638,3 +683,125 @@ class G90Alarm: # pylint: disable=too-many-public-methods
|
|
|
638
683
|
@sms_alert_when_armed.setter
|
|
639
684
|
def sms_alert_when_armed(self, value):
|
|
640
685
|
self._sms_alert_when_armed = value
|
|
686
|
+
|
|
687
|
+
async def start_simulating_alerts_from_history(
|
|
688
|
+
self, interval=5, history_depth=5
|
|
689
|
+
):
|
|
690
|
+
"""
|
|
691
|
+
Starts the separate task to simulate device alerts from history
|
|
692
|
+
entries.
|
|
693
|
+
|
|
694
|
+
The listener for device notifications will be stopped, so device
|
|
695
|
+
notifications will not be processed thus resulting in possible
|
|
696
|
+
duplicated if those could be received from the network.
|
|
697
|
+
|
|
698
|
+
:param int interval: Interval (in seconds) between polling for newer
|
|
699
|
+
history entities
|
|
700
|
+
:param int history_depth: Amount of history entries to fetch during
|
|
701
|
+
each polling cycle
|
|
702
|
+
"""
|
|
703
|
+
# Remember if device notifications listener has been started already
|
|
704
|
+
self._alert_simulation_start_listener_back = self.listener_started
|
|
705
|
+
# And then stop it
|
|
706
|
+
self.close()
|
|
707
|
+
|
|
708
|
+
# Start the task
|
|
709
|
+
self._alert_simulation_task = asyncio.create_task(
|
|
710
|
+
self._simulate_alerts_from_history(interval, history_depth)
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
async def stop_simulating_alerts_from_history(self):
|
|
714
|
+
"""
|
|
715
|
+
Stops the task simulating device alerts from history entries.
|
|
716
|
+
|
|
717
|
+
The listener for device notifications will be started back, if it was
|
|
718
|
+
running when simulation has been started.
|
|
719
|
+
"""
|
|
720
|
+
# Stop the task simulating the device alerts from history if it was
|
|
721
|
+
# running
|
|
722
|
+
if self._alert_simulation_task:
|
|
723
|
+
self._alert_simulation_task.cancel()
|
|
724
|
+
self._alert_simulation_task = None
|
|
725
|
+
|
|
726
|
+
# Start device notifications listener back if it was running when
|
|
727
|
+
# simulated alerts have been enabled
|
|
728
|
+
if self._alert_simulation_start_listener_back:
|
|
729
|
+
await self.listen()
|
|
730
|
+
|
|
731
|
+
async def _simulate_alerts_from_history(self, interval, history_depth):
|
|
732
|
+
"""
|
|
733
|
+
Periodically fetches history entries from the device and simulates
|
|
734
|
+
device alerts off of those.
|
|
735
|
+
|
|
736
|
+
Only the history entries occur after the process is started are
|
|
737
|
+
handled, to avoid triggering callbacks retrospectively.
|
|
738
|
+
|
|
739
|
+
See :method:`start_simulating_alerts_from_history` for the parameters.
|
|
740
|
+
"""
|
|
741
|
+
last_history_ts = None
|
|
742
|
+
|
|
743
|
+
_LOGGER.debug(
|
|
744
|
+
'Simulating device alerts from history:'
|
|
745
|
+
' interval %s, history depth %s',
|
|
746
|
+
interval, history_depth
|
|
747
|
+
)
|
|
748
|
+
while True:
|
|
749
|
+
try:
|
|
750
|
+
# Retrieve the history entries of the specified amount - full
|
|
751
|
+
# history retrieval might be an unnecessary long operation
|
|
752
|
+
history = await self.history(count=history_depth)
|
|
753
|
+
|
|
754
|
+
# Initial iteration where no timestamp of most recent history
|
|
755
|
+
# entry is recorded - do that and skip to next iteration, since
|
|
756
|
+
# it isn't yet known what entries would be considered as new
|
|
757
|
+
# ones.
|
|
758
|
+
# Empty history will skip recording the timestamp and the
|
|
759
|
+
# looping over entries below, effectively skipping to next
|
|
760
|
+
# iteration
|
|
761
|
+
if not last_history_ts and history:
|
|
762
|
+
# First entry in the list is assumed to be the most recent
|
|
763
|
+
# one
|
|
764
|
+
last_history_ts = history[0].datetime
|
|
765
|
+
_LOGGER.debug(
|
|
766
|
+
'Initial time stamp of last history entry: %s',
|
|
767
|
+
last_history_ts
|
|
768
|
+
)
|
|
769
|
+
continue
|
|
770
|
+
|
|
771
|
+
# Process history entries from older to newer to preserve the
|
|
772
|
+
# order of happenings
|
|
773
|
+
for item in reversed(history):
|
|
774
|
+
# Process only the entries newer than one been recorded as
|
|
775
|
+
# most recent one
|
|
776
|
+
if item.datetime > last_history_ts:
|
|
777
|
+
_LOGGER.debug(
|
|
778
|
+
'Found newer history entry: %s, simulating alert',
|
|
779
|
+
repr(item)
|
|
780
|
+
)
|
|
781
|
+
# Send the history entry down the device notification
|
|
782
|
+
# code as alert, as if it came from the device and its
|
|
783
|
+
# notifications port
|
|
784
|
+
self._handle_alert(
|
|
785
|
+
(self._host, self._notifications_port),
|
|
786
|
+
item.as_device_alert()
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
# Record the entry as most recent one
|
|
790
|
+
last_history_ts = item.datetime
|
|
791
|
+
_LOGGER.debug(
|
|
792
|
+
'Time stamp of last history entry: %s',
|
|
793
|
+
last_history_ts
|
|
794
|
+
)
|
|
795
|
+
except (G90Error, G90TimeoutError) as exc:
|
|
796
|
+
_LOGGER.debug(
|
|
797
|
+
'Error interacting with device, ignoring %s', repr(exc)
|
|
798
|
+
)
|
|
799
|
+
except Exception as exc:
|
|
800
|
+
_LOGGER.error(
|
|
801
|
+
'Exception simulating device alerts from history: %s',
|
|
802
|
+
repr(exc)
|
|
803
|
+
)
|
|
804
|
+
raise exc
|
|
805
|
+
|
|
806
|
+
# Sleep to next iteration
|
|
807
|
+
await asyncio.sleep(interval)
|
|
@@ -44,62 +44,6 @@ class G90Header(NamedTuple):
|
|
|
44
44
|
data: Optional[str] = None
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
class G90DeviceProtocol:
|
|
48
|
-
"""
|
|
49
|
-
tbd
|
|
50
|
-
|
|
51
|
-
:meta private:
|
|
52
|
-
"""
|
|
53
|
-
def __init__(self):
|
|
54
|
-
"""
|
|
55
|
-
tbd
|
|
56
|
-
"""
|
|
57
|
-
self._data = None
|
|
58
|
-
|
|
59
|
-
@property
|
|
60
|
-
def future_data(self):
|
|
61
|
-
"""
|
|
62
|
-
tbd
|
|
63
|
-
"""
|
|
64
|
-
return self._data
|
|
65
|
-
|
|
66
|
-
@future_data.setter
|
|
67
|
-
def future_data(self, value):
|
|
68
|
-
"""
|
|
69
|
-
tbd
|
|
70
|
-
"""
|
|
71
|
-
self._data = value
|
|
72
|
-
|
|
73
|
-
def connection_made(self, transport):
|
|
74
|
-
"""
|
|
75
|
-
tbd
|
|
76
|
-
"""
|
|
77
|
-
|
|
78
|
-
def connection_lost(self, exc):
|
|
79
|
-
"""
|
|
80
|
-
tbd
|
|
81
|
-
"""
|
|
82
|
-
|
|
83
|
-
def datagram_received(self, data, addr):
|
|
84
|
-
"""
|
|
85
|
-
tbd
|
|
86
|
-
"""
|
|
87
|
-
if asyncio.isfuture(self._data):
|
|
88
|
-
if self._data.done():
|
|
89
|
-
_LOGGER.warning('Excessive packet received'
|
|
90
|
-
' from %s:%s: %s',
|
|
91
|
-
addr[0], addr[1], data)
|
|
92
|
-
return
|
|
93
|
-
self._data.set_result((*addr, data))
|
|
94
|
-
|
|
95
|
-
def error_received(self, exc):
|
|
96
|
-
"""
|
|
97
|
-
tbd
|
|
98
|
-
"""
|
|
99
|
-
if asyncio.isfuture(self._data) and not self._data.done():
|
|
100
|
-
self._data.set_exception(exc)
|
|
101
|
-
|
|
102
|
-
|
|
103
47
|
class G90BaseCommand:
|
|
104
48
|
"""
|
|
105
49
|
tbd
|
|
@@ -123,6 +67,7 @@ class G90BaseCommand:
|
|
|
123
67
|
self._retries = retries
|
|
124
68
|
self._data = '""'
|
|
125
69
|
self._result = None
|
|
70
|
+
self._connection_result = None
|
|
126
71
|
if data:
|
|
127
72
|
self._data = json.dumps([code, data],
|
|
128
73
|
# No newlines to be inserted
|
|
@@ -131,11 +76,39 @@ class G90BaseCommand:
|
|
|
131
76
|
separators=(',', ':'))
|
|
132
77
|
self._resp = G90Header()
|
|
133
78
|
|
|
134
|
-
|
|
79
|
+
# Implementation of datagram protocol,
|
|
80
|
+
# https://docs.python.org/3/library/asyncio-protocol.html#datagram-protocols
|
|
81
|
+
def connection_made(self, transport):
|
|
82
|
+
"""
|
|
83
|
+
tbd
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def connection_lost(self, exc):
|
|
87
|
+
"""
|
|
88
|
+
tbd
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def datagram_received(self, data, addr):
|
|
92
|
+
"""
|
|
93
|
+
tbd
|
|
94
|
+
"""
|
|
95
|
+
if asyncio.isfuture(self._connection_result):
|
|
96
|
+
if self._connection_result.done():
|
|
97
|
+
_LOGGER.warning('Excessive packet received'
|
|
98
|
+
' from %s:%s: %s',
|
|
99
|
+
addr[0], addr[1], data)
|
|
100
|
+
return
|
|
101
|
+
self._connection_result.set_result((*addr, data))
|
|
102
|
+
|
|
103
|
+
def error_received(self, exc):
|
|
135
104
|
"""
|
|
136
105
|
tbd
|
|
137
106
|
"""
|
|
138
|
-
|
|
107
|
+
if (
|
|
108
|
+
asyncio.isfuture(self._connection_result) and not
|
|
109
|
+
self._connection_result.done()
|
|
110
|
+
):
|
|
111
|
+
self._connection_result.set_exception(exc)
|
|
139
112
|
|
|
140
113
|
async def _create_connection(self):
|
|
141
114
|
"""
|
|
@@ -153,7 +126,7 @@ class G90BaseCommand:
|
|
|
153
126
|
extra_kwargs['local_addr'] = ('0.0.0.0', self._local_port)
|
|
154
127
|
|
|
155
128
|
transport, protocol = await loop.create_datagram_endpoint(
|
|
156
|
-
self
|
|
129
|
+
lambda: self,
|
|
157
130
|
remote_addr=(self.host, self.port),
|
|
158
131
|
**extra_kwargs,
|
|
159
132
|
allow_broadcast=True)
|
|
@@ -241,7 +214,7 @@ class G90BaseCommand:
|
|
|
241
214
|
tbd
|
|
242
215
|
"""
|
|
243
216
|
|
|
244
|
-
transport,
|
|
217
|
+
transport, _ = await self._create_connection()
|
|
245
218
|
attempts = self._retries
|
|
246
219
|
while True:
|
|
247
220
|
attempts = attempts - 1
|
|
@@ -249,24 +222,24 @@ class G90BaseCommand:
|
|
|
249
222
|
loop = asyncio.get_running_loop()
|
|
250
223
|
except AttributeError:
|
|
251
224
|
loop = asyncio.get_event_loop()
|
|
252
|
-
|
|
225
|
+
self._connection_result = loop.create_future()
|
|
253
226
|
async with self._sk_lock:
|
|
254
227
|
_LOGGER.debug('(code %s) Sending request to %s:%s',
|
|
255
228
|
self._code, self.host, self.port)
|
|
256
229
|
transport.sendto(self.to_wire())
|
|
257
|
-
done, _ = await asyncio.wait([
|
|
230
|
+
done, _ = await asyncio.wait([self._connection_result],
|
|
258
231
|
timeout=self._timeout)
|
|
259
|
-
if
|
|
232
|
+
if self._connection_result in done:
|
|
260
233
|
break
|
|
261
234
|
# Cancel the future to signal protocol handler it is no longer
|
|
262
235
|
# valid, the future will be re-created on next retry
|
|
263
|
-
|
|
236
|
+
self._connection_result.cancel()
|
|
264
237
|
if not attempts:
|
|
265
238
|
transport.close()
|
|
266
239
|
raise G90TimeoutError()
|
|
267
240
|
_LOGGER.debug('Timed out, retrying')
|
|
268
241
|
transport.close()
|
|
269
|
-
(host, port, data) =
|
|
242
|
+
(host, port, data) = self._connection_result.result()
|
|
270
243
|
_LOGGER.debug('Received response from %s:%s', host, port)
|
|
271
244
|
if self.host != '255.255.255.255':
|
|
272
245
|
if self.host != host or host == '255.255.255.255':
|
|
@@ -185,10 +185,21 @@ class G90AlertSources(IntEnum):
|
|
|
185
185
|
"""
|
|
186
186
|
Defines possible sources of the alert sent by the panel.
|
|
187
187
|
"""
|
|
188
|
+
DEVICE = 0
|
|
188
189
|
SENSOR = 1
|
|
189
190
|
DOORBELL = 12
|
|
190
191
|
|
|
191
192
|
|
|
193
|
+
class G90AlertStates(IntEnum):
|
|
194
|
+
"""
|
|
195
|
+
Defines possible states of the alert sent by the panel.
|
|
196
|
+
"""
|
|
197
|
+
DOOR_CLOSE = 0
|
|
198
|
+
DOOR_OPEN = 1
|
|
199
|
+
TAMPER = 3
|
|
200
|
+
LOW_BATTERY = 4
|
|
201
|
+
|
|
202
|
+
|
|
192
203
|
class G90AlertStateChangeTypes(IntEnum):
|
|
193
204
|
"""
|
|
194
205
|
Defines types of alert for device state changes.
|
|
@@ -201,3 +212,21 @@ class G90AlertStateChangeTypes(IntEnum):
|
|
|
201
212
|
LOW_BATTERY = 6
|
|
202
213
|
WIFI_CONNECTED = 7
|
|
203
214
|
WIFI_DISCONNECTED = 8
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class G90HistoryStates(IntEnum):
|
|
218
|
+
"""
|
|
219
|
+
Defines possible states for history entities.
|
|
220
|
+
"""
|
|
221
|
+
DOOR_CLOSE = 1
|
|
222
|
+
DOOR_OPEN = 2
|
|
223
|
+
TAMPER = 3
|
|
224
|
+
ALARM = 4
|
|
225
|
+
AC_POWER_FAILURE = 5
|
|
226
|
+
AC_POWER_RECOVER = 6
|
|
227
|
+
DISARM = 7
|
|
228
|
+
ARM_AWAY = 8
|
|
229
|
+
ARM_HOME = 9
|
|
230
|
+
LOW_BATTERY = 10
|
|
231
|
+
WIFI_CONNECTED = 11
|
|
232
|
+
WIFI_DISCONNECTED = 12
|