pyg90alarm 1.10.1__tar.gz → 1.12.1__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.
Files changed (57) hide show
  1. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/.github/workflows/main.yml +3 -0
  2. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/PKG-INFO +14 -2
  3. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/README.rst +7 -0
  4. pyg90alarm-1.12.1/docs/requirements.txt +10 -0
  5. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm/alarm.py +188 -27
  6. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm/base_cmd.py +38 -65
  7. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm/const.py +29 -0
  8. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm/definitions/sensors.py +241 -1
  9. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm/device_notifications.py +99 -73
  10. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm/discovery.py +11 -43
  11. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm/entities/sensor.py +20 -0
  12. pyg90alarm-1.12.1/src/pyg90alarm/history.py +218 -0
  13. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm/targeted_discovery.py +8 -43
  14. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm.egg-info/PKG-INFO +14 -2
  15. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/tests/test_alarm.py +141 -26
  16. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/tests/test_base_commands.py +4 -5
  17. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/tests/test_notifications.py +75 -26
  18. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/tox.ini +16 -8
  19. pyg90alarm-1.10.1/docs/requirements.txt +0 -5
  20. pyg90alarm-1.10.1/src/pyg90alarm/history.py +0 -56
  21. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/.github/CODEOWNERS +0 -0
  22. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/.gitignore +0 -0
  23. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/.pylintrc +0 -0
  24. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/.readthedocs.yaml +0 -0
  25. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/LICENSE +0 -0
  26. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/MANIFEST.in +0 -0
  27. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/docs/.DS_Store +0 -0
  28. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/docs/.gitignore +0 -0
  29. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/docs/api-docs.rst +0 -0
  30. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/docs/conf.py +0 -0
  31. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/docs/index.rst +0 -0
  32. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/docs/protocol.rst +0 -0
  33. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/pyproject.toml +0 -0
  34. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/setup.cfg +0 -0
  35. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/setup.py +0 -0
  36. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/sonar-project.properties +0 -0
  37. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm/__init__.py +0 -0
  38. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm/callback.py +0 -0
  39. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm/config.py +0 -0
  40. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm/definitions/__init__.py +0 -0
  41. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm/entities/__init__.py +0 -0
  42. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm/entities/device.py +0 -0
  43. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm/exceptions.py +0 -0
  44. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm/host_info.py +0 -0
  45. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm/host_status.py +0 -0
  46. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm/paginated_cmd.py +0 -0
  47. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm/paginated_result.py +0 -0
  48. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm/user_data_crc.py +0 -0
  49. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm.egg-info/SOURCES.txt +0 -0
  50. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm.egg-info/dependency_links.txt +0 -0
  51. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm.egg-info/requires.txt +0 -0
  52. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/src/pyg90alarm.egg-info/top_level.txt +0 -0
  53. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/tests/conftest.py +0 -0
  54. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/tests/device_mock.py +0 -0
  55. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/tests/test_callback.py +0 -0
  56. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/tests/test_discovery.py +0 -0
  57. {pyg90alarm-1.10.1 → pyg90alarm-1.12.1}/tests/test_paginated_commands.py +0 -0
@@ -31,6 +31,9 @@ jobs:
31
31
  - os: ubuntu-latest
32
32
  python: '3.11'
33
33
  toxenv: py
34
+ - os: ubuntu-latest
35
+ python: '3.12'
36
+ toxenv: py
34
37
  runs-on: ${{ matrix.os }}
35
38
  steps:
36
39
  - name: Checkout the code
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyg90alarm
3
- Version: 1.10.1
3
+ Version: 1.12.1
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
- License-File: LICENSE
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
- class G90Alarm: # pylint: disable=too-many-public-methods
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
- async def _internal_sensor_cb(self, idx, name, occupancy=True):
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_acitvity: %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 _internal_door_open_close_cb(self, idx, name, is_open):
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`:_internal_sensor_cb for arguments
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._internal_sensor_cb(idx, name, is_open)
505
+ await self.on_sensor_activity(event_id, zone_name, is_open)
491
506
  # Invoke user specified callback if any
492
- G90Callback.invoke(self._door_open_close_cb, idx, name, is_open)
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 _internal_armdisarm_cb(self, state):
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 _internal_alarm_cb(self, sensor_idx, sensor_name):
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,14 @@ 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(sensor_idx, sensor_name)
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
576
  # isn't found, only `extra_data` is skipped. That is to ensur the
560
577
  # important callback isn't filtered
561
578
  extra_data = sensor.extra_data if sensor else None
562
579
 
563
580
  G90Callback.invoke(
564
- self._alarm_cb, sensor_idx, sensor_name, extra_data
581
+ self._alarm_cb, event_id, zone_name, extra_data
565
582
  )
566
583
 
567
584
  @property
@@ -581,27 +598,49 @@ class G90Alarm: # pylint: disable=too-many-public-methods
581
598
  def alarm_callback(self, value):
582
599
  self._alarm_cb = value
583
600
 
584
- async def listen_device_notifications(
585
- self, host='0.0.0.0', port=NOTIFICATIONS_PORT
586
- ):
601
+ async def on_low_battery(self, event_id, zone_name):
602
+ """
603
+ Callback that invoked when the sensor reports on low battery. Fires
604
+ corresponding callback if set by the user with
605
+ `:property:G90Alarm.on_low_battery_callback`.
606
+ Please note the callback is for internal use by the class.
607
+
608
+ :param int: Index of the sensor triggered alarm
609
+ :param str: Sensor name
610
+ """
611
+ sensor = await self.find_sensor(event_id, zone_name)
612
+ if sensor:
613
+ # Invoke per-sensor callback if provided
614
+ G90Callback.invoke(sensor.low_battery_callback)
615
+
616
+ G90Callback.invoke(self._low_battery_cb, event_id, zone_name)
617
+
618
+ @property
619
+ def low_battery_callback(self):
620
+ """
621
+ Get or set low battery callback, the callback is invoked when sensor
622
+ the condition is reported by a sensor.
623
+
624
+ :type: .. py:function:: ()(idx, name)
625
+ """
626
+ return self._low_battery_cb
627
+
628
+ @low_battery_callback.setter
629
+ def low_battery_callback(self, value):
630
+ self._low_battery_cb = value
631
+
632
+ async def listen_device_notifications(self):
587
633
  """
588
634
  Starts internal listener for device notifications/alerts.
589
635
 
590
636
  """
591
- self._notifications = G90DeviceNotifications(
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()
637
+ await self.listen()
598
638
 
599
639
  def close_device_notifications(self):
600
640
  """
601
641
  Closes the listener for device notifications/alerts.
602
642
  """
603
- if self._notifications:
604
- self._notifications.close()
643
+ self.close()
605
644
 
606
645
  async def arm_away(self):
607
646
  """
@@ -638,3 +677,125 @@ class G90Alarm: # pylint: disable=too-many-public-methods
638
677
  @sms_alert_when_armed.setter
639
678
  def sms_alert_when_armed(self, value):
640
679
  self._sms_alert_when_armed = value
680
+
681
+ async def start_simulating_alerts_from_history(
682
+ self, interval=5, history_depth=5
683
+ ):
684
+ """
685
+ Starts the separate task to simulate device alerts from history
686
+ entries.
687
+
688
+ The listener for device notifications will be stopped, so device
689
+ notifications will not be processed thus resulting in possible
690
+ duplicated if those could be received from the network.
691
+
692
+ :param int interval: Interval (in seconds) between polling for newer
693
+ history entities
694
+ :param int history_depth: Amount of history entries to fetch during
695
+ each polling cycle
696
+ """
697
+ # Remember if device notifications listener has been started already
698
+ self._alert_simulation_start_listener_back = self.listener_started
699
+ # And then stop it
700
+ self.close()
701
+
702
+ # Start the task
703
+ self._alert_simulation_task = asyncio.create_task(
704
+ self._simulate_alerts_from_history(interval, history_depth)
705
+ )
706
+
707
+ async def stop_simulating_alerts_from_history(self):
708
+ """
709
+ Stops the task simulating device alerts from history entries.
710
+
711
+ The listener for device notifications will be started back, if it was
712
+ running when simulation has been started.
713
+ """
714
+ # Stop the task simulating the device alerts from history if it was
715
+ # running
716
+ if self._alert_simulation_task:
717
+ self._alert_simulation_task.cancel()
718
+ self._alert_simulation_task = None
719
+
720
+ # Start device notifications listener back if it was running when
721
+ # simulated alerts have been enabled
722
+ if self._alert_simulation_start_listener_back:
723
+ await self.listen()
724
+
725
+ async def _simulate_alerts_from_history(self, interval, history_depth):
726
+ """
727
+ Periodically fetches history entries from the device and simulates
728
+ device alerts off of those.
729
+
730
+ Only the history entries occur after the process is started are
731
+ handled, to avoid triggering callbacks retrospectively.
732
+
733
+ See :method:`start_simulating_alerts_from_history` for the parameters.
734
+ """
735
+ last_history_ts = None
736
+
737
+ _LOGGER.debug(
738
+ 'Simulating device alerts from history:'
739
+ ' interval %s, history depth %s',
740
+ interval, history_depth
741
+ )
742
+ while True:
743
+ try:
744
+ # Retrieve the history entries of the specified amount - full
745
+ # history retrieval might be an unnecessary long operation
746
+ history = await self.history(count=history_depth)
747
+
748
+ # Initial iteration where no timestamp of most recent history
749
+ # entry is recorded - do that and skip to next iteration, since
750
+ # it isn't yet known what entries would be considered as new
751
+ # ones.
752
+ # Empty history will skip recording the timestamp and the
753
+ # looping over entries below, effectively skipping to next
754
+ # iteration
755
+ if not last_history_ts and history:
756
+ # First entry in the list is assumed to be the most recent
757
+ # one
758
+ last_history_ts = history[0].datetime
759
+ _LOGGER.debug(
760
+ 'Initial time stamp of last history entry: %s',
761
+ last_history_ts
762
+ )
763
+ continue
764
+
765
+ # Process history entries from older to newer to preserve the
766
+ # order of happenings
767
+ for item in reversed(history):
768
+ # Process only the entries newer than one been recorded as
769
+ # most recent one
770
+ if item.datetime > last_history_ts:
771
+ _LOGGER.debug(
772
+ 'Found newer history entry: %s, simulating alert',
773
+ repr(item)
774
+ )
775
+ # Send the history entry down the device notification
776
+ # code as alert, as if it came from the device and its
777
+ # notifications port
778
+ self._handle_alert(
779
+ (self._host, self._notifications_port),
780
+ item.as_device_alert()
781
+ )
782
+
783
+ # Record the entry as most recent one
784
+ last_history_ts = item.datetime
785
+ _LOGGER.debug(
786
+ 'Time stamp of last history entry: %s',
787
+ last_history_ts
788
+ )
789
+ except (G90Error, G90TimeoutError) as exc:
790
+ _LOGGER.debug(
791
+ 'Error interacting with device, ignoring %s', repr(exc)
792
+ )
793
+ except Exception as exc:
794
+ _LOGGER.error(
795
+ 'Exception simulating device alerts from history: %s',
796
+ repr(exc)
797
+ )
798
+ raise exc
799
+
800
+ # Sleep to next iteration
801
+ 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
- def _proto_factory(self):
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
- return G90DeviceProtocol()
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._proto_factory,
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, protocol = await self._create_connection()
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
- protocol.future_data = loop.create_future()
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([protocol.future_data],
230
+ done, _ = await asyncio.wait([self._connection_result],
258
231
  timeout=self._timeout)
259
- if protocol.future_data in done:
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
- protocol.future_data.cancel()
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) = protocol.future_data.result()
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