pyg90alarm 1.16.1__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.1 → pyg90alarm-1.17.0}/PKG-INFO +1 -1
  2. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/alarm.py +96 -1
  3. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/const.py +17 -0
  4. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/device_notifications.py +135 -34
  5. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/history.py +40 -16
  6. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm.egg-info/PKG-INFO +1 -1
  7. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm.egg-info/SOURCES.txt +1 -0
  8. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/tests/test_alarm.py +102 -151
  9. pyg90alarm-1.17.0/tests/test_history.py +226 -0
  10. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/tests/test_notifications.py +58 -0
  11. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/.github/CODEOWNERS +0 -0
  12. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/.github/workflows/main.yml +0 -0
  13. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/.gitignore +0 -0
  14. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/.pylintrc +0 -0
  15. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/.readthedocs.yaml +0 -0
  16. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/LICENSE +0 -0
  17. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/MANIFEST.in +0 -0
  18. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/README.rst +0 -0
  19. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/docs/.DS_Store +0 -0
  20. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/docs/.gitignore +0 -0
  21. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/docs/api-docs.rst +0 -0
  22. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/docs/conf.py +0 -0
  23. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/docs/index.rst +0 -0
  24. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/docs/protocol.rst +0 -0
  25. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/docs/requirements.txt +0 -0
  26. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/pyproject.toml +0 -0
  27. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/setup.cfg +0 -0
  28. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/setup.py +0 -0
  29. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/sonar-project.properties +0 -0
  30. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/__init__.py +0 -0
  31. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/base_cmd.py +0 -0
  32. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/callback.py +0 -0
  33. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/config.py +0 -0
  34. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/definitions/__init__.py +0 -0
  35. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/definitions/sensors.py +0 -0
  36. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/discovery.py +0 -0
  37. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/entities/__init__.py +0 -0
  38. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/entities/device.py +0 -0
  39. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/entities/sensor.py +0 -0
  40. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/exceptions.py +0 -0
  41. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/host_info.py +0 -0
  42. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/host_status.py +0 -0
  43. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/paginated_cmd.py +0 -0
  44. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/paginated_result.py +0 -0
  45. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/py.typed +0 -0
  46. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/targeted_discovery.py +0 -0
  47. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm/user_data_crc.py +0 -0
  48. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm.egg-info/dependency_links.txt +0 -0
  49. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm.egg-info/requires.txt +0 -0
  50. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/src/pyg90alarm.egg-info/top_level.txt +0 -0
  51. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/tests/__init__.py +0 -0
  52. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/tests/conftest.py +0 -0
  53. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/tests/device_mock.py +0 -0
  54. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/tests/test_base_commands.py +0 -0
  55. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/tests/test_discovery.py +0 -0
  56. {pyg90alarm-1.16.1 → pyg90alarm-1.17.0}/tests/test_paginated_commands.py +0 -0
  57. {pyg90alarm-1.16.1 → 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.1
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.
@@ -187,8 +187,11 @@ class G90AlertTypes(IntEnum):
187
187
  """
188
188
  Defines types of alerts sent by the alarm panel.
189
189
  """
190
+ HOST_SOS = 1
190
191
  STATE_CHANGE = 2
191
192
  ALARM = 3
193
+ SENSOR_ACTIVITY = 4
194
+ # Retained for compatibility, deprecated
192
195
  DOOR_OPEN_CLOSE = 4
193
196
 
194
197
 
@@ -245,3 +248,17 @@ class G90HistoryStates(IntEnum):
245
248
  LOW_BATTERY = 10
246
249
  WIFI_CONNECTED = 11
247
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
@@ -32,15 +32,17 @@ from .const import (
32
32
  G90AlertStates,
33
33
  G90AlertStateChangeTypes,
34
34
  G90HistoryStates,
35
+ G90RemoteButtonStates,
35
36
  )
36
37
  from .device_notifications import G90DeviceAlert
37
38
 
38
39
  _LOGGER = logging.getLogger(__name__)
39
40
 
40
41
 
41
- # The state of the incoming history entries are mixed of `G90AlertStates` and
42
- # `G90AlertStateChangeTypes`, depending on entry type - hence two separate
43
- # dictionaries, since enums used for keys have conflicting values
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
44
46
  states_mapping_alerts = {
45
47
  G90AlertStates.DOOR_CLOSE:
46
48
  G90HistoryStates.DOOR_CLOSE,
@@ -71,6 +73,17 @@ states_mapping_state_changes = {
71
73
  G90HistoryStates.WIFI_DISCONNECTED,
72
74
  }
73
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
+
74
87
 
75
88
  @dataclass
76
89
  class ProtocolData:
@@ -112,7 +125,7 @@ class G90History:
112
125
  """
113
126
  try:
114
127
  return G90AlertTypes(self._protocol_data.type)
115
- except ValueError:
128
+ except (ValueError, KeyError):
116
129
  _LOGGER.warning(
117
130
  "Can't interpret '%s' as alert type (decoded protocol"
118
131
  " data '%s', raw data '%s')",
@@ -125,18 +138,33 @@ class G90History:
125
138
  """
126
139
  State for the history entry.
127
140
  """
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
+
128
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
+
129
157
  # Door open/close or alert types, mapped against `G90AlertStates`
130
158
  # using `state` incoming field
131
159
  if self.type in [
132
- G90AlertTypes.DOOR_OPEN_CLOSE, G90AlertTypes.ALARM
160
+ G90AlertTypes.SENSOR_ACTIVITY, G90AlertTypes.ALARM
133
161
  ]:
134
162
  return G90HistoryStates(
135
163
  states_mapping_alerts[
136
164
  G90AlertStates(self._protocol_data.state)
137
165
  ]
138
166
  )
139
- except ValueError:
167
+ except (ValueError, KeyError):
140
168
  _LOGGER.warning(
141
169
  "Can't interpret '%s' as alert state (decoded protocol"
142
170
  " data '%s', raw data '%s')",
@@ -151,7 +179,7 @@ class G90History:
151
179
  G90AlertStateChangeTypes(self._protocol_data.event_id)
152
180
  ]
153
181
  )
154
- except ValueError:
182
+ except (ValueError, KeyError):
155
183
  _LOGGER.warning(
156
184
  "Can't interpret '%s' as state change (decoded protocol"
157
185
  " data '%s', raw data '%s')",
@@ -166,13 +194,14 @@ class G90History:
166
194
  Source of the history entry.
167
195
  """
168
196
  try:
169
- # Device state changes or open/close events are mapped against
170
- # `G90AlertSources` using `source` incoming field
197
+ # Device state changes, open/close or alarm events are mapped
198
+ # against `G90AlertSources` using `source` incoming field
171
199
  if self.type in [
172
- G90AlertTypes.STATE_CHANGE, G90AlertTypes.DOOR_OPEN_CLOSE
200
+ G90AlertTypes.STATE_CHANGE, G90AlertTypes.SENSOR_ACTIVITY,
201
+ G90AlertTypes.ALARM
173
202
  ]:
174
203
  return G90AlertSources(self._protocol_data.source)
175
- except ValueError:
204
+ except (ValueError, KeyError):
176
205
  _LOGGER.warning(
177
206
  "Can't interpret '%s' as alert source (decoded protocol"
178
207
  " data '%s', raw data '%s')",
@@ -180,11 +209,6 @@ class G90History:
180
209
  )
181
210
  return None
182
211
 
183
- # Alarm will have `SENSOR` as the source, since that is likely what
184
- # triggered it
185
- if self.type == G90AlertTypes.ALARM:
186
- return G90AlertSources.SENSOR
187
-
188
212
  # Other sources are assumed to be initiated by device itself
189
213
  return G90AlertSources.DEVICE
190
214
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyg90alarm
3
- Version: 1.16.1
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