pyg90alarm 2.3.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.
Files changed (42) hide show
  1. pyg90alarm/__init__.py +84 -0
  2. pyg90alarm/alarm.py +1274 -0
  3. pyg90alarm/callback.py +146 -0
  4. pyg90alarm/cloud/__init__.py +31 -0
  5. pyg90alarm/cloud/const.py +56 -0
  6. pyg90alarm/cloud/messages.py +593 -0
  7. pyg90alarm/cloud/notifications.py +410 -0
  8. pyg90alarm/cloud/protocol.py +518 -0
  9. pyg90alarm/const.py +273 -0
  10. pyg90alarm/definitions/__init__.py +3 -0
  11. pyg90alarm/definitions/base.py +247 -0
  12. pyg90alarm/definitions/devices.py +366 -0
  13. pyg90alarm/definitions/sensors.py +843 -0
  14. pyg90alarm/entities/__init__.py +3 -0
  15. pyg90alarm/entities/base_entity.py +93 -0
  16. pyg90alarm/entities/base_list.py +268 -0
  17. pyg90alarm/entities/device.py +97 -0
  18. pyg90alarm/entities/device_list.py +156 -0
  19. pyg90alarm/entities/sensor.py +891 -0
  20. pyg90alarm/entities/sensor_list.py +183 -0
  21. pyg90alarm/exceptions.py +63 -0
  22. pyg90alarm/local/__init__.py +0 -0
  23. pyg90alarm/local/base_cmd.py +293 -0
  24. pyg90alarm/local/config.py +157 -0
  25. pyg90alarm/local/discovery.py +103 -0
  26. pyg90alarm/local/history.py +272 -0
  27. pyg90alarm/local/host_info.py +89 -0
  28. pyg90alarm/local/host_status.py +52 -0
  29. pyg90alarm/local/notifications.py +117 -0
  30. pyg90alarm/local/paginated_cmd.py +132 -0
  31. pyg90alarm/local/paginated_result.py +135 -0
  32. pyg90alarm/local/targeted_discovery.py +162 -0
  33. pyg90alarm/local/user_data_crc.py +46 -0
  34. pyg90alarm/notifications/__init__.py +0 -0
  35. pyg90alarm/notifications/base.py +481 -0
  36. pyg90alarm/notifications/protocol.py +127 -0
  37. pyg90alarm/py.typed +0 -0
  38. pyg90alarm-2.3.0.dist-info/METADATA +277 -0
  39. pyg90alarm-2.3.0.dist-info/RECORD +42 -0
  40. pyg90alarm-2.3.0.dist-info/WHEEL +5 -0
  41. pyg90alarm-2.3.0.dist-info/licenses/LICENSE +21 -0
  42. pyg90alarm-2.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,481 @@
1
+ # Copyright (c) 2025 Ilia Sotnikov
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+
21
+ """
22
+ Implements support for notifications/alerts sent by G90 alarm panel.
23
+ """
24
+ import json
25
+ import logging
26
+ from typing import (
27
+ Optional, List, Any, Callable
28
+ )
29
+ from dataclasses import dataclass
30
+ from asyncio.transports import BaseTransport
31
+ from datetime import datetime, timezone
32
+
33
+ from ..callback import G90Callback
34
+ from ..const import (
35
+ G90MessageTypes,
36
+ G90NotificationTypes,
37
+ G90AlertTypes,
38
+ G90AlertStateChangeTypes,
39
+ G90ArmDisarmTypes,
40
+ G90AlertSources,
41
+ G90AlertStates,
42
+ G90RemoteButtonStates,
43
+ )
44
+ from .protocol import G90NotificationProtocol
45
+
46
+ _LOGGER = logging.getLogger(__name__)
47
+
48
+
49
+ @dataclass
50
+ class G90Message:
51
+ """
52
+ Represents the message received from the device.
53
+
54
+ :meta private:
55
+ """
56
+ code: G90MessageTypes
57
+ data: List[Any]
58
+
59
+
60
+ @dataclass
61
+ class G90Notification:
62
+ """
63
+ Represents the notification received from the device.
64
+
65
+ :meta private:
66
+ """
67
+ kind: G90NotificationTypes
68
+ data: List[Any]
69
+
70
+
71
+ @dataclass
72
+ class G90ZoneInfo:
73
+ """
74
+ Represents zone details received from the device.
75
+
76
+ :meta private:
77
+ """
78
+ idx: int
79
+ name: str
80
+
81
+
82
+ @dataclass
83
+ class G90ArmDisarmInfo:
84
+ """
85
+ Represents the arm/disarm state received from the device.
86
+
87
+ :meta private:
88
+ """
89
+ state: int
90
+
91
+
92
+ @dataclass
93
+ class G90SensorChangeInfo:
94
+ """
95
+ Represents the sensor added notification received from the device.
96
+
97
+ :meta private:
98
+ """
99
+ idx: int
100
+ name: str
101
+ action: int # 1 - if added, 2 - for update (?)
102
+
103
+
104
+ @dataclass
105
+ class G90DeviceAlert: # pylint: disable=too-many-instance-attributes
106
+ """
107
+ Represents alert received from the device.
108
+ """
109
+ type: G90AlertTypes
110
+ event_id: G90AlertStateChangeTypes
111
+ source: G90AlertSources
112
+ state: int
113
+ zone_name: str
114
+ device_id: str
115
+ unix_time: int
116
+ resv4: int
117
+ other: str
118
+
119
+
120
+ class G90NotificationsBase:
121
+ """
122
+ Implements support for notifications/alerts sent by alarm panel.
123
+
124
+ There is a basic check to ensure only notifications/alerts from the correct
125
+ device are processed - the check uses the host and port of the device, and
126
+ the device ID (GUID) that is set by the ancestor class that implements the
127
+ commands (e.g. :class:`.G90Alarm`). The latter to work correctly needs a
128
+ command to be performed first, one that fetches device GUID and then stores
129
+ it using :attr:`.device_id` (e.g. :meth:`.G90Alarm.get_host_info`).
130
+ """
131
+ def __init__(
132
+ self, protocol_factory: Callable[[], G90NotificationProtocol],
133
+ ):
134
+ # pylint: disable=too-many-arguments
135
+ self._transport: Optional[BaseTransport] = None
136
+ # Same but for device ID (GUID) - the notifications logic uses it to
137
+ # perform validation, but doesn't set it from messages received (it
138
+ # will diminish the purpose of the validation, should be done by an
139
+ # ancestor class).
140
+ self._device_id: Optional[str] = None
141
+ self._protocol = protocol_factory()
142
+ self._last_device_packet_time: Optional[datetime] = None
143
+ self._last_upstream_packet_time: Optional[datetime] = None
144
+
145
+ def handle_notification(
146
+ self, notification: G90Notification
147
+ ) -> None:
148
+ """
149
+ Handles notification received from the device.
150
+
151
+ :param notification: The notification to handle.
152
+ """
153
+ # Sensor activity notification
154
+ if notification.kind == G90NotificationTypes.SENSOR_ACTIVITY:
155
+ g90_zone_info = G90ZoneInfo(*notification.data)
156
+
157
+ _LOGGER.debug('Sensor notification: %s', g90_zone_info)
158
+ G90Callback.invoke(
159
+ self._protocol.on_sensor_activity,
160
+ g90_zone_info.idx, g90_zone_info.name
161
+ )
162
+
163
+ return
164
+
165
+ # Arm/disarm notification
166
+ if notification.kind == G90NotificationTypes.ARM_DISARM:
167
+ g90_armdisarm_info = G90ArmDisarmInfo(
168
+ *notification.data)
169
+ # Map the state received from the device to corresponding enum
170
+ state = G90ArmDisarmTypes(g90_armdisarm_info.state)
171
+
172
+ _LOGGER.debug('Arm/disarm notification: %s',
173
+ state)
174
+ G90Callback.invoke(self._protocol.on_armdisarm, state)
175
+
176
+ return
177
+
178
+ # An open door is detected when arming
179
+ if notification.kind == G90NotificationTypes.DOOR_OPEN_WHEN_ARMING:
180
+ g90_zone_info = G90ZoneInfo(*notification.data)
181
+ _LOGGER.debug('Door open detected when arming: %s', g90_zone_info)
182
+ G90Callback.invoke(
183
+ self._protocol.on_door_open_when_arming,
184
+ g90_zone_info.idx, g90_zone_info.name
185
+ )
186
+ return
187
+
188
+ # Sensor has been added or removed
189
+ if notification.kind == G90NotificationTypes.SENSOR_CHANGE:
190
+ g90_sensor_info = G90SensorChangeInfo(*notification.data)
191
+ sensor_added = g90_sensor_info.action == 1
192
+ _LOGGER.debug(
193
+ 'Sensor change notification, added=%s: %s',
194
+ sensor_added, g90_sensor_info
195
+ )
196
+ G90Callback.invoke(
197
+ self._protocol.on_sensor_change,
198
+ g90_sensor_info.idx, g90_sensor_info.name,
199
+ sensor_added
200
+ )
201
+ return
202
+
203
+ _LOGGER.warning('Unknown notification received:'
204
+ ' kind %s, data %s',
205
+ notification.kind, notification.data)
206
+
207
+ def _handle_alert_sensor_activity(self, alert: G90DeviceAlert) -> bool:
208
+ """
209
+ Handles sensor activity alert.
210
+ """
211
+ if alert.source == G90AlertSources.REMOTE:
212
+ _LOGGER.debug('Remote button press alert: %s', alert)
213
+ G90Callback.invoke(
214
+ self._protocol.on_remote_button_press,
215
+ alert.event_id, alert.zone_name,
216
+ G90RemoteButtonStates(alert.state)
217
+ )
218
+
219
+ return True
220
+
221
+ if alert.state in (
222
+ G90AlertStates.DOOR_OPEN, G90AlertStates.DOOR_CLOSE
223
+ ):
224
+ is_open = (
225
+ alert.source == G90AlertSources.SENSOR
226
+ and alert.state == G90AlertStates.DOOR_OPEN # noqa: W503
227
+ ) or alert.source == G90AlertSources.DOORBELL
228
+
229
+ _LOGGER.debug('Door open_close alert: %s', alert)
230
+ G90Callback.invoke(
231
+ self._protocol.on_door_open_close,
232
+ alert.event_id, alert.zone_name, is_open
233
+ )
234
+
235
+ return True
236
+
237
+ if (
238
+ alert.source == G90AlertSources.SENSOR
239
+ and alert.state == G90AlertStates.LOW_BATTERY # noqa: W503
240
+ ):
241
+ _LOGGER.debug('Low battery alert: %s', alert)
242
+ G90Callback.invoke(
243
+ self._protocol.on_low_battery,
244
+ alert.event_id, alert.zone_name
245
+ )
246
+
247
+ return True
248
+
249
+ return False
250
+
251
+ def handle_alert(
252
+ self, alert: G90DeviceAlert,
253
+ verify_device_id: bool = True
254
+ ) -> None:
255
+ """
256
+ Handles alert received from the device.
257
+
258
+ :param alert: The alert to handle.
259
+ :param verify_device_id: Whether to verify the device ID (GUID) in the
260
+ alert. If set to False, the device ID will not be verified.
261
+ """
262
+ handled = False
263
+
264
+ # Stop processing when alert is received from the device with different
265
+ # GUID (if enabled)
266
+ if (
267
+ verify_device_id and self.device_id
268
+ and alert.device_id != self.device_id
269
+ ):
270
+ _LOGGER.error(
271
+ "Received alert from wrong device: expected '%s', got '%s'",
272
+ self.device_id, alert.device_id
273
+ )
274
+ return
275
+
276
+ if alert.type == G90AlertTypes.SENSOR_ACTIVITY:
277
+ handled = self._handle_alert_sensor_activity(alert)
278
+
279
+ if alert.type == G90AlertTypes.STATE_CHANGE:
280
+ # Define the mapping between device state received in the alert, to
281
+ # common `G90ArmDisarmTypes` enum that is used when setting device
282
+ # arm state and received in the corresponding notifications. The
283
+ # primary reason is to unify state as passed down to the callbacks.
284
+ # The map covers only subset of state changes pertinent to
285
+ # arm/disarm state changes
286
+ alarm_arm_disarm_state_map = {
287
+ G90AlertStateChangeTypes.ARM_HOME: G90ArmDisarmTypes.ARM_HOME,
288
+ G90AlertStateChangeTypes.ARM_AWAY: G90ArmDisarmTypes.ARM_AWAY,
289
+ G90AlertStateChangeTypes.DISARM: G90ArmDisarmTypes.DISARM
290
+ }
291
+
292
+ state = alarm_arm_disarm_state_map.get(alert.event_id, None)
293
+ if state:
294
+ # We received the device state change related to arm/disarm,
295
+ # invoke the corresponding callback
296
+ _LOGGER.debug('Arm/disarm state change: %s', state)
297
+ G90Callback.invoke(self._protocol.on_armdisarm, state)
298
+
299
+ handled = True
300
+
301
+ if alert.type == G90AlertTypes.ALARM:
302
+ # Remote SOS
303
+ if alert.source == G90AlertSources.REMOTE:
304
+ _LOGGER.debug('SOS: %s', alert.zone_name)
305
+ G90Callback.invoke(
306
+ self._protocol.on_sos,
307
+ alert.event_id, alert.zone_name, False
308
+ )
309
+ # Regular alarm
310
+ else:
311
+ is_tampered = alert.state == G90AlertStates.TAMPER
312
+ _LOGGER.debug(
313
+ 'Alarm: %s, is tampered: %s', alert.zone_name, is_tampered
314
+ )
315
+ G90Callback.invoke(
316
+ self._protocol.on_alarm,
317
+ alert.event_id, alert.zone_name, is_tampered
318
+ )
319
+
320
+ handled = True
321
+
322
+ # Host SOS
323
+ if alert.type == G90AlertTypes.HOST_SOS:
324
+ zone_name = 'Host SOS'
325
+
326
+ _LOGGER.debug('SOS: Host')
327
+ G90Callback.invoke(
328
+ self._protocol.on_sos, alert.event_id, zone_name, True
329
+ )
330
+
331
+ handled = True
332
+
333
+ if not handled:
334
+ _LOGGER.warning(
335
+ 'Unknown alert received: type %s, data %s',
336
+ alert.type, alert
337
+ )
338
+
339
+ # pylint:disable=too-many-return-statements
340
+ def handle(self, data: bytes) -> None:
341
+ """
342
+ Invoked when message is received from the device.
343
+ """
344
+ try:
345
+ s_data = data.decode('utf-8')
346
+ except UnicodeDecodeError:
347
+ _LOGGER.error('Unable to decode device message from UTF-8')
348
+ return
349
+
350
+ if not s_data.endswith('\0'):
351
+ _LOGGER.error('Missing end marker in data')
352
+ return
353
+ payload = s_data[:-1]
354
+
355
+ try:
356
+ message = json.loads(payload)
357
+ g90_message = G90Message(*message)
358
+ except json.JSONDecodeError as exc:
359
+ _LOGGER.error("Unable to parse device message '%s' as JSON: %s",
360
+ payload, exc)
361
+ return
362
+ except TypeError as exc:
363
+ _LOGGER.error("Device message '%s' is malformed: %s",
364
+ payload, exc)
365
+ return
366
+
367
+ # Device notifications
368
+ if g90_message.code == G90MessageTypes.NOTIFICATION:
369
+ try:
370
+ notification_data = G90Notification(*g90_message.data)
371
+ except TypeError as exc:
372
+ _LOGGER.error('Bad notification received: %s', exc)
373
+ return
374
+ self.handle_notification(notification_data)
375
+ return
376
+
377
+ # Device alerts
378
+ if g90_message.code == G90MessageTypes.ALERT:
379
+ try:
380
+ alert_data = G90DeviceAlert(*g90_message.data)
381
+ except TypeError as exc:
382
+ _LOGGER.error('Bad alert received: %s', exc)
383
+ return
384
+ self.handle_alert(alert_data)
385
+ return
386
+
387
+ _LOGGER.warning('Unknown message received: %s', message)
388
+
389
+ async def listen(self) -> None:
390
+ """
391
+ Listens for notifications/alerts from the device.
392
+ """
393
+ raise NotImplementedError
394
+
395
+ @property
396
+ def listener_started(self) -> bool:
397
+ """
398
+ Indicates if the listener of the device notifications has been started.
399
+ """
400
+ return self._transport is not None
401
+
402
+ async def close(self) -> None:
403
+ """
404
+ Closes the listener.
405
+ """
406
+ if self._transport:
407
+ _LOGGER.debug('No longer listening for device notifications')
408
+ self._transport.close()
409
+ self._transport = None
410
+
411
+ @property
412
+ def device_id(self) -> Optional[str]:
413
+ """
414
+ The ID (GUID) of the panel being communicated with thru commands.
415
+
416
+ Available when any panel command receives it from the device
417
+ (`GETHOSTINFO` local command or Hello / HelloDiscovery cloud ones).
418
+ """
419
+ return self._device_id
420
+
421
+ @device_id.setter
422
+ def device_id(self, device_id: str) -> None:
423
+ # Under not yet identified circumstances the device ID might be empty
424
+ # string provided by :meth:`G90Alarm.get_host_info` - disallow that
425
+ if not device_id or len(device_id.strip()) == 0:
426
+ _LOGGER.debug(
427
+ 'Device ID is empty or contains whitespace only, not setting'
428
+ )
429
+ return
430
+
431
+ self._device_id = device_id
432
+
433
+ def clear_device_id(self) -> None:
434
+ """
435
+ Clears the device ID.
436
+ """
437
+ self._device_id = None
438
+
439
+ @property
440
+ def last_device_packet_time(self) -> Optional[datetime]:
441
+ """
442
+ Returns the timestamp of the last packet received from the device.
443
+
444
+ This property can be used to monitor the communication health with the
445
+ device.
446
+ """
447
+ return self._last_device_packet_time
448
+
449
+ def set_last_device_packet_time(self) -> None:
450
+ """
451
+ Updates the timestamp of the last packet received from the device.
452
+
453
+ This method is called internally when a packet is received from the
454
+ device.
455
+ """
456
+ self._last_device_packet_time = datetime.now(tz=timezone.utc)
457
+ _LOGGER.debug(
458
+ 'Last device packet time: %s', self._last_device_packet_time
459
+ )
460
+
461
+ @property
462
+ def last_upstream_packet_time(self) -> Optional[datetime]:
463
+ """
464
+ Returns the timestamp of the last packet sent to the upstream server.
465
+
466
+ This property can be used to monitor the communication health with the
467
+ cloud/upstream server.
468
+ """
469
+ return self._last_upstream_packet_time
470
+
471
+ def set_last_upstream_packet_time(self) -> None:
472
+ """
473
+ Updates the timestamp of the last packet sent to the upstream server.
474
+
475
+ This method is called internally when a packet is sent to the upstream
476
+ server.
477
+ """
478
+ self._last_upstream_packet_time = datetime.now(tz=timezone.utc)
479
+ _LOGGER.debug(
480
+ 'Last upstream packet time: %s', self._last_upstream_packet_time
481
+ )
@@ -0,0 +1,127 @@
1
+ # Copyright (c) 2021 Ilia Sotnikov
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+
21
+ """
22
+ Defines notification protocol for `NotificationBase` class.
23
+ """
24
+
25
+ from ..const import (
26
+ G90ArmDisarmTypes,
27
+ G90RemoteButtonStates,
28
+ )
29
+
30
+
31
+ class G90NotificationProtocol:
32
+ """
33
+ Protocol for notification handling.
34
+ """
35
+ async def on_armdisarm(self, state: G90ArmDisarmTypes) -> None:
36
+ """
37
+ Invoked when device is armed or disarmed.
38
+
39
+ :param state: State of the device
40
+ """
41
+
42
+ async def on_sensor_activity(self, idx: int, name: str) -> None:
43
+ """
44
+ Invoked on sensor activity.
45
+
46
+ :param idx: Index of the sensor.
47
+ :param name: Name of the sensor.
48
+ """
49
+
50
+ async def on_door_open_when_arming(
51
+ self, event_id: int, zone_name: str
52
+ ) -> None:
53
+ """
54
+ Invoked when door open is detected when panel is armed.
55
+
56
+ :param event_id: Index of the sensor.
57
+ :param zone_name: Name of the sensor that reports door open.
58
+ """
59
+
60
+ async def on_door_open_close(
61
+ self, event_id: int, zone_name: str, is_open: bool
62
+ ) -> None:
63
+ """
64
+ Invoked when door sensor reports it opened or closed.
65
+
66
+ :param event_id: Index of the sensor reporting the event.
67
+ :param zone_name: Name of the sensor that reports door open/close.
68
+ :param is_open: Indicates if the door is open.
69
+ """
70
+
71
+ async def on_low_battery(self, event_id: int, zone_name: str) -> None:
72
+ """
73
+ Invoked when a sensor reports it is low on battery.
74
+
75
+ :param event_id: Index of the sensor.
76
+ :param zone_name: Name of the sensor that reports low battery.
77
+ """
78
+
79
+ async def on_alarm(
80
+ self, event_id: int, zone_name: str, is_tampered: bool
81
+ ) -> None:
82
+ """
83
+ Invoked when device triggers the alarm.
84
+
85
+ :param event_id: Index of the sensor.
86
+ :param zone_name: Name of the zone that triggered the alarm.
87
+ """
88
+
89
+ async def on_remote_button_press(
90
+ self, event_id: int, zone_name: str, button: G90RemoteButtonStates
91
+ ) -> None:
92
+ """
93
+ Invoked when a remote button is pressed.
94
+
95
+ Please note there will only be call to the method w/o invoking
96
+ :meth:`G90DeviceNotifications.on_sensor_activity`.
97
+
98
+ :param event_id: Index of the sensor associated with the remote.
99
+ :param zone_name: Name of the sensor that reports remote button press.
100
+ :param button: The button pressed on the remote
101
+ """
102
+
103
+ async def on_sos(
104
+ self, event_id: int, zone_name: str, is_host_sos: bool
105
+ ) -> None:
106
+ """
107
+ Invoked when SOS is triggered.
108
+
109
+ Please note that the panel might not set its status to alarm
110
+ internally, so that :meth:`G90DeviceNotifications` might need an
111
+ explicit call in the derived class to simulate that.
112
+
113
+ :param event_id: Index of the sensor.
114
+ :param zone_name: Name of the sensor that reports SOS.
115
+ :param is_host_sos: Indicates if the SOS is host-initiated.
116
+ """
117
+
118
+ async def on_sensor_change(
119
+ self, sensor_idx: int, sensor_name: str, added: bool
120
+ ) -> None:
121
+ """
122
+ Invoked when a sensor is added or changed on the panel.
123
+
124
+ :param sensor_idx: Index of the sensor.
125
+ :param sensor_name: Name of the sensor.
126
+ :param added: True if the sensor was added.
127
+ """
pyg90alarm/py.typed ADDED
File without changes