pyg90alarm 1.10.1__tar.gz → 1.12.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.12.0}/.github/workflows/main.yml +3 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/PKG-INFO +14 -2
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/README.rst +7 -0
- pyg90alarm-1.12.0/docs/requirements.txt +10 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm/alarm.py +170 -27
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm/base_cmd.py +38 -65
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm/const.py +29 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm/definitions/sensors.py +241 -1
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm/device_notifications.py +99 -73
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm/discovery.py +11 -43
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm/entities/sensor.py +20 -0
- pyg90alarm-1.12.0/src/pyg90alarm/history.py +218 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm/targeted_discovery.py +8 -43
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm.egg-info/PKG-INFO +14 -2
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/tests/test_alarm.py +105 -25
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/tests/test_base_commands.py +4 -5
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/tests/test_notifications.py +75 -26
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.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.12.0}/.github/CODEOWNERS +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/.gitignore +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/.pylintrc +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/.readthedocs.yaml +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/LICENSE +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/MANIFEST.in +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/docs/.DS_Store +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/docs/.gitignore +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/docs/api-docs.rst +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/docs/conf.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/docs/index.rst +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/docs/protocol.rst +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/pyproject.toml +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/setup.cfg +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/setup.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/sonar-project.properties +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm/__init__.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm/callback.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm/config.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm/definitions/__init__.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm/entities/__init__.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm/entities/device.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm/exceptions.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm/host_info.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm/host_status.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm/paginated_cmd.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm/paginated_result.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm/user_data_crc.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm.egg-info/SOURCES.txt +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm.egg-info/dependency_links.txt +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm.egg-info/requires.txt +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/src/pyg90alarm.egg-info/top_level.txt +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/tests/conftest.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/tests/device_mock.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/tests/test_callback.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.0}/tests/test_discovery.py +0 -0
- {pyg90alarm-1.10.1 → pyg90alarm-1.12.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.12.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,
|
|
@@ -76,7 +76,9 @@ from .callback import G90Callback
|
|
|
76
76
|
_LOGGER = logging.getLogger(__name__)
|
|
77
77
|
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
# pylint: disable=too-many-public-methods
|
|
80
|
+
class G90Alarm(G90DeviceNotifications):
|
|
81
|
+
|
|
80
82
|
"""
|
|
81
83
|
Allows to interact with G90 alarm panel.
|
|
82
84
|
|
|
@@ -92,9 +94,12 @@ class G90Alarm: # pylint: disable=too-many-public-methods
|
|
|
92
94
|
simulated to go into inactive state.
|
|
93
95
|
:type reset_occupancy_interval: int, optional
|
|
94
96
|
"""
|
|
95
|
-
# pylint: disable=too-many-instance-attributes
|
|
97
|
+
# pylint: disable=too-many-instance-attributes,too-many-arguments
|
|
96
98
|
def __init__(self, host, port=REMOTE_PORT,
|
|
97
|
-
reset_occupancy_interval=3
|
|
99
|
+
reset_occupancy_interval=3,
|
|
100
|
+
notifications_host='0.0.0.0',
|
|
101
|
+
notifications_port=NOTIFICATIONS_PORT):
|
|
102
|
+
super().__init__(host=notifications_host, port=notifications_port)
|
|
98
103
|
self._host = host
|
|
99
104
|
self._port = port
|
|
100
105
|
self._sensors = []
|
|
@@ -104,9 +109,12 @@ class G90Alarm: # pylint: disable=too-many-public-methods
|
|
|
104
109
|
self._armdisarm_cb = None
|
|
105
110
|
self._door_open_close_cb = None
|
|
106
111
|
self._alarm_cb = None
|
|
112
|
+
self._low_battery_cb = None
|
|
107
113
|
self._reset_occupancy_interval = reset_occupancy_interval
|
|
108
114
|
self._alert_config = None
|
|
109
115
|
self._sms_alert_when_armed = False
|
|
116
|
+
self._alert_simulation_task = None
|
|
117
|
+
self._alert_simulation_start_listener_back = False
|
|
110
118
|
|
|
111
119
|
async def command(self, code, data=None):
|
|
112
120
|
"""
|
|
@@ -400,10 +408,15 @@ class G90Alarm: # pylint: disable=too-many-public-methods
|
|
|
400
408
|
"""
|
|
401
409
|
res = self.paginated_result(G90Commands.GETHISTORY,
|
|
402
410
|
start, count)
|
|
403
|
-
history = [G90History(*x.data) async for x in res]
|
|
404
|
-
return history
|
|
405
411
|
|
|
406
|
-
|
|
412
|
+
# Sort the history entries from older to newer - device typically does
|
|
413
|
+
# that, but apparently that is not guaranteed
|
|
414
|
+
return sorted(
|
|
415
|
+
[G90History(*x.data) async for x in res],
|
|
416
|
+
key=lambda x: x.datetime, reverse=True
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
async def on_sensor_activity(self, idx, name, occupancy=True):
|
|
407
420
|
"""
|
|
408
421
|
Callback that invoked both for sensor notifications and door open/close
|
|
409
422
|
alerts, since the logic for both is same and could be reused. Please
|
|
@@ -420,6 +433,7 @@ class G90Alarm: # pylint: disable=too-many-public-methods
|
|
|
420
433
|
alerts (only for `door` type sensors, if door open/close alerts are
|
|
421
434
|
enabled)
|
|
422
435
|
"""
|
|
436
|
+
_LOGGER.debug('on_sensor_acitvity: %s %s %s', idx, name, occupancy)
|
|
423
437
|
sensor = await self.find_sensor(idx, name)
|
|
424
438
|
if sensor:
|
|
425
439
|
_LOGGER.debug('Setting occupancy to %s (previously %s)',
|
|
@@ -477,19 +491,21 @@ class G90Alarm: # pylint: disable=too-many-public-methods
|
|
|
477
491
|
def sensor_callback(self, value):
|
|
478
492
|
self._sensor_cb = value
|
|
479
493
|
|
|
480
|
-
async def
|
|
494
|
+
async def on_door_open_close(self, event_id, zone_name, is_open):
|
|
481
495
|
"""
|
|
482
496
|
Callback that invoked when door open/close alert comes from the alarm
|
|
483
497
|
panel. Please note the callback is for internal use by the class.
|
|
484
498
|
|
|
485
|
-
.. seealso:: `method`:
|
|
499
|
+
.. seealso:: `method`:on_sensor_activity for arguments
|
|
486
500
|
"""
|
|
487
501
|
# Same internal callback is reused both for door open/close alerts and
|
|
488
502
|
# sensor notifications. The former adds reporting when a door is
|
|
489
503
|
# closed, since the notifications aren't sent for such events
|
|
490
|
-
await self.
|
|
504
|
+
await self.on_sensor_activity(event_id, zone_name, is_open)
|
|
491
505
|
# Invoke user specified callback if any
|
|
492
|
-
G90Callback.invoke(
|
|
506
|
+
G90Callback.invoke(
|
|
507
|
+
self._door_open_close_cb, event_id, zone_name, is_open
|
|
508
|
+
)
|
|
493
509
|
|
|
494
510
|
@property
|
|
495
511
|
def door_open_close_callback(self):
|
|
@@ -509,7 +525,7 @@ class G90Alarm: # pylint: disable=too-many-public-methods
|
|
|
509
525
|
"""
|
|
510
526
|
self._door_open_close_cb = value
|
|
511
527
|
|
|
512
|
-
async def
|
|
528
|
+
async def on_armdisarm(self, state):
|
|
513
529
|
"""
|
|
514
530
|
Callback that invoked when the device is armed or disarmed. Please note
|
|
515
531
|
the callback is for internal use by the class.
|
|
@@ -545,7 +561,7 @@ class G90Alarm: # pylint: disable=too-many-public-methods
|
|
|
545
561
|
def armdisarm_callback(self, value):
|
|
546
562
|
self._armdisarm_cb = value
|
|
547
563
|
|
|
548
|
-
async def
|
|
564
|
+
async def on_alarm(self, event_id, zone_name):
|
|
549
565
|
"""
|
|
550
566
|
Callback that invoked when alarm is triggered. Fires alarm callback if
|
|
551
567
|
set by the user with `:property:G90Alarm.alarm_callback`.
|
|
@@ -554,14 +570,14 @@ class G90Alarm: # pylint: disable=too-many-public-methods
|
|
|
554
570
|
:param int: Index of the sensor triggered alarm
|
|
555
571
|
:param str: Sensor name
|
|
556
572
|
"""
|
|
557
|
-
sensor = await self.find_sensor(
|
|
573
|
+
sensor = await self.find_sensor(event_id, zone_name)
|
|
558
574
|
# The callback is still delivered to the caller even if the sensor
|
|
559
575
|
# isn't found, only `extra_data` is skipped. That is to ensur the
|
|
560
576
|
# important callback isn't filtered
|
|
561
577
|
extra_data = sensor.extra_data if sensor else None
|
|
562
578
|
|
|
563
579
|
G90Callback.invoke(
|
|
564
|
-
self._alarm_cb,
|
|
580
|
+
self._alarm_cb, event_id, zone_name, extra_data
|
|
565
581
|
)
|
|
566
582
|
|
|
567
583
|
@property
|
|
@@ -581,27 +597,49 @@ class G90Alarm: # pylint: disable=too-many-public-methods
|
|
|
581
597
|
def alarm_callback(self, value):
|
|
582
598
|
self._alarm_cb = value
|
|
583
599
|
|
|
584
|
-
async def
|
|
585
|
-
|
|
586
|
-
|
|
600
|
+
async def on_low_battery(self, event_id, zone_name):
|
|
601
|
+
"""
|
|
602
|
+
Callback that invoked when the sensor reports on low battery. Fires
|
|
603
|
+
corresponding callback if set by the user with
|
|
604
|
+
`:property:G90Alarm.on_low_battery_callback`.
|
|
605
|
+
Please note the callback is for internal use by the class.
|
|
606
|
+
|
|
607
|
+
:param int: Index of the sensor triggered alarm
|
|
608
|
+
:param str: Sensor name
|
|
609
|
+
"""
|
|
610
|
+
sensor = await self.find_sensor(event_id, zone_name)
|
|
611
|
+
if sensor:
|
|
612
|
+
# Invoke per-sensor callback if provided
|
|
613
|
+
G90Callback.invoke(sensor.low_battery_callback)
|
|
614
|
+
|
|
615
|
+
G90Callback.invoke(self._low_battery_cb, event_id, zone_name)
|
|
616
|
+
|
|
617
|
+
@property
|
|
618
|
+
def low_battery_callback(self):
|
|
619
|
+
"""
|
|
620
|
+
Get or set low battery callback, the callback is invoked when sensor
|
|
621
|
+
the condition is reported by a sensor.
|
|
622
|
+
|
|
623
|
+
:type: .. py:function:: ()(idx, name)
|
|
624
|
+
"""
|
|
625
|
+
return self._low_battery_cb
|
|
626
|
+
|
|
627
|
+
@low_battery_callback.setter
|
|
628
|
+
def low_battery_callback(self, value):
|
|
629
|
+
self._low_battery_cb = value
|
|
630
|
+
|
|
631
|
+
async def listen_device_notifications(self):
|
|
587
632
|
"""
|
|
588
633
|
Starts internal listener for device notifications/alerts.
|
|
589
634
|
|
|
590
635
|
"""
|
|
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()
|
|
636
|
+
await self.listen()
|
|
598
637
|
|
|
599
638
|
def close_device_notifications(self):
|
|
600
639
|
"""
|
|
601
640
|
Closes the listener for device notifications/alerts.
|
|
602
641
|
"""
|
|
603
|
-
|
|
604
|
-
self._notifications.close()
|
|
642
|
+
self.close()
|
|
605
643
|
|
|
606
644
|
async def arm_away(self):
|
|
607
645
|
"""
|
|
@@ -638,3 +676,108 @@ class G90Alarm: # pylint: disable=too-many-public-methods
|
|
|
638
676
|
@sms_alert_when_armed.setter
|
|
639
677
|
def sms_alert_when_armed(self, value):
|
|
640
678
|
self._sms_alert_when_armed = value
|
|
679
|
+
|
|
680
|
+
async def start_simulating_alerts_from_history(
|
|
681
|
+
self, interval=5, history_depth=5
|
|
682
|
+
):
|
|
683
|
+
"""
|
|
684
|
+
Starts the separate task to simulate device alerts from history
|
|
685
|
+
entries.
|
|
686
|
+
|
|
687
|
+
The listener for device notifications will be stopped, so device
|
|
688
|
+
notifications will not be processed thus resulting in possible
|
|
689
|
+
duplicated if those could be received from the network.
|
|
690
|
+
|
|
691
|
+
:param int interval: Interval (in seconds) between polling for newer
|
|
692
|
+
history entities
|
|
693
|
+
:param int history_depth: Amount of history entries to fetch during
|
|
694
|
+
each polling cycle
|
|
695
|
+
"""
|
|
696
|
+
# Remember if device notifications listener has been started already
|
|
697
|
+
self._alert_simulation_start_listener_back = self.listener_started
|
|
698
|
+
# And then stop it
|
|
699
|
+
self.close()
|
|
700
|
+
|
|
701
|
+
# Start the task
|
|
702
|
+
self._alert_simulation_task = asyncio.create_task(
|
|
703
|
+
self._simulate_alerts_from_history(interval, history_depth)
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
async def stop_simulating_alerts_from_history(self):
|
|
707
|
+
"""
|
|
708
|
+
Stops the task simulating device alerts from history entries.
|
|
709
|
+
|
|
710
|
+
The listener for device notifications will be started back, if it was
|
|
711
|
+
running when simulation has been started.
|
|
712
|
+
"""
|
|
713
|
+
# Stop the task simulating the device alerts from history if it was
|
|
714
|
+
# running
|
|
715
|
+
if self._alert_simulation_task:
|
|
716
|
+
self._alert_simulation_task.cancel()
|
|
717
|
+
self._alert_simulation_task = None
|
|
718
|
+
|
|
719
|
+
# Start device notifications listener back if it was running when
|
|
720
|
+
# simulated alerts have been enabled
|
|
721
|
+
if self._alert_simulation_start_listener_back:
|
|
722
|
+
await self.listen()
|
|
723
|
+
|
|
724
|
+
async def _simulate_alerts_from_history(self, interval, history_depth):
|
|
725
|
+
"""
|
|
726
|
+
Periodically fetches history entries from the device and simulates
|
|
727
|
+
device alerts off of those.
|
|
728
|
+
|
|
729
|
+
Only the history entries occur after the process is started are
|
|
730
|
+
handled, to avoid triggering callbacks retrospectively.
|
|
731
|
+
|
|
732
|
+
See :method:`start_simulating_alerts_from_history` for the parameters.
|
|
733
|
+
"""
|
|
734
|
+
last_history_ts = None
|
|
735
|
+
|
|
736
|
+
_LOGGER.debug(
|
|
737
|
+
'Simulating device alerts from history:'
|
|
738
|
+
' interval %s, history depth %s',
|
|
739
|
+
interval, history_depth
|
|
740
|
+
)
|
|
741
|
+
while True:
|
|
742
|
+
# Retrieve the history entries of the specified amount - full
|
|
743
|
+
# history retrieval might be an unnecessary long operation
|
|
744
|
+
history = await self.history(count=history_depth)
|
|
745
|
+
|
|
746
|
+
# Initial iteration where no timestamp of most recent history entry
|
|
747
|
+
# is recorded - do that and skip to next iteration, since it isn't
|
|
748
|
+
# yet known what entries would be considered as new ones
|
|
749
|
+
if not last_history_ts:
|
|
750
|
+
# First entry in the list is assumed to be the most recent one
|
|
751
|
+
last_history_ts = history[0].datetime
|
|
752
|
+
_LOGGER.debug(
|
|
753
|
+
'Initial time stamp of last history entry: %s',
|
|
754
|
+
last_history_ts
|
|
755
|
+
)
|
|
756
|
+
continue
|
|
757
|
+
|
|
758
|
+
# Process history entries from older to newer to preserve the order
|
|
759
|
+
# of happenings
|
|
760
|
+
for item in reversed(history):
|
|
761
|
+
# Process only the entries newer than one been recorded as most
|
|
762
|
+
# recent one
|
|
763
|
+
if item.datetime > last_history_ts:
|
|
764
|
+
_LOGGER.debug(
|
|
765
|
+
'Found newer history entry: %s, simulating alert',
|
|
766
|
+
repr(item)
|
|
767
|
+
)
|
|
768
|
+
# Send the history entry down the device notification code
|
|
769
|
+
# as alert, as if it came from the device and its
|
|
770
|
+
# notifications port
|
|
771
|
+
self._handle_alert(
|
|
772
|
+
(self._host, self._notifications_port),
|
|
773
|
+
item.as_device_alert()
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
# Record the entry as most recent one
|
|
777
|
+
last_history_ts = item.datetime
|
|
778
|
+
_LOGGER.debug(
|
|
779
|
+
'Time stamp of last history entry: %s', last_history_ts
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
# Sleep to next iteration
|
|
783
|
+
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
|