pyg90alarm 1.11.0__py3-none-any.whl → 1.12.0__py3-none-any.whl
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/alarm.py +170 -27
- pyg90alarm/base_cmd.py +38 -65
- pyg90alarm/const.py +29 -0
- pyg90alarm/device_notifications.py +99 -73
- pyg90alarm/discovery.py +11 -43
- pyg90alarm/entities/sensor.py +20 -0
- pyg90alarm/history.py +174 -12
- pyg90alarm/targeted_discovery.py +8 -43
- {pyg90alarm-1.11.0.dist-info → pyg90alarm-1.12.0.dist-info}/METADATA +13 -6
- {pyg90alarm-1.11.0.dist-info → pyg90alarm-1.12.0.dist-info}/RECORD +13 -13
- {pyg90alarm-1.11.0.dist-info → pyg90alarm-1.12.0.dist-info}/WHEEL +1 -1
- {pyg90alarm-1.11.0.dist-info → pyg90alarm-1.12.0.dist-info}/LICENSE +0 -0
- {pyg90alarm-1.11.0.dist-info → pyg90alarm-1.12.0.dist-info}/top_level.txt +0 -0
pyg90alarm/alarm.py
CHANGED
|
@@ -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)
|
pyg90alarm/base_cmd.py
CHANGED
|
@@ -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':
|
pyg90alarm/const.py
CHANGED
|
@@ -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
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
"""
|
|
22
22
|
Implements support for notifications/alerts sent by G90 alarm panel.
|
|
23
23
|
"""
|
|
24
|
-
|
|
25
24
|
import json
|
|
26
25
|
import logging
|
|
27
26
|
from collections import namedtuple
|
|
@@ -34,6 +33,7 @@ from .const import (
|
|
|
34
33
|
G90AlertStateChangeTypes,
|
|
35
34
|
G90ArmDisarmTypes,
|
|
36
35
|
G90AlertSources,
|
|
36
|
+
G90AlertStates,
|
|
37
37
|
)
|
|
38
38
|
|
|
39
39
|
|
|
@@ -43,7 +43,7 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
43
43
|
class G90Message(namedtuple('G90Message',
|
|
44
44
|
['code', 'data'])):
|
|
45
45
|
"""
|
|
46
|
-
|
|
46
|
+
Represents the message received from the device.
|
|
47
47
|
|
|
48
48
|
:meta private:
|
|
49
49
|
"""
|
|
@@ -52,7 +52,7 @@ class G90Message(namedtuple('G90Message',
|
|
|
52
52
|
class G90Notification(namedtuple('G90Notification',
|
|
53
53
|
['kind', 'data'])):
|
|
54
54
|
"""
|
|
55
|
-
|
|
55
|
+
Represents the notification received from the device.
|
|
56
56
|
|
|
57
57
|
:meta private:
|
|
58
58
|
"""
|
|
@@ -61,7 +61,7 @@ class G90Notification(namedtuple('G90Notification',
|
|
|
61
61
|
class G90ZoneInfo(namedtuple('G90ZoneInfo',
|
|
62
62
|
['idx', 'name'])):
|
|
63
63
|
"""
|
|
64
|
-
|
|
64
|
+
Represents zone details received from the device.
|
|
65
65
|
|
|
66
66
|
:meta private:
|
|
67
67
|
"""
|
|
@@ -70,7 +70,7 @@ class G90ZoneInfo(namedtuple('G90ZoneInfo',
|
|
|
70
70
|
class G90ArmDisarmInfo(namedtuple('G90ArmDisarmInfo',
|
|
71
71
|
['state'])):
|
|
72
72
|
"""
|
|
73
|
-
|
|
73
|
+
Represents the arm/disarm state received from the device.
|
|
74
74
|
|
|
75
75
|
:meta private:
|
|
76
76
|
"""
|
|
@@ -81,46 +81,31 @@ class G90DeviceAlert(namedtuple('G90DeviceAlert',
|
|
|
81
81
|
'zone_name', 'device_id', 'unix_time',
|
|
82
82
|
'resv4', 'other'])):
|
|
83
83
|
"""
|
|
84
|
-
|
|
84
|
+
Represents alert received from the device.
|
|
85
85
|
|
|
86
86
|
:meta private:
|
|
87
87
|
"""
|
|
88
88
|
|
|
89
89
|
|
|
90
|
-
class
|
|
90
|
+
class G90DeviceNotifications:
|
|
91
91
|
"""
|
|
92
92
|
tbd
|
|
93
|
-
|
|
94
|
-
:meta private:
|
|
95
93
|
"""
|
|
96
|
-
def __init__(self,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
self._armdisarm_cb = armdisarm_cb
|
|
102
|
-
self._sensor_cb = sensor_cb
|
|
103
|
-
self._door_open_close_cb = door_open_close_cb
|
|
104
|
-
self._alarm_cb = alarm_cb
|
|
105
|
-
|
|
106
|
-
def connection_made(self, transport):
|
|
107
|
-
"""
|
|
108
|
-
tbd
|
|
109
|
-
"""
|
|
110
|
-
|
|
111
|
-
def connection_lost(self, exc):
|
|
112
|
-
"""
|
|
113
|
-
tbd
|
|
114
|
-
"""
|
|
94
|
+
def __init__(self, port, host):
|
|
95
|
+
# pylint: disable=too-many-arguments
|
|
96
|
+
self._notification_transport = None
|
|
97
|
+
self._notifications_host = host
|
|
98
|
+
self._notifications_port = port
|
|
115
99
|
|
|
116
100
|
def _handle_notification(self, addr, notification):
|
|
117
101
|
# Sensor activity notification
|
|
118
102
|
if notification.kind == G90NotificationTypes.SENSOR_ACTIVITY:
|
|
119
103
|
g90_zone_info = G90ZoneInfo(*notification.data)
|
|
120
104
|
_LOGGER.debug('Sensor notification: %s', g90_zone_info)
|
|
121
|
-
G90Callback.invoke(
|
|
122
|
-
|
|
123
|
-
|
|
105
|
+
G90Callback.invoke(
|
|
106
|
+
self.on_sensor_activity,
|
|
107
|
+
g90_zone_info.idx, g90_zone_info.name
|
|
108
|
+
)
|
|
124
109
|
return
|
|
125
110
|
|
|
126
111
|
# Arm/disarm notification
|
|
@@ -131,8 +116,7 @@ class G90DeviceNotificationProtocol:
|
|
|
131
116
|
state = G90ArmDisarmTypes(g90_armdisarm_info.state)
|
|
132
117
|
_LOGGER.debug('Arm/disarm notification: %s',
|
|
133
118
|
state)
|
|
134
|
-
G90Callback.invoke(self.
|
|
135
|
-
state)
|
|
119
|
+
G90Callback.invoke(self.on_armdisarm, state)
|
|
136
120
|
return
|
|
137
121
|
|
|
138
122
|
_LOGGER.warning('Unknown notification received from %s:%s:'
|
|
@@ -141,14 +125,30 @@ class G90DeviceNotificationProtocol:
|
|
|
141
125
|
|
|
142
126
|
def _handle_alert(self, addr, alert):
|
|
143
127
|
if alert.type == G90AlertTypes.DOOR_OPEN_CLOSE:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
128
|
+
if alert.state in (
|
|
129
|
+
G90AlertStates.DOOR_OPEN, G90AlertStates.DOOR_CLOSE
|
|
130
|
+
):
|
|
131
|
+
is_open = (
|
|
132
|
+
alert.source == G90AlertSources.SENSOR
|
|
133
|
+
and alert.state == G90AlertStates.DOOR_OPEN # noqa: W503
|
|
134
|
+
) or alert.source == G90AlertSources.DOORBELL
|
|
135
|
+
_LOGGER.debug('Door open_close alert: %s', alert)
|
|
136
|
+
G90Callback.invoke(
|
|
137
|
+
self.on_door_open_close,
|
|
138
|
+
alert.event_id, alert.zone_name, is_open
|
|
139
|
+
)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
if (
|
|
143
|
+
alert.source == G90AlertSources.SENSOR
|
|
144
|
+
and alert.state == G90AlertStates.LOW_BATTERY # noqa: W503
|
|
145
|
+
):
|
|
146
|
+
_LOGGER.debug('Low battery alert: %s', alert)
|
|
147
|
+
G90Callback.invoke(
|
|
148
|
+
self.on_low_battery,
|
|
149
|
+
alert.event_id, alert.zone_name
|
|
150
|
+
)
|
|
151
|
+
return
|
|
152
152
|
|
|
153
153
|
if alert.type == G90AlertTypes.STATE_CHANGE:
|
|
154
154
|
# Define the mapping between device state received in the alert, to
|
|
@@ -168,21 +168,36 @@ class G90DeviceNotificationProtocol:
|
|
|
168
168
|
# We received the device state change related to arm/disarm,
|
|
169
169
|
# invoke the corresponding callback
|
|
170
170
|
_LOGGER.debug('Arm/disarm state change: %s', state)
|
|
171
|
-
G90Callback.invoke(self.
|
|
171
|
+
G90Callback.invoke(self.on_armdisarm, state)
|
|
172
172
|
return
|
|
173
173
|
|
|
174
174
|
if alert.type == G90AlertTypes.ALARM:
|
|
175
175
|
_LOGGER.debug('Alarm: %s', alert.zone_name)
|
|
176
|
-
G90Callback.invoke(
|
|
176
|
+
G90Callback.invoke(
|
|
177
|
+
self.on_alarm,
|
|
178
|
+
alert.event_id, alert.zone_name
|
|
179
|
+
)
|
|
177
180
|
return
|
|
178
181
|
|
|
179
182
|
_LOGGER.warning('Unknown alert received from %s:%s:'
|
|
180
183
|
' type %s, data %s',
|
|
181
184
|
addr[0], addr[1], alert.type, alert)
|
|
182
185
|
|
|
186
|
+
# Implementation of datagram protocol,
|
|
187
|
+
# https://docs.python.org/3/library/asyncio-protocol.html#datagram-protocols
|
|
188
|
+
def connection_made(self, transport):
|
|
189
|
+
"""
|
|
190
|
+
Invoked when connection from the device is made.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
def connection_lost(self, exc):
|
|
194
|
+
"""
|
|
195
|
+
Same but when the connection is lost.
|
|
196
|
+
"""
|
|
197
|
+
|
|
183
198
|
def datagram_received(self, data, addr): # pylint:disable=R0911
|
|
184
199
|
"""
|
|
185
|
-
|
|
200
|
+
Invoked from datagram is received from the device.
|
|
186
201
|
"""
|
|
187
202
|
s_data = data.decode('utf-8')
|
|
188
203
|
if not s_data.endswith('\0'):
|
|
@@ -228,37 +243,34 @@ class G90DeviceNotificationProtocol:
|
|
|
228
243
|
_LOGGER.warning('Unknown message received from %s:%s: %s',
|
|
229
244
|
addr[0], addr[1], message)
|
|
230
245
|
|
|
246
|
+
async def on_armdisarm(self, state):
|
|
247
|
+
"""
|
|
248
|
+
Invoked when device is armed or disarmed.
|
|
249
|
+
"""
|
|
231
250
|
|
|
232
|
-
|
|
233
|
-
"""
|
|
234
|
-
tbd
|
|
235
|
-
"""
|
|
236
|
-
def __init__(self, port, host,
|
|
237
|
-
armdisarm_cb=None, sensor_cb=None,
|
|
238
|
-
door_open_close_cb=None, alarm_cb=None):
|
|
239
|
-
# pylint: disable=too-many-arguments
|
|
240
|
-
self._notification_transport = None
|
|
241
|
-
self._host = host
|
|
242
|
-
self._port = port
|
|
243
|
-
self._armdisarm_cb = armdisarm_cb
|
|
244
|
-
self._sensor_cb = sensor_cb
|
|
245
|
-
self._door_open_close_cb = door_open_close_cb
|
|
246
|
-
self._alarm_cb = alarm_cb
|
|
247
|
-
|
|
248
|
-
def proto_factory(self):
|
|
251
|
+
async def on_sensor_activity(self, idx, name):
|
|
249
252
|
"""
|
|
250
|
-
|
|
253
|
+
Invoked on sensor activity.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
async def on_door_open_close(self, event_id, zone_name, is_open):
|
|
257
|
+
"""
|
|
258
|
+
Invoked when door sensor reports it opened or closed.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
async def on_low_battery(self, event_id, zone_name):
|
|
262
|
+
"""
|
|
263
|
+
Invoked when a sensor reports it is low on battery.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
async def on_alarm(self, event_id, zone_name):
|
|
267
|
+
"""
|
|
268
|
+
Invoked when device triggers the alarm.
|
|
251
269
|
"""
|
|
252
|
-
return G90DeviceNotificationProtocol(
|
|
253
|
-
armdisarm_cb=self._armdisarm_cb,
|
|
254
|
-
sensor_cb=self._sensor_cb,
|
|
255
|
-
door_open_close_cb=self._door_open_close_cb,
|
|
256
|
-
alarm_cb=self._alarm_cb
|
|
257
|
-
)
|
|
258
270
|
|
|
259
271
|
async def listen(self):
|
|
260
272
|
"""
|
|
261
|
-
|
|
273
|
+
Listens for notifications/alers from the device.
|
|
262
274
|
"""
|
|
263
275
|
try:
|
|
264
276
|
loop = asyncio.get_running_loop()
|
|
@@ -266,15 +278,29 @@ class G90DeviceNotifications:
|
|
|
266
278
|
loop = asyncio.get_event_loop()
|
|
267
279
|
|
|
268
280
|
_LOGGER.debug('Creating UDP endpoint for %s:%s',
|
|
269
|
-
self.
|
|
281
|
+
self._notifications_host,
|
|
282
|
+
self._notifications_port)
|
|
270
283
|
(self._notification_transport,
|
|
271
284
|
_protocol) = await loop.create_datagram_endpoint(
|
|
272
|
-
self
|
|
273
|
-
local_addr=(
|
|
285
|
+
lambda: self,
|
|
286
|
+
local_addr=(
|
|
287
|
+
self._notifications_host, self._notifications_port
|
|
288
|
+
))
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def listener_started(self):
|
|
292
|
+
"""
|
|
293
|
+
Indicates if the listener of the device notifications has been started.
|
|
294
|
+
|
|
295
|
+
:rtype: bool
|
|
296
|
+
"""
|
|
297
|
+
return self._notification_transport is not None
|
|
274
298
|
|
|
275
299
|
def close(self):
|
|
276
300
|
"""
|
|
277
|
-
|
|
301
|
+
Closes the listener.
|
|
278
302
|
"""
|
|
279
303
|
if self._notification_transport:
|
|
304
|
+
_LOGGER.debug('No longer listening for device notifications')
|
|
280
305
|
self._notification_transport.close()
|
|
306
|
+
self._notification_transport = None
|
pyg90alarm/discovery.py
CHANGED
|
@@ -32,34 +32,28 @@ from .const import G90Commands
|
|
|
32
32
|
_LOGGER = logging.getLogger(__name__)
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
class
|
|
35
|
+
class G90Discovery(G90BaseCommand):
|
|
36
36
|
"""
|
|
37
37
|
tbd
|
|
38
|
-
|
|
39
|
-
:meta private:
|
|
40
38
|
"""
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
tbd
|
|
44
|
-
"""
|
|
45
|
-
self._parent = parent
|
|
46
|
-
|
|
47
|
-
def connection_made(self, transport):
|
|
48
|
-
"""
|
|
49
|
-
tbd
|
|
50
|
-
"""
|
|
51
|
-
|
|
52
|
-
def connection_lost(self, exc):
|
|
39
|
+
# pylint: disable=too-few-public-methods
|
|
40
|
+
def __init__(self, timeout=10, **kwargs):
|
|
53
41
|
"""
|
|
54
42
|
tbd
|
|
55
43
|
"""
|
|
44
|
+
# pylint: disable=too-many-arguments
|
|
45
|
+
super().__init__(code=G90Commands.GETHOSTINFO, timeout=timeout,
|
|
46
|
+
**kwargs)
|
|
47
|
+
self._discovered_devices = []
|
|
56
48
|
|
|
49
|
+
# Implementation of datagram protocol,
|
|
50
|
+
# https://docs.python.org/3/library/asyncio-protocol.html#datagram-protocols
|
|
57
51
|
def datagram_received(self, data, addr):
|
|
58
52
|
"""
|
|
59
53
|
tbd
|
|
60
54
|
"""
|
|
61
55
|
try:
|
|
62
|
-
ret = self.
|
|
56
|
+
ret = self.from_wire(data)
|
|
63
57
|
host_info = G90HostInfo(*ret)
|
|
64
58
|
_LOGGER.debug('Received from %s:%s: %s', addr[0], addr[1], ret)
|
|
65
59
|
res = {
|
|
@@ -69,31 +63,11 @@ class G90DiscoveryProtocol:
|
|
|
69
63
|
}
|
|
70
64
|
res.update(host_info._asdict())
|
|
71
65
|
_LOGGER.debug('Discovered device: %s', res)
|
|
72
|
-
self.
|
|
66
|
+
self.add_device(res)
|
|
73
67
|
|
|
74
68
|
except Exception as exc: # pylint: disable=broad-except
|
|
75
69
|
_LOGGER.warning('Got exception, ignoring: %s', exc)
|
|
76
70
|
|
|
77
|
-
def error_received(self, exc):
|
|
78
|
-
"""
|
|
79
|
-
tbd
|
|
80
|
-
"""
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
class G90Discovery(G90BaseCommand):
|
|
84
|
-
"""
|
|
85
|
-
tbd
|
|
86
|
-
"""
|
|
87
|
-
# pylint: disable=too-few-public-methods
|
|
88
|
-
def __init__(self, timeout=10, **kwargs):
|
|
89
|
-
"""
|
|
90
|
-
tbd
|
|
91
|
-
"""
|
|
92
|
-
# pylint: disable=too-many-arguments
|
|
93
|
-
super().__init__(code=G90Commands.GETHOSTINFO, timeout=timeout,
|
|
94
|
-
**kwargs)
|
|
95
|
-
self._discovered_devices = []
|
|
96
|
-
|
|
97
71
|
async def process(self):
|
|
98
72
|
"""
|
|
99
73
|
tbd
|
|
@@ -118,9 +92,3 @@ class G90Discovery(G90BaseCommand):
|
|
|
118
92
|
tbd
|
|
119
93
|
"""
|
|
120
94
|
self._discovered_devices.append(value)
|
|
121
|
-
|
|
122
|
-
def _proto_factory(self):
|
|
123
|
-
"""
|
|
124
|
-
tbd
|
|
125
|
-
"""
|
|
126
|
-
return G90DiscoveryProtocol(self)
|
pyg90alarm/entities/sensor.py
CHANGED
|
@@ -172,6 +172,7 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
172
172
|
self._subindex = subindex
|
|
173
173
|
self._occupancy = False
|
|
174
174
|
self._state_callback = None
|
|
175
|
+
self._low_battery_callback = None
|
|
175
176
|
self._proto_idx = proto_idx
|
|
176
177
|
self._extra_data = None
|
|
177
178
|
|
|
@@ -217,6 +218,25 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
217
218
|
"""
|
|
218
219
|
self._state_callback = value
|
|
219
220
|
|
|
221
|
+
@property
|
|
222
|
+
def low_battery_callback(self):
|
|
223
|
+
"""
|
|
224
|
+
Returns callback the sensor might have set for low battery condition.
|
|
225
|
+
|
|
226
|
+
:return: Sensor's low battery callback
|
|
227
|
+
:rtype: object
|
|
228
|
+
"""
|
|
229
|
+
return self._low_battery_callback
|
|
230
|
+
|
|
231
|
+
@low_battery_callback.setter
|
|
232
|
+
def low_battery_callback(self, value):
|
|
233
|
+
"""
|
|
234
|
+
Sets callback for the low battery condition reported by the sensor.
|
|
235
|
+
|
|
236
|
+
:param object value: Sensor's low battery callback
|
|
237
|
+
"""
|
|
238
|
+
self._low_battery_callback = value
|
|
239
|
+
|
|
220
240
|
@property
|
|
221
241
|
def occupancy(self):
|
|
222
242
|
"""
|
pyg90alarm/history.py
CHANGED
|
@@ -22,35 +22,197 @@
|
|
|
22
22
|
History protocol entity.
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
|
-
import
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
26
|
from collections import namedtuple
|
|
27
|
+
from .const import (
|
|
28
|
+
G90AlertTypes,
|
|
29
|
+
G90AlertSources,
|
|
30
|
+
G90AlertStates,
|
|
31
|
+
G90AlertStateChangeTypes,
|
|
32
|
+
G90HistoryStates,
|
|
33
|
+
)
|
|
34
|
+
from .device_notifications import G90DeviceAlert
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# The state of the incoming history entries are mixed of `G90AlertStates` and
|
|
38
|
+
# `G90AlertStateChangeTypes`, depending on entry type - the mapping
|
|
39
|
+
# consilidates them into unified `G90HistoryStates`. The latter enum can't be
|
|
40
|
+
# just an union of former two, since those have conflicting values
|
|
41
|
+
states_mapping = {
|
|
42
|
+
G90AlertStates.DOOR_CLOSE:
|
|
43
|
+
G90HistoryStates.DOOR_CLOSE,
|
|
44
|
+
G90AlertStates.DOOR_OPEN:
|
|
45
|
+
G90HistoryStates.DOOR_OPEN,
|
|
46
|
+
G90AlertStates.TAMPER:
|
|
47
|
+
G90HistoryStates.TAMPER,
|
|
48
|
+
G90AlertStates.LOW_BATTERY:
|
|
49
|
+
G90HistoryStates.LOW_BATTERY,
|
|
50
|
+
G90AlertStateChangeTypes.AC_POWER_FAILURE:
|
|
51
|
+
G90HistoryStates.AC_POWER_FAILURE,
|
|
52
|
+
G90AlertStateChangeTypes.AC_POWER_RECOVER:
|
|
53
|
+
G90HistoryStates.AC_POWER_RECOVER,
|
|
54
|
+
G90AlertStateChangeTypes.DISARM:
|
|
55
|
+
G90HistoryStates.DISARM,
|
|
56
|
+
G90AlertStateChangeTypes.ARM_AWAY:
|
|
57
|
+
G90HistoryStates.ARM_AWAY,
|
|
58
|
+
G90AlertStateChangeTypes.ARM_HOME:
|
|
59
|
+
G90HistoryStates.ARM_HOME,
|
|
60
|
+
G90AlertStateChangeTypes.LOW_BATTERY:
|
|
61
|
+
G90HistoryStates.LOW_BATTERY,
|
|
62
|
+
G90AlertStateChangeTypes.WIFI_CONNECTED:
|
|
63
|
+
G90HistoryStates.WIFI_CONNECTED,
|
|
64
|
+
G90AlertStateChangeTypes.WIFI_DISCONNECTED:
|
|
65
|
+
G90HistoryStates.WIFI_DISCONNECTED,
|
|
66
|
+
}
|
|
27
67
|
|
|
28
68
|
INCOMING_FIELDS = [
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
|
|
32
|
-
'
|
|
33
|
-
'param3',
|
|
69
|
+
'type',
|
|
70
|
+
'event_id',
|
|
71
|
+
'source',
|
|
72
|
+
'state',
|
|
34
73
|
'sensor_name',
|
|
35
74
|
'unix_time',
|
|
36
|
-
'
|
|
75
|
+
'other',
|
|
37
76
|
]
|
|
77
|
+
# Class representing the data incoming from the device
|
|
78
|
+
ProtocolData = namedtuple('ProtocolData', INCOMING_FIELDS)
|
|
38
79
|
|
|
39
80
|
|
|
40
|
-
class G90History
|
|
81
|
+
class G90History:
|
|
41
82
|
"""
|
|
42
83
|
tbd
|
|
43
84
|
"""
|
|
85
|
+
def __init__(self, *args, **kwargs):
|
|
86
|
+
self._protocol_data = ProtocolData(*args, **kwargs)
|
|
44
87
|
|
|
45
88
|
@property
|
|
46
89
|
def datetime(self):
|
|
47
90
|
"""
|
|
48
|
-
|
|
91
|
+
Date/time of the history entry.
|
|
92
|
+
|
|
93
|
+
:rtype: :class:`datetime.datetime`
|
|
94
|
+
"""
|
|
95
|
+
return datetime.fromtimestamp(
|
|
96
|
+
self._protocol_data.unix_time, tz=timezone.utc
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def type(self):
|
|
101
|
+
"""
|
|
102
|
+
Type of the history entry.
|
|
103
|
+
|
|
104
|
+
:rtype: :class:`.G90AlertTypes`
|
|
105
|
+
"""
|
|
106
|
+
return G90AlertTypes(self._protocol_data.type)
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def state(self):
|
|
110
|
+
"""
|
|
111
|
+
State for the history entry.
|
|
112
|
+
|
|
113
|
+
:rtype: :class:`.G90HistoryStates`
|
|
114
|
+
"""
|
|
115
|
+
# Door open/close type, mapped against `G90AlertStates` using `state`
|
|
116
|
+
# incoming field
|
|
117
|
+
if self.type == G90AlertTypes.DOOR_OPEN_CLOSE:
|
|
118
|
+
return G90HistoryStates(
|
|
119
|
+
states_mapping[G90AlertStates(self._protocol_data.state)]
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Device state change, mapped against `G90AlertStateChangeTypes` using
|
|
123
|
+
# `event_id` incoming field
|
|
124
|
+
if self.type == G90AlertTypes.STATE_CHANGE:
|
|
125
|
+
return G90HistoryStates(
|
|
126
|
+
states_mapping[
|
|
127
|
+
G90AlertStateChangeTypes(self._protocol_data.event_id)
|
|
128
|
+
]
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Alarm gets mapped to its counterpart in `G90HistoryStates`
|
|
132
|
+
if self.type == G90AlertTypes.ALARM:
|
|
133
|
+
return G90HistoryStates.ALARM
|
|
134
|
+
|
|
135
|
+
# Other types are mapped against `G90AlertStateChangeTypes`
|
|
136
|
+
return G90HistoryStates(
|
|
137
|
+
states_mapping[
|
|
138
|
+
G90AlertStateChangeTypes(self._protocol_data.event_id)
|
|
139
|
+
]
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def source(self):
|
|
49
144
|
"""
|
|
50
|
-
|
|
145
|
+
Source of the history entry.
|
|
146
|
+
|
|
147
|
+
:rtype: :class:`.G90AlertSources`
|
|
148
|
+
"""
|
|
149
|
+
# Device state changes or open/close events are mapped against
|
|
150
|
+
# `G90AlertSources` using `source` incoming field
|
|
151
|
+
if self.type in [
|
|
152
|
+
G90AlertTypes.STATE_CHANGE, G90AlertTypes.DOOR_OPEN_CLOSE
|
|
153
|
+
]:
|
|
154
|
+
return G90AlertSources(self._protocol_data.source)
|
|
155
|
+
|
|
156
|
+
# Alarm will have `SENSOR` as the source, since that is likely what
|
|
157
|
+
# triggered it
|
|
158
|
+
if self.type == G90AlertTypes.ALARM:
|
|
159
|
+
return G90AlertSources.SENSOR
|
|
160
|
+
|
|
161
|
+
# Other sources are assumed to be initiated by device itself
|
|
162
|
+
return G90AlertSources.DEVICE
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def sensor_name(self):
|
|
166
|
+
"""
|
|
167
|
+
Name of the sensor related to the history entry, might be empty if none
|
|
168
|
+
associated.
|
|
169
|
+
|
|
170
|
+
:rtype: str|None
|
|
171
|
+
"""
|
|
172
|
+
return self._protocol_data.sensor_name or None
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def sensor_idx(self):
|
|
176
|
+
"""
|
|
177
|
+
ID of the sensor related to the history entry, might be empty if none
|
|
178
|
+
associated.
|
|
179
|
+
|
|
180
|
+
:rtype: str|None
|
|
181
|
+
"""
|
|
182
|
+
# Sensor ID will only be available if entry source is a sensor
|
|
183
|
+
if self.source == G90AlertSources.SENSOR:
|
|
184
|
+
return self._protocol_data.event_id
|
|
185
|
+
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
def as_device_alert(self):
|
|
189
|
+
"""
|
|
190
|
+
Returns the history entry represented as device alert structure,
|
|
191
|
+
suitable for :meth:`G90DeviceNotifications._handle_alert`.
|
|
192
|
+
|
|
193
|
+
:rtype: :class:`.G90DeviceAlert`
|
|
194
|
+
"""
|
|
195
|
+
return G90DeviceAlert(
|
|
196
|
+
type=self._protocol_data.type,
|
|
197
|
+
event_id=self._protocol_data.event_id,
|
|
198
|
+
source=self._protocol_data.source,
|
|
199
|
+
state=self._protocol_data.state,
|
|
200
|
+
zone_name=self._protocol_data.sensor_name,
|
|
201
|
+
device_id=None,
|
|
202
|
+
unix_time=self._protocol_data.unix_time,
|
|
203
|
+
resv4=None,
|
|
204
|
+
other=self._protocol_data.other
|
|
205
|
+
)
|
|
51
206
|
|
|
52
207
|
def __repr__(self):
|
|
53
208
|
"""
|
|
54
|
-
|
|
209
|
+
Textural representation of the history entry.
|
|
210
|
+
|
|
211
|
+
:rtype: str
|
|
55
212
|
"""
|
|
56
|
-
return
|
|
213
|
+
return f'type={repr(self.type)}' \
|
|
214
|
+
+ f' source={repr(self.source)}' \
|
|
215
|
+
+ f' state={repr(self.state)}' \
|
|
216
|
+
+ f' sensor_name={self.sensor_name}' \
|
|
217
|
+
+ f' sensor_idx={self.sensor_idx}' \
|
|
218
|
+
+ f' datetime={repr(self.datetime)}'
|
pyg90alarm/targeted_discovery.py
CHANGED
|
@@ -55,29 +55,21 @@ class G90TargetedDiscoveryInfo(namedtuple('G90TargetedDiscoveryInfo',
|
|
|
55
55
|
"""
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
class
|
|
58
|
+
class G90TargetedDiscovery(G90Discovery):
|
|
59
59
|
"""
|
|
60
60
|
tbd
|
|
61
|
-
|
|
62
|
-
:meta private:
|
|
63
61
|
"""
|
|
64
|
-
def __init__(self, device_id, parent):
|
|
65
|
-
"""
|
|
66
|
-
tbd
|
|
67
|
-
"""
|
|
68
|
-
self._parent = parent
|
|
69
|
-
self._device_id = device_id
|
|
70
62
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
tbd
|
|
74
|
-
"""
|
|
75
|
-
|
|
76
|
-
def connection_lost(self, exc):
|
|
63
|
+
# pylint: disable=too-few-public-methods
|
|
64
|
+
def __init__(self, device_id, **kwargs):
|
|
77
65
|
"""
|
|
78
66
|
tbd
|
|
79
67
|
"""
|
|
68
|
+
super().__init__(**kwargs)
|
|
69
|
+
self._device_id = device_id
|
|
80
70
|
|
|
71
|
+
# Implementation of datagram protocol,
|
|
72
|
+
# https://docs.python.org/3/library/asyncio-protocol.html#datagram-protocols
|
|
81
73
|
def datagram_received(self, data, addr):
|
|
82
74
|
"""
|
|
83
75
|
tbd
|
|
@@ -95,39 +87,12 @@ class G90TargetedDiscoveryProtocol:
|
|
|
95
87
|
'port': addr[1]}
|
|
96
88
|
res.update(host_info._asdict())
|
|
97
89
|
_LOGGER.debug('Discovered device: %s', res)
|
|
98
|
-
self.
|
|
90
|
+
self.add_device(res)
|
|
99
91
|
except Exception as exc: # pylint: disable=broad-except
|
|
100
92
|
_LOGGER.warning('Got exception, ignoring: %s', exc)
|
|
101
93
|
|
|
102
|
-
def error_received(self, exc):
|
|
103
|
-
"""
|
|
104
|
-
tbd
|
|
105
|
-
"""
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
class G90TargetedDiscovery(G90Discovery):
|
|
109
|
-
"""
|
|
110
|
-
tbd
|
|
111
|
-
"""
|
|
112
|
-
|
|
113
|
-
# pylint: disable=too-few-public-methods
|
|
114
|
-
def __init__(self, device_id, **kwargs):
|
|
115
|
-
"""
|
|
116
|
-
tbd
|
|
117
|
-
"""
|
|
118
|
-
|
|
119
|
-
super().__init__(**kwargs)
|
|
120
|
-
self._device_id = device_id
|
|
121
|
-
|
|
122
94
|
def to_wire(self):
|
|
123
95
|
"""
|
|
124
96
|
tbd
|
|
125
97
|
"""
|
|
126
98
|
return bytes(f'IWTAC_PROBE_DEVICE,{self._device_id}\0', 'ascii')
|
|
127
|
-
|
|
128
|
-
def _proto_factory(self):
|
|
129
|
-
"""
|
|
130
|
-
tbd
|
|
131
|
-
"""
|
|
132
|
-
return G90TargetedDiscoveryProtocol(self._device_id,
|
|
133
|
-
self)
|
|
@@ -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
|
|
@@ -24,13 +24,13 @@ Requires-Python: >=3.7, <4
|
|
|
24
24
|
Description-Content-Type: text/x-rst
|
|
25
25
|
License-File: LICENSE
|
|
26
26
|
Provides-Extra: dev
|
|
27
|
-
Requires-Dist: check-manifest
|
|
27
|
+
Requires-Dist: check-manifest; extra == "dev"
|
|
28
28
|
Provides-Extra: docs
|
|
29
|
-
Requires-Dist: sphinx
|
|
30
|
-
Requires-Dist: sphinx-rtd-theme
|
|
29
|
+
Requires-Dist: sphinx; extra == "docs"
|
|
30
|
+
Requires-Dist: sphinx-rtd-theme; extra == "docs"
|
|
31
31
|
Provides-Extra: test
|
|
32
|
-
Requires-Dist: coverage
|
|
33
|
-
Requires-Dist: asynctest
|
|
32
|
+
Requires-Dist: coverage; extra == "test"
|
|
33
|
+
Requires-Dist: asynctest; extra == "test"
|
|
34
34
|
|
|
35
35
|
.. image:: https://github.com/hostcc/pyg90alarm/actions/workflows/main.yml/badge.svg?branch=master
|
|
36
36
|
:target: https://github.com/hostcc/pyg90alarm/tree/master
|
|
@@ -158,6 +158,13 @@ set the IP address allocation up.
|
|
|
158
158
|
status will not be reflected and those will always be reported as inactive,
|
|
159
159
|
since there is no way to read their state in a polled manner.
|
|
160
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
|
+
|
|
161
168
|
Quick start
|
|
162
169
|
===========
|
|
163
170
|
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
pyg90alarm/__init__.py,sha256=zidYApReScSFZCpC9Tk7pdsBNPMql6XiUtt-O7l3D5M,1381
|
|
2
|
-
pyg90alarm/alarm.py,sha256=
|
|
3
|
-
pyg90alarm/base_cmd.py,sha256=
|
|
2
|
+
pyg90alarm/alarm.py,sha256=IsNpnPyBh90lTP9L03R1GJ608Ld6ZgUsJ7D5nS3WpCk,29157
|
|
3
|
+
pyg90alarm/base_cmd.py,sha256=rjpjIzEgtI5mUvmurBasrt9psYKdbIEsivnx8QboivY,8561
|
|
4
4
|
pyg90alarm/callback.py,sha256=zg698TCjjYhjAMk770J9CZp8-dDbX0Zj5wtoC6axq6w,4033
|
|
5
5
|
pyg90alarm/config.py,sha256=FiYjiz_WrDH2OEqHyUJXZDDK7v1fLAUpZcQ3JRMmmX0,1974
|
|
6
|
-
pyg90alarm/const.py,sha256=
|
|
7
|
-
pyg90alarm/device_notifications.py,sha256=
|
|
8
|
-
pyg90alarm/discovery.py,sha256=
|
|
6
|
+
pyg90alarm/const.py,sha256=XP_x5w6quiKQceOJDpExXI86L5KbQLdTed4lbjlNth0,5760
|
|
7
|
+
pyg90alarm/device_notifications.py,sha256=JLdOIIwyoxQdqvIjuLj6_5Rz_W6slYeM8HBPzPIa1ew,10628
|
|
8
|
+
pyg90alarm/discovery.py,sha256=sPk2mRhc0IgpOIlC9kRlKCnlAfdXsT7TyWfHAc1bfqQ,3077
|
|
9
9
|
pyg90alarm/exceptions.py,sha256=eiOcRe7D18EIPyPFDNU9DdFgbnkwPmkiLl8lGPOhBNw,1475
|
|
10
|
-
pyg90alarm/history.py,sha256=
|
|
10
|
+
pyg90alarm/history.py,sha256=Ln3-v8hvagIXTmj_2iURE3oOsVLeQb8OMczUy-SKSRs,7039
|
|
11
11
|
pyg90alarm/host_info.py,sha256=fGGI2ZH6GVD0WhYT72rIELTbiIAmmPiT31eZkyVugwY,2571
|
|
12
12
|
pyg90alarm/host_status.py,sha256=PEPgpkfGNkUzKUgRpfPKldz5qq3_9lqBwX86Ld613vk,1406
|
|
13
13
|
pyg90alarm/paginated_cmd.py,sha256=7pXLAgFQHheByBpwRV-I1yEdZnm8hk6j2OMPZ_Wn-vE,3768
|
|
14
14
|
pyg90alarm/paginated_result.py,sha256=xweFfPLn1a2yYm5h0AxGoDCyDIoy0JkUC_tI80vsrLc,5246
|
|
15
|
-
pyg90alarm/targeted_discovery.py,sha256=
|
|
15
|
+
pyg90alarm/targeted_discovery.py,sha256=DPxNvs8qJfyIOq6KLBYBpqkHPCpHbMW70_gfNyvNAoY,3221
|
|
16
16
|
pyg90alarm/user_data_crc.py,sha256=RsQlbuXC4baD88hX4y0XdysmxEMtQkqkNVX_FhTLSmw,1467
|
|
17
17
|
pyg90alarm/definitions/__init__.py,sha256=s0NZnkW_gMH718DJbgez28z9WA231CyszUf1O_ojUiI,68
|
|
18
18
|
pyg90alarm/definitions/sensors.py,sha256=2Liap0stTT5qNmvsbP_7UscA21IYXQcSAKQLLm7YfHQ,19921
|
|
19
19
|
pyg90alarm/entities/__init__.py,sha256=hHb6AOiC4Tz--rOWiiICMdLaZDs1Tf_xpWk_HeS_gO4,66
|
|
20
20
|
pyg90alarm/entities/device.py,sha256=QbsQyIq2wFLjIH389zyD3d0CyME_rpG_ciD3srAdqXQ,2772
|
|
21
|
-
pyg90alarm/entities/sensor.py,sha256=
|
|
22
|
-
pyg90alarm-1.
|
|
23
|
-
pyg90alarm-1.
|
|
24
|
-
pyg90alarm-1.
|
|
25
|
-
pyg90alarm-1.
|
|
26
|
-
pyg90alarm-1.
|
|
21
|
+
pyg90alarm/entities/sensor.py,sha256=AkVj_qrK35K5SwSlAHXU9UXdBKVaJzV4TBcoeZUevn4,13942
|
|
22
|
+
pyg90alarm-1.12.0.dist-info/LICENSE,sha256=f884inRbeNv-O-hbwz62Ro_1J8xiHRTnJ2cCx6A0WvU,1070
|
|
23
|
+
pyg90alarm-1.12.0.dist-info/METADATA,sha256=8zrPZpzJVGK7UZ-It72WrJEpQst6S5BWVGjatOaLr_M,7611
|
|
24
|
+
pyg90alarm-1.12.0.dist-info/WHEEL,sha256=HiCZjzuy6Dw0hdX5R3LCFPDmFS4BWl8H-8W39XfmgX4,91
|
|
25
|
+
pyg90alarm-1.12.0.dist-info/top_level.txt,sha256=czHiGxYMyTk5QEDTDb0EpPiKqUMRa8zI4zx58Ii409M,11
|
|
26
|
+
pyg90alarm-1.12.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|