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 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
- class G90Alarm: # pylint: disable=too-many-public-methods
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
- async def _internal_sensor_cb(self, idx, name, occupancy=True):
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 _internal_door_open_close_cb(self, idx, name, is_open):
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`:_internal_sensor_cb for arguments
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._internal_sensor_cb(idx, name, is_open)
504
+ await self.on_sensor_activity(event_id, zone_name, is_open)
491
505
  # Invoke user specified callback if any
492
- G90Callback.invoke(self._door_open_close_cb, idx, name, is_open)
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 _internal_armdisarm_cb(self, state):
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 _internal_alarm_cb(self, sensor_idx, sensor_name):
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(sensor_idx, sensor_name)
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, sensor_idx, sensor_name, extra_data
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 listen_device_notifications(
585
- self, host='0.0.0.0', port=NOTIFICATIONS_PORT
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._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()
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
- if self._notifications:
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
- 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':
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
- tbd
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
- tbd
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
- tbd
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
- tbd
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
- tbd
84
+ Represents alert received from the device.
85
85
 
86
86
  :meta private:
87
87
  """
88
88
 
89
89
 
90
- class G90DeviceNotificationProtocol:
90
+ class G90DeviceNotifications:
91
91
  """
92
92
  tbd
93
-
94
- :meta private:
95
93
  """
96
- def __init__(self, armdisarm_cb=None, sensor_cb=None,
97
- door_open_close_cb=None, alarm_cb=None):
98
- """
99
- tbd
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(self._sensor_cb,
122
- g90_zone_info.idx,
123
- g90_zone_info.name)
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._armdisarm_cb,
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
- is_open = (
145
- alert.source == G90AlertSources.SENSOR and alert.state == 1
146
- ) or alert.source == G90AlertSources.DOORBELL
147
- _LOGGER.debug('Door open_close alert: %s', alert)
148
- G90Callback.invoke(self._door_open_close_cb,
149
- alert.event_id, alert.zone_name,
150
- is_open)
151
- return
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._armdisarm_cb, state)
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(self._alarm_cb, alert.event_id, alert.zone_name)
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
- tbd
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
- class G90DeviceNotifications:
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
- tbd
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
- tbd
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._host, self._port)
281
+ self._notifications_host,
282
+ self._notifications_port)
270
283
  (self._notification_transport,
271
284
  _protocol) = await loop.create_datagram_endpoint(
272
- self.proto_factory,
273
- local_addr=(self._host, self._port))
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
- tbd
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 G90DiscoveryProtocol:
35
+ class G90Discovery(G90BaseCommand):
36
36
  """
37
37
  tbd
38
-
39
- :meta private:
40
38
  """
41
- def __init__(self, parent):
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._parent.from_wire(data)
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._parent.add_device(res)
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)
@@ -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 time
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
- 'log_type', # (1 or 3 - alarm, 2 or 4 - notification)
30
- 'param1', # (type 1: 1 - SOS, 2 - tamper alarm; type 3 - device ID; type
31
- # 2 - 5 stayarm, 3 - disarm, 4 - awayarm )
32
- 'param2', # (type 3: device type)
33
- 'param3',
69
+ 'type',
70
+ 'event_id',
71
+ 'source',
72
+ 'state',
34
73
  'sensor_name',
35
74
  'unix_time',
36
- 'rest',
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(namedtuple('G90History', INCOMING_FIELDS)):
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
- tbd
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
- return time.ctime(self.unix_time)
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
- tbd
209
+ Textural representation of the history entry.
210
+
211
+ :rtype: str
55
212
  """
56
- return super().__repr__() + f'(datetime={str(self.datetime)})'
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)}'
@@ -55,29 +55,21 @@ class G90TargetedDiscoveryInfo(namedtuple('G90TargetedDiscoveryInfo',
55
55
  """
56
56
 
57
57
 
58
- class G90TargetedDiscoveryProtocol:
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
- def connection_made(self, transport):
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._parent.add_device(res)
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.11.0
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 ; extra == 'dev'
27
+ Requires-Dist: check-manifest; extra == "dev"
28
28
  Provides-Extra: docs
29
- Requires-Dist: sphinx ; extra == 'docs'
30
- Requires-Dist: sphinx-rtd-theme ; extra == 'docs'
29
+ Requires-Dist: sphinx; extra == "docs"
30
+ Requires-Dist: sphinx-rtd-theme; extra == "docs"
31
31
  Provides-Extra: test
32
- Requires-Dist: coverage ; extra == 'test'
33
- Requires-Dist: asynctest ; extra == 'test'
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=0SMGPe9J1550j_Yq-KxoFlKPjzVIZ5Z-RhsjUusUS9U,23528
3
- pyg90alarm/base_cmd.py,sha256=xuW-jjuScag1kAVPKNa2GUmL99UYeyu2n-C9ppLmOMo,8768
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=nhqrP9GqsU4VeEYHSW4NJTh6QO20L7uDea2reqDnqO4,5226
7
- pyg90alarm/device_notifications.py,sha256=wxDf0m_LOt6Sj_jG8cfTFZM3LNry2xrPwkFPa4lVscI,9315
8
- pyg90alarm/discovery.py,sha256=07TZIIGgPsRcW_iYpS-82lG18rotuykYqfGV-cE2eHc,3474
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=E8c8ucy5cscPSawIz4nXxV4HKdyhsoJda4Akvvt3k6I,1836
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=_SR_pBZxDkLNL5WA8LfGboQw2AH09G65ASn9EEtxlDs,3735
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=2d_671Kefx7wM6mWRQR79XyScEkkp3bk7HsA1Qnx75I,13354
22
- pyg90alarm-1.11.0.dist-info/LICENSE,sha256=f884inRbeNv-O-hbwz62Ro_1J8xiHRTnJ2cCx6A0WvU,1070
23
- pyg90alarm-1.11.0.dist-info/METADATA,sha256=4TEvcDDcPQcq7E0bHPGZ83NWBxl56gDL7I98goAmHvU,7211
24
- pyg90alarm-1.11.0.dist-info/WHEEL,sha256=cpQTJ5IWu9CdaPViMhC9YzF8gZuS5-vlfoFihTBC86A,91
25
- pyg90alarm-1.11.0.dist-info/top_level.txt,sha256=czHiGxYMyTk5QEDTDb0EpPiKqUMRa8zI4zx58Ii409M,11
26
- pyg90alarm-1.11.0.dist-info/RECORD,,
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (70.1.0)
2
+ Generator: setuptools (72.2.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5