pyg90alarm 1.16.0__tar.gz → 1.17.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/PKG-INFO +1 -1
  2. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/alarm.py +96 -1
  3. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/const.py +24 -0
  4. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/device_notifications.py +135 -34
  5. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/history.py +96 -40
  6. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm.egg-info/PKG-INFO +1 -1
  7. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm.egg-info/SOURCES.txt +1 -0
  8. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/tests/test_alarm.py +102 -124
  9. pyg90alarm-1.17.0/tests/test_history.py +226 -0
  10. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/tests/test_notifications.py +58 -0
  11. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/.github/CODEOWNERS +0 -0
  12. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/.github/workflows/main.yml +0 -0
  13. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/.gitignore +0 -0
  14. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/.pylintrc +0 -0
  15. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/.readthedocs.yaml +0 -0
  16. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/LICENSE +0 -0
  17. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/MANIFEST.in +0 -0
  18. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/README.rst +0 -0
  19. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/docs/.DS_Store +0 -0
  20. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/docs/.gitignore +0 -0
  21. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/docs/api-docs.rst +0 -0
  22. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/docs/conf.py +0 -0
  23. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/docs/index.rst +0 -0
  24. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/docs/protocol.rst +0 -0
  25. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/docs/requirements.txt +0 -0
  26. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/pyproject.toml +0 -0
  27. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/setup.cfg +0 -0
  28. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/setup.py +0 -0
  29. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/sonar-project.properties +0 -0
  30. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/__init__.py +0 -0
  31. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/base_cmd.py +0 -0
  32. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/callback.py +0 -0
  33. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/config.py +0 -0
  34. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/definitions/__init__.py +0 -0
  35. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/definitions/sensors.py +0 -0
  36. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/discovery.py +0 -0
  37. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/entities/__init__.py +0 -0
  38. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/entities/device.py +0 -0
  39. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/entities/sensor.py +0 -0
  40. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/exceptions.py +0 -0
  41. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/host_info.py +0 -0
  42. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/host_status.py +0 -0
  43. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/paginated_cmd.py +0 -0
  44. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/paginated_result.py +0 -0
  45. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/py.typed +0 -0
  46. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/targeted_discovery.py +0 -0
  47. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm/user_data_crc.py +0 -0
  48. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm.egg-info/dependency_links.txt +0 -0
  49. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm.egg-info/requires.txt +0 -0
  50. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/src/pyg90alarm.egg-info/top_level.txt +0 -0
  51. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/tests/__init__.py +0 -0
  52. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/tests/conftest.py +0 -0
  53. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/tests/device_mock.py +0 -0
  54. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/tests/test_base_commands.py +0 -0
  55. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/tests/test_discovery.py +0 -0
  56. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/tests/test_paginated_commands.py +0 -0
  57. {pyg90alarm-1.16.0 → pyg90alarm-1.17.0}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyg90alarm
3
- Version: 1.16.0
3
+ Version: 1.17.0
4
4
  Summary: G90 Alarm system protocol
5
5
  Home-page: https://github.com/hostcc/pyg90alarm
6
6
  Author: Ilia Sotnikov
@@ -63,6 +63,7 @@ from .const import (
63
63
  LOCAL_NOTIFICATIONS_HOST,
64
64
  LOCAL_NOTIFICATIONS_PORT,
65
65
  G90ArmDisarmTypes,
66
+ G90RemoteButtonStates,
66
67
  )
67
68
  from .base_cmd import (G90BaseCommand, G90BaseCommandData)
68
69
  from .paginated_result import G90PaginatedResult, G90PaginatedResponse
@@ -109,8 +110,18 @@ if TYPE_CHECKING:
109
110
  Callable[[G90ArmDisarmTypes], None],
110
111
  Callable[[G90ArmDisarmTypes], Coroutine[None, None, None]]
111
112
  ]
113
+ SosCallback = Union[
114
+ Callable[[int, str, bool], None],
115
+ Callable[[int, str, bool], Coroutine[None, None, None]]
116
+ ]
117
+ RemoteButtonPressCallback = Union[
118
+ Callable[[int, str, G90RemoteButtonStates], None],
119
+ Callable[
120
+ [int, str, G90RemoteButtonStates], Coroutine[None, None, None]
121
+ ]
122
+ ]
112
123
  # Sensor-related callbacks for `G90Sensor` class - despite that class
113
- # stores them, the invication is done by the `G90Alarm` class hence these
124
+ # stores them, the invocation is done by the `G90Alarm` class hence these
114
125
  # are defined here
115
126
  SensorStateCallback = Union[
116
127
  Callable[[bool], None],
@@ -157,6 +168,10 @@ class G90Alarm(G90DeviceNotifications):
157
168
  self._door_open_close_cb: Optional[DoorOpenCloseCallback] = None
158
169
  self._alarm_cb: Optional[AlarmCallback] = None
159
170
  self._low_battery_cb: Optional[LowBatteryCallback] = None
171
+ self._sos_cb: Optional[SosCallback] = None
172
+ self._remote_button_press_cb: Optional[
173
+ RemoteButtonPressCallback
174
+ ] = None
160
175
  self._reset_occupancy_interval = reset_occupancy_interval
161
176
  self._alert_config: Optional[G90AlertConfigFlags] = None
162
177
  self._sms_alert_when_armed = False
@@ -671,6 +686,86 @@ class G90Alarm(G90DeviceNotifications):
671
686
  def low_battery_callback(self, value: LowBatteryCallback) -> None:
672
687
  self._low_battery_cb = value
673
688
 
689
+ async def on_sos(
690
+ self, event_id: int, zone_name: str, is_host_sos: bool
691
+ ) -> None:
692
+ """
693
+ Invoked when SOS alert is triggered. Fires corresponding callback if
694
+ set by the user with :attr:`.sos_callback`.
695
+
696
+ Please note the method is for internal use by the class.
697
+
698
+ :param event_id: Index of the sensor triggered alarm
699
+ :param zone_name: Sensor name
700
+ :param is_host_sos:
701
+ Flag indicating if the SOS alert is triggered by the panel itself
702
+ (host)
703
+ """
704
+ _LOGGER.debug('on_sos: %s %s %s', event_id, zone_name, is_host_sos)
705
+ G90Callback.invoke(self._sos_cb, event_id, zone_name, is_host_sos)
706
+
707
+ # Also report the event as alarm for unification, hard-coding the
708
+ # sensor name in case of host SOS
709
+ await self.on_alarm(event_id, 'Host SOS' if is_host_sos else zone_name)
710
+
711
+ if not is_host_sos:
712
+ # Also report the remote button press for SOS - the panel will not
713
+ # send corresponding alert
714
+ await self.on_remote_button_press(
715
+ event_id, zone_name, G90RemoteButtonStates.SOS
716
+ )
717
+
718
+ @property
719
+ def sos_callback(self) -> Optional[SosCallback]:
720
+ """
721
+ SOS callback, which is invoked when SOS alert is triggered.
722
+ """
723
+ return self._sos_cb
724
+
725
+ @sos_callback.setter
726
+ def sos_callback(self, value: SosCallback) -> None:
727
+ self._sos_cb = value
728
+
729
+ async def on_remote_button_press(
730
+ self, event_id: int, zone_name: str, button: G90RemoteButtonStates
731
+ ) -> None:
732
+ """
733
+ Invoked when remote button is pressed. Fires corresponding callback if
734
+ set by the user with :attr:`.remote_button_press_callback`.
735
+
736
+ Please note the method is for internal use by the class.
737
+
738
+ :param event_id: Index of the sensor triggered alarm
739
+ :param zone_name: Sensor name
740
+ :param button: The button pressed
741
+ """
742
+ _LOGGER.debug(
743
+ 'on_remote_button_press: %s %s %s', event_id, zone_name, button
744
+ )
745
+ G90Callback.invoke(
746
+ self._remote_button_press_cb, event_id, zone_name, button
747
+ )
748
+
749
+ # Also report the event as sensor activity for unification (remote is
750
+ # just a special type of the sensor)
751
+ await self.on_sensor_activity(event_id, zone_name, True)
752
+
753
+ @property
754
+ def remote_button_press_callback(
755
+ self
756
+ ) -> Optional[RemoteButtonPressCallback]:
757
+ """
758
+ Remote button press callback, which is invoked when remote button is
759
+ pressed.
760
+ """
761
+ return self._remote_button_press_cb
762
+
763
+ @remote_button_press_callback.setter
764
+ def remote_button_press_callback(
765
+ self, value: RemoteButtonPressCallback
766
+ ) -> None:
767
+ self._remote_button_press_cb = value
768
+
674
769
  async def listen_device_notifications(self) -> None:
675
770
  """
676
771
  Starts internal listener for device notifications/alerts.
@@ -166,7 +166,10 @@ class G90NotificationTypes(IntEnum):
166
166
  Defines types of notifications sent by the alarm panel.
167
167
  """
168
168
  ARM_DISARM = 1
169
+ SENSOR_ADDED = 4
169
170
  SENSOR_ACTIVITY = 5
171
+ DOOR_OPEN_WHEN_ARMING = 6
172
+ FIRMWARE_UPDATING = 8
170
173
 
171
174
 
172
175
  class G90ArmDisarmTypes(IntEnum):
@@ -184,8 +187,11 @@ class G90AlertTypes(IntEnum):
184
187
  """
185
188
  Defines types of alerts sent by the alarm panel.
186
189
  """
190
+ HOST_SOS = 1
187
191
  STATE_CHANGE = 2
188
192
  ALARM = 3
193
+ SENSOR_ACTIVITY = 4
194
+ # Retained for compatibility, deprecated
189
195
  DOOR_OPEN_CLOSE = 4
190
196
 
191
197
 
@@ -195,7 +201,10 @@ class G90AlertSources(IntEnum):
195
201
  """
196
202
  DEVICE = 0
197
203
  SENSOR = 1
204
+ REMOTE = 10
205
+ RFID = 11
198
206
  DOORBELL = 12
207
+ FINGERPRINT = 15
199
208
 
200
209
 
201
210
  class G90AlertStates(IntEnum):
@@ -204,6 +213,7 @@ class G90AlertStates(IntEnum):
204
213
  """
205
214
  DOOR_CLOSE = 0
206
215
  DOOR_OPEN = 1
216
+ SOS = 2
207
217
  TAMPER = 3
208
218
  LOW_BATTERY = 4
209
219
 
@@ -238,3 +248,17 @@ class G90HistoryStates(IntEnum):
238
248
  LOW_BATTERY = 10
239
249
  WIFI_CONNECTED = 11
240
250
  WIFI_DISCONNECTED = 12
251
+ REMOTE_BUTTON_ARM_AWAY = 13
252
+ REMOTE_BUTTON_ARM_HOME = 14
253
+ REMOTE_BUTTON_DISARM = 15
254
+ REMOTE_BUTTON_SOS = 16
255
+
256
+
257
+ class G90RemoteButtonStates(IntEnum):
258
+ """
259
+ Defines possible states for remote control buttons.
260
+ """
261
+ ARM_AWAY = 0
262
+ ARM_HOME = 1
263
+ DISARM = 2
264
+ SOS = 3
@@ -39,6 +39,7 @@ from .const import (
39
39
  G90ArmDisarmTypes,
40
40
  G90AlertSources,
41
41
  G90AlertStates,
42
+ G90RemoteButtonStates,
42
43
  )
43
44
 
44
45
  _LOGGER = logging.getLogger(__name__)
@@ -136,11 +137,13 @@ class G90DeviceNotifications(DatagramProtocol):
136
137
  # Sensor activity notification
137
138
  if notification.kind == G90NotificationTypes.SENSOR_ACTIVITY:
138
139
  g90_zone_info = G90ZoneInfo(*notification.data)
140
+
139
141
  _LOGGER.debug('Sensor notification: %s', g90_zone_info)
140
142
  G90Callback.invoke(
141
143
  self.on_sensor_activity,
142
144
  g90_zone_info.idx, g90_zone_info.name
143
145
  )
146
+
144
147
  return
145
148
 
146
149
  # Arm/disarm notification
@@ -149,18 +152,66 @@ class G90DeviceNotifications(DatagramProtocol):
149
152
  *notification.data)
150
153
  # Map the state received from the device to corresponding enum
151
154
  state = G90ArmDisarmTypes(g90_armdisarm_info.state)
155
+
152
156
  _LOGGER.debug('Arm/disarm notification: %s',
153
157
  state)
154
158
  G90Callback.invoke(self.on_armdisarm, state)
159
+
155
160
  return
156
161
 
157
162
  _LOGGER.warning('Unknown notification received from %s:%s:'
158
163
  ' kind %s, data %s',
159
164
  addr[0], addr[1], notification.kind, notification.data)
160
165
 
166
+ def _handle_alert_sensor_activity(self, alert: G90DeviceAlert) -> bool:
167
+ """
168
+ Handles sensor activity alert.
169
+ """
170
+ if alert.source == G90AlertSources.REMOTE:
171
+ _LOGGER.debug('Remote button press alert: %s', alert)
172
+ G90Callback.invoke(
173
+ self.on_remote_button_press,
174
+ alert.event_id, alert.zone_name,
175
+ G90RemoteButtonStates(alert.state)
176
+ )
177
+
178
+ return True
179
+
180
+ if alert.state in (
181
+ G90AlertStates.DOOR_OPEN, G90AlertStates.DOOR_CLOSE
182
+ ):
183
+ is_open = (
184
+ alert.source == G90AlertSources.SENSOR
185
+ and alert.state == G90AlertStates.DOOR_OPEN # noqa: W503
186
+ ) or alert.source == G90AlertSources.DOORBELL
187
+
188
+ _LOGGER.debug('Door open_close alert: %s', alert)
189
+ G90Callback.invoke(
190
+ self.on_door_open_close,
191
+ alert.event_id, alert.zone_name, is_open
192
+ )
193
+
194
+ return True
195
+
196
+ if (
197
+ alert.source == G90AlertSources.SENSOR
198
+ and alert.state == G90AlertStates.LOW_BATTERY # noqa: W503
199
+ ):
200
+ _LOGGER.debug('Low battery alert: %s', alert)
201
+ G90Callback.invoke(
202
+ self.on_low_battery,
203
+ alert.event_id, alert.zone_name
204
+ )
205
+
206
+ return True
207
+
208
+ return False
209
+
161
210
  def _handle_alert(
162
211
  self, addr: Tuple[str, int], alert: G90DeviceAlert
163
212
  ) -> None:
213
+ handled = False
214
+
164
215
  # Stop processing when alert is received from the device with different
165
216
  # GUID
166
217
  if self.device_id and alert.device_id != self.device_id:
@@ -170,31 +221,8 @@ class G90DeviceNotifications(DatagramProtocol):
170
221
  )
171
222
  return
172
223
 
173
- if alert.type == G90AlertTypes.DOOR_OPEN_CLOSE:
174
- if alert.state in (
175
- G90AlertStates.DOOR_OPEN, G90AlertStates.DOOR_CLOSE
176
- ):
177
- is_open = (
178
- alert.source == G90AlertSources.SENSOR
179
- and alert.state == G90AlertStates.DOOR_OPEN # noqa: W503
180
- ) or alert.source == G90AlertSources.DOORBELL
181
- _LOGGER.debug('Door open_close alert: %s', alert)
182
- G90Callback.invoke(
183
- self.on_door_open_close,
184
- alert.event_id, alert.zone_name, is_open
185
- )
186
- return
187
-
188
- if (
189
- alert.source == G90AlertSources.SENSOR
190
- and alert.state == G90AlertStates.LOW_BATTERY # noqa: W503
191
- ):
192
- _LOGGER.debug('Low battery alert: %s', alert)
193
- G90Callback.invoke(
194
- self.on_low_battery,
195
- alert.event_id, alert.zone_name
196
- )
197
- return
224
+ if alert.type == G90AlertTypes.SENSOR_ACTIVITY:
225
+ handled = self._handle_alert_sensor_activity(alert)
198
226
 
199
227
  if alert.type == G90AlertTypes.STATE_CHANGE:
200
228
  # Define the mapping between device state received in the alert, to
@@ -209,25 +237,46 @@ class G90DeviceNotifications(DatagramProtocol):
209
237
  G90AlertStateChangeTypes.DISARM: G90ArmDisarmTypes.DISARM
210
238
  }
211
239
 
212
- state = alarm_arm_disarm_state_map.get(alert.event_id)
240
+ state = alarm_arm_disarm_state_map.get(alert.event_id, None)
213
241
  if state:
214
242
  # We received the device state change related to arm/disarm,
215
243
  # invoke the corresponding callback
216
244
  _LOGGER.debug('Arm/disarm state change: %s', state)
217
245
  G90Callback.invoke(self.on_armdisarm, state)
218
- return
246
+
247
+ handled = True
219
248
 
220
249
  if alert.type == G90AlertTypes.ALARM:
221
- _LOGGER.debug('Alarm: %s', alert.zone_name)
250
+ # Remote SOS
251
+ if alert.source == G90AlertSources.REMOTE:
252
+ _LOGGER.debug('SOS: %s', alert.zone_name)
253
+ G90Callback.invoke(
254
+ self.on_sos, alert.event_id, alert.zone_name, False
255
+ )
256
+ # Regular alarm
257
+ else:
258
+ _LOGGER.debug('Alarm: %s', alert.zone_name)
259
+ G90Callback.invoke(
260
+ self.on_alarm,
261
+ alert.event_id, alert.zone_name
262
+ )
263
+ handled = True
264
+
265
+ # Host SOS
266
+ if alert.type == G90AlertTypes.HOST_SOS:
267
+ zone_name = 'Host SOS'
268
+
269
+ _LOGGER.debug('SOS: Host')
222
270
  G90Callback.invoke(
223
- self.on_alarm,
224
- alert.event_id, alert.zone_name
271
+ self.on_sos, alert.event_id, zone_name, True
225
272
  )
226
- return
227
273
 
228
- _LOGGER.warning('Unknown alert received from %s:%s:'
229
- ' type %s, data %s',
230
- addr[0], addr[1], alert.type, alert)
274
+ handled = True
275
+
276
+ if not handled:
277
+ _LOGGER.warning('Unknown alert received from %s:%s:'
278
+ ' type %s, data %s',
279
+ addr[0], addr[1], alert.type, alert)
231
280
 
232
281
  # Implementation of datagram protocol,
233
282
  # https://docs.python.org/3/library/asyncio-protocol.html#datagram-protocols
@@ -307,11 +356,16 @@ class G90DeviceNotifications(DatagramProtocol):
307
356
  async def on_armdisarm(self, state: G90ArmDisarmTypes) -> None:
308
357
  """
309
358
  Invoked when device is armed or disarmed.
359
+
360
+ :param state: State of the device
310
361
  """
311
362
 
312
363
  async def on_sensor_activity(self, idx: int, name: str) -> None:
313
364
  """
314
365
  Invoked on sensor activity.
366
+
367
+ :param idx: Index of the sensor.
368
+ :param name: Name of the sensor.
315
369
  """
316
370
 
317
371
  async def on_door_open_close(
@@ -319,16 +373,55 @@ class G90DeviceNotifications(DatagramProtocol):
319
373
  ) -> None:
320
374
  """
321
375
  Invoked when door sensor reports it opened or closed.
376
+
377
+ :param event_id: Index of the sensor reporting the event.
378
+ :param zone_name: Name of the sensor that reports door open/close.
379
+ :param is_open: Indicates if the door is open.
322
380
  """
323
381
 
324
382
  async def on_low_battery(self, event_id: int, zone_name: str) -> None:
325
383
  """
326
384
  Invoked when a sensor reports it is low on battery.
385
+
386
+ :param event_id: Index of the sensor.
387
+ :param zone_name: Name of the sensor that reports low battery.
327
388
  """
328
389
 
329
390
  async def on_alarm(self, event_id: int, zone_name: str) -> None:
330
391
  """
331
392
  Invoked when device triggers the alarm.
393
+
394
+ :param event_id: Index of the sensor.
395
+ :param zone_name: Name of the zone that triggered the alarm.
396
+ """
397
+
398
+ async def on_remote_button_press(
399
+ self, event_id: int, zone_name: str, button: G90RemoteButtonStates
400
+ ) -> None:
401
+ """
402
+ Invoked when a remote button is pressed.
403
+
404
+ Please note there will only be call to the method w/o invoking
405
+ :meth:`G90DeviceNotifications.on_sensor_activity`.
406
+
407
+ :param event_id: Index of the sensor associated with the remote.
408
+ :param zone_name: Name of the sensor that reports remote button press.
409
+ :param button: The button pressed on the remote
410
+ """
411
+
412
+ async def on_sos(
413
+ self, event_id: int, zone_name: str, is_host_sos: bool
414
+ ) -> None:
415
+ """
416
+ Invoked when SOS is triggered.
417
+
418
+ Please note that the panel might not set its status to alarm
419
+ internally, so that :meth:`G90DeviceNotifications` might need an
420
+ explicit call in the derived class to simulate that.
421
+
422
+ :param event_id: Index of the sensor.
423
+ :param zone_name: Name of the sensor that reports SOS.
424
+ :param is_host_sos: Indicates if the SOS is host-initiated.
332
425
  """
333
426
 
334
427
  async def listen(self) -> None:
@@ -378,4 +471,12 @@ class G90DeviceNotifications(DatagramProtocol):
378
471
 
379
472
  @device_id.setter
380
473
  def device_id(self, device_id: str) -> None:
474
+ # Under not yet identified circumstances the device ID might be empty
475
+ # string provided by :meth:`G90Alarm.get_host_info` - disallow that
476
+ if not device_id or len(device_id.strip()) == 0:
477
+ _LOGGER.debug(
478
+ 'Device ID is empty or contains whitespace only, not setting'
479
+ )
480
+ return
481
+
381
482
  self._device_id = device_id
@@ -21,6 +21,8 @@
21
21
  """
22
22
  History protocol entity.
23
23
  """
24
+ import logging
25
+
24
26
  from typing import Any, Optional, Dict
25
27
  from dataclasses import dataclass
26
28
  from datetime import datetime, timezone
@@ -30,15 +32,18 @@ from .const import (
30
32
  G90AlertStates,
31
33
  G90AlertStateChangeTypes,
32
34
  G90HistoryStates,
35
+ G90RemoteButtonStates,
33
36
  )
34
37
  from .device_notifications import G90DeviceAlert
35
38
 
39
+ _LOGGER = logging.getLogger(__name__)
40
+
36
41
 
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
+ # The state of the incoming history entries are mixed of `G90AlertStates`,
43
+ # `G90AlertStateChangeTypes` and `G90RemoteButtonStates`, depending on entry
44
+ # type - hence separate dictionaries, since enums used for keys have
45
+ # conflicting values
46
+ states_mapping_alerts = {
42
47
  G90AlertStates.DOOR_CLOSE:
43
48
  G90HistoryStates.DOOR_CLOSE,
44
49
  G90AlertStates.DOOR_OPEN:
@@ -47,6 +52,9 @@ states_mapping = {
47
52
  G90HistoryStates.TAMPER,
48
53
  G90AlertStates.LOW_BATTERY:
49
54
  G90HistoryStates.LOW_BATTERY,
55
+ }
56
+
57
+ states_mapping_state_changes = {
50
58
  G90AlertStateChangeTypes.AC_POWER_FAILURE:
51
59
  G90HistoryStates.AC_POWER_FAILURE,
52
60
  G90AlertStateChangeTypes.AC_POWER_RECOVER:
@@ -65,6 +73,17 @@ states_mapping = {
65
73
  G90HistoryStates.WIFI_DISCONNECTED,
66
74
  }
67
75
 
76
+ states_mapping_remote_buttons = {
77
+ G90RemoteButtonStates.ARM_AWAY:
78
+ G90HistoryStates.REMOTE_BUTTON_ARM_AWAY,
79
+ G90RemoteButtonStates.ARM_HOME:
80
+ G90HistoryStates.REMOTE_BUTTON_ARM_HOME,
81
+ G90RemoteButtonStates.DISARM:
82
+ G90HistoryStates.REMOTE_BUTTON_DISARM,
83
+ G90RemoteButtonStates.SOS:
84
+ G90HistoryStates.REMOTE_BUTTON_SOS,
85
+ }
86
+
68
87
 
69
88
  @dataclass
70
89
  class ProtocolData:
@@ -87,6 +106,7 @@ class G90History:
87
106
  Represents a history entry from the alarm panel.
88
107
  """
89
108
  def __init__(self, *args: Any, **kwargs: Any):
109
+ self._raw_data = args
90
110
  self._protocol_data = ProtocolData(*args, **kwargs)
91
111
 
92
112
  @property
@@ -99,60 +119,95 @@ class G90History:
99
119
  )
100
120
 
101
121
  @property
102
- def type(self) -> G90AlertTypes:
122
+ def type(self) -> Optional[G90AlertTypes]:
103
123
  """
104
124
  Type of the history entry.
105
125
  """
106
- return G90AlertTypes(self._protocol_data.type)
126
+ try:
127
+ return G90AlertTypes(self._protocol_data.type)
128
+ except (ValueError, KeyError):
129
+ _LOGGER.warning(
130
+ "Can't interpret '%s' as alert type (decoded protocol"
131
+ " data '%s', raw data '%s')",
132
+ self._protocol_data.type, self._protocol_data, self._raw_data
133
+ )
134
+ return None
107
135
 
108
136
  @property
109
- def state(self) -> G90HistoryStates:
137
+ def state(self) -> Optional[G90HistoryStates]:
110
138
  """
111
139
  State for the history entry.
112
140
  """
113
- # Door open/close type, mapped against `G90AlertStates` using `state`
114
- # incoming field
115
- if self.type == G90AlertTypes.DOOR_OPEN_CLOSE:
116
- return G90HistoryStates(
117
- states_mapping[G90AlertStates(self._protocol_data.state)]
141
+ # No meaningful state for SOS alerts initiated by the panel itself
142
+ # (host)
143
+ if self.type == G90AlertTypes.HOST_SOS:
144
+ return None
145
+
146
+ try:
147
+ # State of the remote indicate which button has been pressed
148
+ if (
149
+ self.type in [
150
+ G90AlertTypes.SENSOR_ACTIVITY, G90AlertTypes.ALARM
151
+ ] and self.source == G90AlertSources.REMOTE
152
+ ):
153
+ return states_mapping_remote_buttons[
154
+ G90RemoteButtonStates(self._protocol_data.state)
155
+ ]
156
+
157
+ # Door open/close or alert types, mapped against `G90AlertStates`
158
+ # using `state` incoming field
159
+ if self.type in [
160
+ G90AlertTypes.SENSOR_ACTIVITY, G90AlertTypes.ALARM
161
+ ]:
162
+ return G90HistoryStates(
163
+ states_mapping_alerts[
164
+ G90AlertStates(self._protocol_data.state)
165
+ ]
166
+ )
167
+ except (ValueError, KeyError):
168
+ _LOGGER.warning(
169
+ "Can't interpret '%s' as alert state (decoded protocol"
170
+ " data '%s', raw data '%s')",
171
+ self._protocol_data.state, self._protocol_data, self._raw_data
118
172
  )
173
+ return None
119
174
 
120
- # Device state change, mapped against `G90AlertStateChangeTypes` using
121
- # `event_id` incoming field
122
- if self.type == G90AlertTypes.STATE_CHANGE:
175
+ try:
176
+ # Other types are mapped against `G90AlertStateChangeTypes`
123
177
  return G90HistoryStates(
124
- states_mapping[
178
+ states_mapping_state_changes[
125
179
  G90AlertStateChangeTypes(self._protocol_data.event_id)
126
180
  ]
127
181
  )
128
-
129
- # Alarm gets mapped to its counterpart in `G90HistoryStates`
130
- if self.type == G90AlertTypes.ALARM:
131
- return G90HistoryStates.ALARM
132
-
133
- # Other types are mapped against `G90AlertStateChangeTypes`
134
- return G90HistoryStates(
135
- states_mapping[
136
- G90AlertStateChangeTypes(self._protocol_data.event_id)
137
- ]
138
- )
182
+ except (ValueError, KeyError):
183
+ _LOGGER.warning(
184
+ "Can't interpret '%s' as state change (decoded protocol"
185
+ " data '%s', raw data '%s')",
186
+ self._protocol_data.event_id, self._protocol_data,
187
+ self._raw_data
188
+ )
189
+ return None
139
190
 
140
191
  @property
141
- def source(self) -> G90AlertSources:
192
+ def source(self) -> Optional[G90AlertSources]:
142
193
  """
143
194
  Source of the history entry.
144
195
  """
145
- # Device state changes or open/close events are mapped against
146
- # `G90AlertSources` using `source` incoming field
147
- if self.type in [
148
- G90AlertTypes.STATE_CHANGE, G90AlertTypes.DOOR_OPEN_CLOSE
149
- ]:
150
- return G90AlertSources(self._protocol_data.source)
151
-
152
- # Alarm will have `SENSOR` as the source, since that is likely what
153
- # triggered it
154
- if self.type == G90AlertTypes.ALARM:
155
- return G90AlertSources.SENSOR
196
+ try:
197
+ # Device state changes, open/close or alarm events are mapped
198
+ # against `G90AlertSources` using `source` incoming field
199
+ if self.type in [
200
+ G90AlertTypes.STATE_CHANGE, G90AlertTypes.SENSOR_ACTIVITY,
201
+ G90AlertTypes.ALARM
202
+ ]:
203
+ return G90AlertSources(self._protocol_data.source)
204
+ except (ValueError, KeyError):
205
+ _LOGGER.warning(
206
+ "Can't interpret '%s' as alert source (decoded protocol"
207
+ " data '%s', raw data '%s')",
208
+ self._protocol_data.source, self._protocol_data, self._raw_data
209
+ )
210
+ return None
156
211
 
157
212
  # Other sources are assumed to be initiated by device itself
158
213
  return G90AlertSources.DEVICE
@@ -182,6 +237,7 @@ class G90History:
182
237
  Returns the history entry represented as device alert structure,
183
238
  suitable for :meth:`G90DeviceNotifications._handle_alert`.
184
239
  """
240
+
185
241
  return G90DeviceAlert(
186
242
  type=self._protocol_data.type,
187
243
  event_id=self._protocol_data.event_id,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyg90alarm
3
- Version: 1.16.0
3
+ Version: 1.17.0
4
4
  Summary: G90 Alarm system protocol
5
5
  Home-page: https://github.com/hostcc/pyg90alarm
6
6
  Author: Ilia Sotnikov
@@ -51,5 +51,6 @@ tests/device_mock.py
51
51
  tests/test_alarm.py
52
52
  tests/test_base_commands.py
53
53
  tests/test_discovery.py
54
+ tests/test_history.py
54
55
  tests/test_notifications.py
55
56
  tests/test_paginated_commands.py