pyg90alarm 1.16.1__tar.gz → 1.17.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/PKG-INFO +1 -1
  2. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/alarm.py +100 -2
  3. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/const.py +17 -0
  4. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/device_notifications.py +142 -37
  5. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/history.py +40 -16
  6. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm.egg-info/PKG-INFO +1 -1
  7. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm.egg-info/SOURCES.txt +1 -0
  8. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/tests/test_alarm.py +102 -151
  9. pyg90alarm-1.17.1/tests/test_history.py +233 -0
  10. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/tests/test_notifications.py +58 -0
  11. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/.github/CODEOWNERS +0 -0
  12. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/.github/workflows/main.yml +0 -0
  13. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/.gitignore +0 -0
  14. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/.pylintrc +0 -0
  15. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/.readthedocs.yaml +0 -0
  16. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/LICENSE +0 -0
  17. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/MANIFEST.in +0 -0
  18. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/README.rst +0 -0
  19. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/docs/.DS_Store +0 -0
  20. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/docs/.gitignore +0 -0
  21. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/docs/api-docs.rst +0 -0
  22. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/docs/conf.py +0 -0
  23. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/docs/index.rst +0 -0
  24. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/docs/protocol.rst +0 -0
  25. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/docs/requirements.txt +0 -0
  26. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/pyproject.toml +0 -0
  27. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/setup.cfg +0 -0
  28. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/setup.py +0 -0
  29. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/sonar-project.properties +0 -0
  30. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/__init__.py +0 -0
  31. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/base_cmd.py +0 -0
  32. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/callback.py +0 -0
  33. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/config.py +0 -0
  34. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/definitions/__init__.py +0 -0
  35. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/definitions/sensors.py +0 -0
  36. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/discovery.py +0 -0
  37. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/entities/__init__.py +0 -0
  38. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/entities/device.py +0 -0
  39. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/entities/sensor.py +0 -0
  40. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/exceptions.py +0 -0
  41. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/host_info.py +0 -0
  42. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/host_status.py +0 -0
  43. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/paginated_cmd.py +0 -0
  44. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/paginated_result.py +0 -0
  45. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/py.typed +0 -0
  46. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/targeted_discovery.py +0 -0
  47. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm/user_data_crc.py +0 -0
  48. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm.egg-info/dependency_links.txt +0 -0
  49. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm.egg-info/requires.txt +0 -0
  50. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/src/pyg90alarm.egg-info/top_level.txt +0 -0
  51. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/tests/__init__.py +0 -0
  52. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/tests/conftest.py +0 -0
  53. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/tests/device_mock.py +0 -0
  54. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/tests/test_base_commands.py +0 -0
  55. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/tests/test_discovery.py +0 -0
  56. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/tests/test_paginated_commands.py +0 -0
  57. {pyg90alarm-1.16.1 → pyg90alarm-1.17.1}/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.1
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.
@@ -820,7 +915,10 @@ class G90Alarm(G90DeviceNotifications):
820
915
  # notifications port
821
916
  self._handle_alert(
822
917
  (self._host, self._notifications_local_port),
823
- item.as_device_alert()
918
+ item.as_device_alert(),
919
+ # Skip verifying device GUID, since history entry
920
+ # don't have it
921
+ verify_device_id=False
824
922
  )
825
923
 
826
924
  # Record the entry as most recent one
@@ -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,52 +152,81 @@ 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
- self, addr: Tuple[str, int], alert: G90DeviceAlert
211
+ self, addr: Tuple[str, int], alert: G90DeviceAlert,
212
+ verify_device_id: bool = True
163
213
  ) -> None:
214
+ handled = False
215
+
164
216
  # Stop processing when alert is received from the device with different
165
- # GUID
166
- if self.device_id and alert.device_id != self.device_id:
217
+ # GUID (if enabled)
218
+ if (
219
+ verify_device_id and self.device_id
220
+ and alert.device_id != self.device_id
221
+ ):
167
222
  _LOGGER.error(
168
223
  "Received alert from wrong device: expected '%s', got '%s'",
169
224
  self.device_id, alert.device_id
170
225
  )
171
226
  return
172
227
 
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
228
+ if alert.type == G90AlertTypes.SENSOR_ACTIVITY:
229
+ handled = self._handle_alert_sensor_activity(alert)
198
230
 
199
231
  if alert.type == G90AlertTypes.STATE_CHANGE:
200
232
  # Define the mapping between device state received in the alert, to
@@ -209,25 +241,46 @@ class G90DeviceNotifications(DatagramProtocol):
209
241
  G90AlertStateChangeTypes.DISARM: G90ArmDisarmTypes.DISARM
210
242
  }
211
243
 
212
- state = alarm_arm_disarm_state_map.get(alert.event_id)
244
+ state = alarm_arm_disarm_state_map.get(alert.event_id, None)
213
245
  if state:
214
246
  # We received the device state change related to arm/disarm,
215
247
  # invoke the corresponding callback
216
248
  _LOGGER.debug('Arm/disarm state change: %s', state)
217
249
  G90Callback.invoke(self.on_armdisarm, state)
218
- return
250
+
251
+ handled = True
219
252
 
220
253
  if alert.type == G90AlertTypes.ALARM:
221
- _LOGGER.debug('Alarm: %s', alert.zone_name)
254
+ # Remote SOS
255
+ if alert.source == G90AlertSources.REMOTE:
256
+ _LOGGER.debug('SOS: %s', alert.zone_name)
257
+ G90Callback.invoke(
258
+ self.on_sos, alert.event_id, alert.zone_name, False
259
+ )
260
+ # Regular alarm
261
+ else:
262
+ _LOGGER.debug('Alarm: %s', alert.zone_name)
263
+ G90Callback.invoke(
264
+ self.on_alarm,
265
+ alert.event_id, alert.zone_name
266
+ )
267
+ handled = True
268
+
269
+ # Host SOS
270
+ if alert.type == G90AlertTypes.HOST_SOS:
271
+ zone_name = 'Host SOS'
272
+
273
+ _LOGGER.debug('SOS: Host')
222
274
  G90Callback.invoke(
223
- self.on_alarm,
224
- alert.event_id, alert.zone_name
275
+ self.on_sos, alert.event_id, zone_name, True
225
276
  )
226
- return
227
277
 
228
- _LOGGER.warning('Unknown alert received from %s:%s:'
229
- ' type %s, data %s',
230
- addr[0], addr[1], alert.type, alert)
278
+ handled = True
279
+
280
+ if not handled:
281
+ _LOGGER.warning('Unknown alert received from %s:%s:'
282
+ ' type %s, data %s',
283
+ addr[0], addr[1], alert.type, alert)
231
284
 
232
285
  # Implementation of datagram protocol,
233
286
  # https://docs.python.org/3/library/asyncio-protocol.html#datagram-protocols
@@ -307,11 +360,16 @@ class G90DeviceNotifications(DatagramProtocol):
307
360
  async def on_armdisarm(self, state: G90ArmDisarmTypes) -> None:
308
361
  """
309
362
  Invoked when device is armed or disarmed.
363
+
364
+ :param state: State of the device
310
365
  """
311
366
 
312
367
  async def on_sensor_activity(self, idx: int, name: str) -> None:
313
368
  """
314
369
  Invoked on sensor activity.
370
+
371
+ :param idx: Index of the sensor.
372
+ :param name: Name of the sensor.
315
373
  """
316
374
 
317
375
  async def on_door_open_close(
@@ -319,16 +377,55 @@ class G90DeviceNotifications(DatagramProtocol):
319
377
  ) -> None:
320
378
  """
321
379
  Invoked when door sensor reports it opened or closed.
380
+
381
+ :param event_id: Index of the sensor reporting the event.
382
+ :param zone_name: Name of the sensor that reports door open/close.
383
+ :param is_open: Indicates if the door is open.
322
384
  """
323
385
 
324
386
  async def on_low_battery(self, event_id: int, zone_name: str) -> None:
325
387
  """
326
388
  Invoked when a sensor reports it is low on battery.
389
+
390
+ :param event_id: Index of the sensor.
391
+ :param zone_name: Name of the sensor that reports low battery.
327
392
  """
328
393
 
329
394
  async def on_alarm(self, event_id: int, zone_name: str) -> None:
330
395
  """
331
396
  Invoked when device triggers the alarm.
397
+
398
+ :param event_id: Index of the sensor.
399
+ :param zone_name: Name of the zone that triggered the alarm.
400
+ """
401
+
402
+ async def on_remote_button_press(
403
+ self, event_id: int, zone_name: str, button: G90RemoteButtonStates
404
+ ) -> None:
405
+ """
406
+ Invoked when a remote button is pressed.
407
+
408
+ Please note there will only be call to the method w/o invoking
409
+ :meth:`G90DeviceNotifications.on_sensor_activity`.
410
+
411
+ :param event_id: Index of the sensor associated with the remote.
412
+ :param zone_name: Name of the sensor that reports remote button press.
413
+ :param button: The button pressed on the remote
414
+ """
415
+
416
+ async def on_sos(
417
+ self, event_id: int, zone_name: str, is_host_sos: bool
418
+ ) -> None:
419
+ """
420
+ Invoked when SOS is triggered.
421
+
422
+ Please note that the panel might not set its status to alarm
423
+ internally, so that :meth:`G90DeviceNotifications` might need an
424
+ explicit call in the derived class to simulate that.
425
+
426
+ :param event_id: Index of the sensor.
427
+ :param zone_name: Name of the sensor that reports SOS.
428
+ :param is_host_sos: Indicates if the SOS is host-initiated.
332
429
  """
333
430
 
334
431
  async def listen(self) -> None:
@@ -378,4 +475,12 @@ class G90DeviceNotifications(DatagramProtocol):
378
475
 
379
476
  @device_id.setter
380
477
  def device_id(self, device_id: str) -> None:
478
+ # Under not yet identified circumstances the device ID might be empty
479
+ # string provided by :meth:`G90Alarm.get_host_info` - disallow that
480
+ if not device_id or len(device_id.strip()) == 0:
481
+ _LOGGER.debug(
482
+ 'Device ID is empty or contains whitespace only, not setting'
483
+ )
484
+ return
485
+
381
486
  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.1
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