pyg90alarm 1.13.0__py3-none-any.whl → 1.15.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyg90alarm/alarm.py CHANGED
@@ -48,8 +48,14 @@ G90HostInfo(host_guid='<...>',
48
48
  wifi_signal_level=100)
49
49
 
50
50
  """
51
+ from __future__ import annotations
51
52
  import asyncio
53
+ from asyncio import Task
52
54
  import logging
55
+ from typing import (
56
+ TYPE_CHECKING, Any, List, Optional, AsyncGenerator,
57
+ Callable, Coroutine, Union
58
+ )
53
59
  from .const import (
54
60
  G90Commands, REMOTE_PORT,
55
61
  REMOTE_TARGETED_DISCOVERY_PORT,
@@ -57,15 +63,17 @@ from .const import (
57
63
  NOTIFICATIONS_PORT,
58
64
  G90ArmDisarmTypes,
59
65
  )
60
- from .base_cmd import G90BaseCommand
61
- from .paginated_result import G90PaginatedResult
66
+ from .base_cmd import (G90BaseCommand, G90BaseCommandData)
67
+ from .paginated_result import G90PaginatedResult, G90PaginatedResponse
62
68
  from .entities.sensor import (G90Sensor, G90SensorTypes)
63
69
  from .entities.device import G90Device
64
70
  from .device_notifications import (
65
71
  G90DeviceNotifications,
66
72
  )
67
- from .discovery import G90Discovery
68
- from .targeted_discovery import G90TargetedDiscovery
73
+ from .discovery import G90Discovery, G90DiscoveredDevice
74
+ from .targeted_discovery import (
75
+ G90TargetedDiscovery, G90DiscoveredDeviceTargeted,
76
+ )
69
77
  from .host_info import G90HostInfo
70
78
  from .host_status import G90HostStatus
71
79
  from .config import (G90AlertConfig, G90AlertConfigFlags)
@@ -76,6 +84,39 @@ from .exceptions import G90Error, G90TimeoutError
76
84
 
77
85
  _LOGGER = logging.getLogger(__name__)
78
86
 
87
+ if TYPE_CHECKING:
88
+ # Type alias for the callback functions available to the user, should be
89
+ # compatible with `G90Callback.Callback` type, since `G90Callback.invoke`
90
+ # is used to invoke them
91
+ AlarmCallback = Union[
92
+ Callable[[int, str, Any], None],
93
+ Callable[[int, str, Any], Coroutine[None, None, None]]
94
+ ]
95
+ DoorOpenCloseCallback = Union[
96
+ Callable[[int, str, bool], None],
97
+ Callable[[int, str, bool], Coroutine[None, None, None]]
98
+ ]
99
+ SensorCallback = Union[
100
+ Callable[[int, str, bool], None],
101
+ Callable[[int, str, bool], Coroutine[None, None, None]]
102
+ ]
103
+ SensorStateCallback = Union[
104
+ Callable[[bool], None],
105
+ Callable[[bool], Coroutine[None, None, None]]
106
+ ]
107
+ LowBatteryCallback = Union[
108
+ Callable[[int, str], None],
109
+ Callable[[int, str], Coroutine[None, None, None]]
110
+ ]
111
+ SensorLowBatteryCallback = Union[
112
+ Callable[[bool], None],
113
+ Callable[[bool], Coroutine[None, None, None]]
114
+ ]
115
+ ArmDisarmCallback = Union[
116
+ Callable[[G90ArmDisarmTypes], None],
117
+ Callable[[G90ArmDisarmTypes], Coroutine[None, None, None]]
118
+ ]
119
+
79
120
 
80
121
  # pylint: disable=too-many-public-methods
81
122
  class G90Alarm(G90DeviceNotifications):
@@ -83,121 +124,117 @@ class G90Alarm(G90DeviceNotifications):
83
124
  """
84
125
  Allows to interact with G90 alarm panel.
85
126
 
86
- :param str host: Hostname or IP address of the alarm panel. Since the
127
+ :param host: Hostname or IP address of the alarm panel. Since the
87
128
  protocol is UDP-based it is ok to use broadcast or directed broadcast
88
129
  addresses, such as `255.255.255.255` or `10.10.10.255` (the latter assumes
89
130
  the device is on `10.10.10.0/24` network)
90
131
  :param port: The UDP port of the device where it listens for the
91
132
  protocol commands on WiFi interface, currently the devices don't allow it
92
133
  to be customized
93
- :type port: int, optional
94
134
  :param reset_occupancy_interval: The interval upon that the sensors are
95
135
  simulated to go into inactive state.
96
- :type reset_occupancy_interval: int, optional
97
136
  """
98
137
  # pylint: disable=too-many-instance-attributes,too-many-arguments
99
- def __init__(self, host, port=REMOTE_PORT,
100
- reset_occupancy_interval=3,
101
- notifications_host='0.0.0.0',
102
- notifications_port=NOTIFICATIONS_PORT):
138
+ def __init__(self, host: str, port: int = REMOTE_PORT,
139
+ reset_occupancy_interval: float = 3.0,
140
+ notifications_host: str = '0.0.0.0',
141
+ notifications_port: int = NOTIFICATIONS_PORT):
103
142
  super().__init__(host=notifications_host, port=notifications_port)
104
143
  self._host = host
105
144
  self._port = port
106
- self._sensors = []
107
- self._devices = []
108
- self._notifications = None
109
- self._sensor_cb = None
110
- self._armdisarm_cb = None
111
- self._door_open_close_cb = None
112
- self._alarm_cb = None
113
- self._low_battery_cb = None
145
+ self._sensors: List[G90Sensor] = []
146
+ self._devices: List[G90Device] = []
147
+ self._notifications: Optional[G90DeviceNotifications] = None
148
+ self._sensor_cb: Optional[SensorCallback] = None
149
+ self._armdisarm_cb: Optional[ArmDisarmCallback] = None
150
+ self._door_open_close_cb: Optional[DoorOpenCloseCallback] = None
151
+ self._alarm_cb: Optional[AlarmCallback] = None
152
+ self._low_battery_cb: Optional[LowBatteryCallback] = None
114
153
  self._reset_occupancy_interval = reset_occupancy_interval
115
- self._alert_config = None
154
+ self._alert_config: Optional[G90AlertConfigFlags] = None
116
155
  self._sms_alert_when_armed = False
117
- self._alert_simulation_task = None
156
+ self._alert_simulation_task: Optional[Task[Any]] = None
118
157
  self._alert_simulation_start_listener_back = False
119
158
 
120
- async def command(self, code, data=None):
159
+ async def command(
160
+ self, code: G90Commands, data: Optional[G90BaseCommandData] = None
161
+ ) -> G90BaseCommandData:
121
162
  """
122
163
  Invokes a command against the alarm panel.
123
164
 
124
165
  :param code: Command code
125
- :type code: :class:`.G90Commands`
126
166
  :param data: Command data
127
- :type data: object or None, optional
128
- :return: :attr:`.G90BaseCommand.result` that contains the result of
129
- command invocation
167
+ :return: The result of command invocation
130
168
  """
131
- cmd = await G90BaseCommand(
169
+ cmd: G90BaseCommand = await G90BaseCommand(
132
170
  self._host, self._port, code, data).process()
133
171
  return cmd.result
134
172
 
135
- def paginated_result(self, code, start=1, end=None):
173
+ def paginated_result(
174
+ self, code: G90Commands, start: int = 1, end: Optional[int] = None
175
+ ) -> AsyncGenerator[G90PaginatedResponse, None]:
136
176
  """
137
177
  Returns asynchronous generator for a paginated command, that is -
138
178
  command operating on a range of records.
139
179
 
140
180
  :param code: Command code
141
- :type code: :class:`.G90Commands`
142
- :param int start: Starting record position (one-based)
143
- :param int end: Ending record position (one-based)
144
- :return: :class:`.G90PaginatedResult` being asynchronous generator
145
- over the result of command invocation. Each access to the generator
146
- yields :class:`.G90PaginatedResponse` instance
181
+ :param start: Starting record position (one-based)
182
+ :param end: Ending record position (one-based)
183
+ :return: Asynchronous generator over the result of command invocation.
147
184
  """
148
185
  return G90PaginatedResult(
149
186
  self._host, self._port, code, start, end
150
187
  ).process()
151
188
 
152
189
  @classmethod
153
- async def discover(cls):
190
+ async def discover(cls) -> List[G90DiscoveredDevice]:
154
191
  """
155
192
  Initiates discovering devices available in the same network segment, by
156
193
  using global broadcast address as the destination.
157
194
 
158
195
  :return: List of discovered devices
159
- :rtype: list[{'guid', 'host', 'port'}]
160
196
  """
161
- return await G90Discovery(
197
+ cmd: G90Discovery = await G90Discovery(
162
198
  port=REMOTE_PORT,
163
199
  host='255.255.255.255'
164
200
  ).process()
201
+ return cmd.devices
165
202
 
166
203
  @classmethod
167
- async def targeted_discover(cls, device_id):
204
+ async def targeted_discover(
205
+ cls, device_id: str
206
+ ) -> List[G90DiscoveredDeviceTargeted]:
168
207
  """
169
208
  Initiates discovering devices available in the same network segment
170
209
  using targeted protocol, that is - specifying target device GUID in the
171
210
  request, so only the specific device should respond to the query.
172
211
 
173
212
  :param device_id: GUID of the target device to discover
174
- :type device_id: str
175
213
  :return: List of discovered devices
176
- :rtype: list[{'guid', 'host', 'port'}]
177
214
  """
178
- return await G90TargetedDiscovery(
215
+ cmd = await G90TargetedDiscovery(
179
216
  device_id=device_id,
180
217
  port=REMOTE_TARGETED_DISCOVERY_PORT,
181
218
  local_port=LOCAL_TARGETED_DISCOVERY_PORT,
182
219
  host='255.255.255.255'
183
220
  ).process()
221
+ return cmd.devices
184
222
 
185
223
  @property
186
- async def sensors(self):
224
+ async def sensors(self) -> List[G90Sensor]:
187
225
  """
188
226
  Property over new :meth:`.get_sensors` method, retained for
189
227
  compatibility.
190
228
  """
191
229
  return await self.get_sensors()
192
230
 
193
- async def get_sensors(self):
231
+ async def get_sensors(self) -> List[G90Sensor]:
194
232
  """
195
233
  Provides list of sensors configured in the device. Please note the list
196
234
  is cached upon first call, so you need to re-instantiate the class to
197
235
  reflect any updates there.
198
236
 
199
237
  :return: List of sensors
200
- :rtype: list(:class:`.G90Sensor`)
201
238
  """
202
239
  if not self._sensors:
203
240
  sensors = self.paginated_result(
@@ -214,14 +251,13 @@ class G90Alarm(G90DeviceNotifications):
214
251
 
215
252
  return self._sensors
216
253
 
217
- async def find_sensor(self, idx, name):
254
+ async def find_sensor(self, idx: int, name: str) -> Optional[G90Sensor]:
218
255
  """
219
256
  Finds sensor by index and name.
220
257
 
221
- :param int idx: Sensor index
222
- :param str name: Sensor name
258
+ :param idx: Sensor index
259
+ :param name: Sensor name
223
260
  :return: Sensor instance
224
- :rtype :class:`.G90Sensor`|None
225
261
  """
226
262
  sensors = await self.get_sensors()
227
263
 
@@ -241,14 +277,14 @@ class G90Alarm(G90DeviceNotifications):
241
277
  return None
242
278
 
243
279
  @property
244
- async def devices(self):
280
+ async def devices(self) -> List[G90Device]:
245
281
  """
246
282
  Property over new :meth:`.get_devices` method, retained for
247
283
  compatibility.
248
284
  """
249
285
  return await self.get_devices()
250
286
 
251
- async def get_devices(self):
287
+ async def get_devices(self) -> List[G90Device]:
252
288
  """
253
289
  Provides list of devices (switches) configured in the device. Please
254
290
  note the list is cached upon first call, so you need to re-instantiate
@@ -257,7 +293,6 @@ class G90Alarm(G90DeviceNotifications):
257
293
  resulting entries.
258
294
 
259
295
  :return: List of devices
260
- :rtype: list(:class:`.G90Device`)
261
296
  """
262
297
  if not self._devices:
263
298
  devices = self.paginated_result(
@@ -283,82 +318,74 @@ class G90Alarm(G90DeviceNotifications):
283
318
  return self._devices
284
319
 
285
320
  @property
286
- async def host_info(self):
321
+ async def host_info(self) -> G90HostInfo:
287
322
  """
288
323
  Property over new :meth:`.get_host_info` method, retained for
289
324
  compatibility.
290
325
  """
291
326
  return await self.get_host_info()
292
327
 
293
- async def get_host_info(self):
328
+ async def get_host_info(self) -> G90HostInfo:
294
329
  """
295
330
  Provides the device information (for example hardware versions, signal
296
331
  levels etc.).
297
332
 
298
333
  :return: Device information
299
- :rtype: Instance of :class:`.G90HostInfo`
300
334
  """
301
335
  res = await self.command(G90Commands.GETHOSTINFO)
302
336
  return G90HostInfo(*res)
303
337
 
304
338
  @property
305
- async def host_status(self):
339
+ async def host_status(self) -> G90HostStatus:
306
340
  """
307
341
  Property over new :meth:`.get_host_status` method, retained for
308
342
  compatibility.
309
343
  """
310
344
  return await self.get_host_status()
311
345
 
312
- async def get_host_status(self):
346
+ async def get_host_status(self) -> G90HostStatus:
313
347
  """
314
348
  Provides the device status (for example, armed or disarmed, configured
315
349
  phone number, product name etc.).
316
350
 
317
351
  :return: Device information
318
- :rtype: Instance of :class:`.G90HostStatus`
319
352
 
320
353
  """
321
354
  res = await self.command(G90Commands.GETHOSTSTATUS)
322
355
  return G90HostStatus(*res)
323
356
 
324
357
  @property
325
- async def alert_config(self):
358
+ async def alert_config(self) -> G90AlertConfigFlags:
326
359
  """
327
360
  Property over new :meth:`.get_alert_config` method, retained for
328
361
  compatibility.
329
362
  """
330
363
  return await self.get_alert_config()
331
364
 
332
- async def get_alert_config(self):
365
+ async def get_alert_config(self) -> G90AlertConfigFlags:
333
366
  """
334
367
  Retrieves the alert configuration flags from the device. Please note
335
368
  the configuration is cached upon first call, so you need to
336
369
  re-instantiate the class to reflect any updates there.
337
370
 
338
- :return: Instance of :class:`.G90AlertConfigFlags` containing the
339
- alerts configured
371
+ :return: The alerts configured
340
372
  """
341
373
  if not self._alert_config:
342
374
  self._alert_config = await self._alert_config_uncached()
343
375
  return self._alert_config
344
376
 
345
- async def _alert_config_uncached(self):
377
+ async def _alert_config_uncached(self) -> G90AlertConfigFlags:
346
378
  """
347
379
  Retrieves the alert configuration flags directly from the device.
348
380
 
349
- :return: Instance of :class:`.G90AlertConfigFlags` containing the
350
- alerts configured
381
+ :return: The alerts configured
351
382
  """
352
383
  res = await self.command(G90Commands.GETNOTICEFLAG)
353
384
  return G90AlertConfig(*res).flags
354
385
 
355
- async def set_alert_config(self, value):
386
+ async def set_alert_config(self, flags: G90AlertConfigFlags) -> None:
356
387
  """
357
- It might be possible to implement the async property setter with
358
- `async_as_sync` decorator, although it might have implications with the
359
- setter not executed if the program terminates earlier. Hence, for the
360
- sake of better predictability this is implemented as regular
361
- (non-property) method
388
+ Sets the alert configuration flags on the device.
362
389
  """
363
390
  # Use uncached method retrieving the alert configuration, to ensure the
364
391
  # actual value retrieved from the device
@@ -367,45 +394,43 @@ class G90Alarm(G90DeviceNotifications):
367
394
  _LOGGER.warning(
368
395
  'Alert configuration changed externally,'
369
396
  ' overwriting (read "%s", will be set to "%s")',
370
- str(alert_config), str(value)
397
+ str(alert_config), str(flags)
371
398
  )
372
- await self.command(G90Commands.SETNOTICEFLAG, [value])
399
+ await self.command(G90Commands.SETNOTICEFLAG, [flags.value])
373
400
  # Update the alert configuration stored
374
- self._alert_config = value
401
+ self._alert_config = flags
375
402
 
376
403
  @property
377
- async def user_data_crc(self):
404
+ async def user_data_crc(self) -> G90UserDataCRC:
378
405
  """
379
406
  Property over new :meth:`.get_user_data_crc` method, retained for
380
407
  compatibility.
381
408
  """
382
409
  return await self.get_user_data_crc()
383
410
 
384
- async def get_user_data_crc(self):
411
+ async def get_user_data_crc(self) -> G90UserDataCRC:
385
412
  """
386
413
  Retieves checksums (CRC) for different on-device databases (history,
387
414
  sensors etc.). Might be used to detect if there is a change in a
388
415
  particular database.
389
416
 
390
- .. note:: Note that due to a bug in the firmware CRC for sensos and
417
+ .. note:: Note that due to a bug in the firmware CRC for sensors and
391
418
  device databases change on each call even if there were no changes
392
419
 
393
- :return: Instance of :class:`.G90UserDataCRC` containing checksums for
394
- different databases
420
+ :return: Checksums for different databases
395
421
  """
396
422
  res = await self.command(G90Commands.GETUSERDATACRC)
397
423
  return G90UserDataCRC(*res)
398
424
 
399
- async def history(self, start=1, count=1):
425
+ async def history(
426
+ self, start: int = 1, count: int = 1
427
+ ) -> List[G90History]:
400
428
  """
401
429
  Retrieves event history from the device.
402
430
 
403
431
  :param start: Starting record number (one-based)
404
- :type start: int
405
432
  :param count: Number of records to retrieve
406
- :type count: int
407
433
  :return: List of history entries
408
- :rtype: list[:class:`.G90History`]
409
434
  """
410
435
  res = self.paginated_result(G90Commands.GETHISTORY,
411
436
  start, count)
@@ -417,18 +442,23 @@ class G90Alarm(G90DeviceNotifications):
417
442
  key=lambda x: x.datetime, reverse=True
418
443
  )
419
444
 
420
- async def on_sensor_activity(self, idx, name, occupancy=True):
445
+ async def on_sensor_activity(
446
+ self, idx: int, name: str, occupancy: bool = True
447
+ ) -> None:
421
448
  """
422
- Callback that invoked both for sensor notifications and door open/close
423
- alerts, since the logic for both is same and could be reused. Please
424
- note the callback is for internal use by the class.
449
+ Invoked both for sensor notifications and door open/close
450
+ alerts, since the logic for both is same and could be reused.
451
+ Fires corresponding callback if set by the user with
452
+ :attr:`.sensor_callback`.
453
+
454
+ Please note the method is for internal use by the class.
425
455
 
426
- :param int idx: The index of the sensor the callback is invoked for.
456
+ :param idx: The index of the sensor the callback is invoked for.
427
457
  Please note the index is a property of sensor, not the direct index of
428
458
  :attr:`sensors` array
429
- :param str name: The name of the sensor, along with the `idx` parameter
459
+ :param name: The name of the sensor, along with the `idx` parameter
430
460
  it is used to look the sensor up from the :attr:`sensors` list
431
- :param bool occupancy: The flag indicating the target sensor state
461
+ :param occupancy: The flag indicating the target sensor state
432
462
  (=occupancy), will always be `True` for callbacks invoked from alarm
433
463
  panel notifications, and reflects actual sensor state for device
434
464
  alerts (only for `door` type sensors, if door open/close alerts are
@@ -437,16 +467,19 @@ class G90Alarm(G90DeviceNotifications):
437
467
  _LOGGER.debug('on_sensor_activity: %s %s %s', idx, name, occupancy)
438
468
  sensor = await self.find_sensor(idx, name)
439
469
  if sensor:
440
- _LOGGER.debug('Setting occupancy to %s (previously %s)',
441
- occupancy, sensor.occupancy)
442
- sensor.occupancy = occupancy
470
+ # Reset the low battery flag since the sensor reports activity,
471
+ # implying it has sufficient battery power
472
+ # pylint: disable=protected-access
473
+ sensor._set_low_battery(False)
474
+ # Set the sensor occupancy
475
+ # pylint: disable=protected-access
476
+ sensor._set_occupancy(occupancy)
443
477
 
444
478
  # Emulate turning off the occupancy - most of sensors will not
445
479
  # notify the device of that, nor the device would emit such
446
480
  # notification itself
447
- def reset_sensor_occupancy(sensor):
448
- _LOGGER.debug('Resetting occupancy for sensor %s', sensor)
449
- sensor.occupancy = False
481
+ def reset_sensor_occupancy(sensor: G90Sensor) -> None:
482
+ sensor._set_occupancy(False)
450
483
  G90Callback.invoke(sensor.state_callback, sensor.occupancy)
451
484
 
452
485
  # Determine if door close notifications are available for the given
@@ -479,25 +512,27 @@ class G90Alarm(G90DeviceNotifications):
479
512
  G90Callback.invoke(self._sensor_cb, idx, name, occupancy)
480
513
 
481
514
  @property
482
- def sensor_callback(self):
515
+ def sensor_callback(self) -> Optional[SensorCallback]:
483
516
  """
484
- Get or set sensor activity callback, the callback is invoked when
485
- sensor activates.
486
-
487
- :type: .. py:function:: ()(idx, name, occupancy)
517
+ Sensor activity callback, which is invoked when sensor activates.
488
518
  """
489
519
  return self._sensor_cb
490
520
 
491
521
  @sensor_callback.setter
492
- def sensor_callback(self, value):
522
+ def sensor_callback(self, value: SensorCallback) -> None:
493
523
  self._sensor_cb = value
494
524
 
495
- async def on_door_open_close(self, event_id, zone_name, is_open):
525
+ async def on_door_open_close(
526
+ self, event_id: int, zone_name: str, is_open: bool
527
+ ) -> None:
496
528
  """
497
- Callback that invoked when door open/close alert comes from the alarm
498
- panel. Please note the callback is for internal use by the class.
529
+ Invoked when door open/close alert comes from the alarm
530
+ panel. Fires corresponding callback if set by the user with
531
+ :attr:`.door_open_close_callback`.
532
+
533
+ Please note the method is for internal use by the class.
499
534
 
500
- .. seealso:: `method`:on_sensor_activity for arguments
535
+ .. seealso:: :meth:`.on_sensor_activity` method for arguments
501
536
  """
502
537
  # Same internal callback is reused both for door open/close alerts and
503
538
  # sensor notifications. The former adds reporting when a door is
@@ -509,30 +544,26 @@ class G90Alarm(G90DeviceNotifications):
509
544
  )
510
545
 
511
546
  @property
512
- def door_open_close_callback(self):
547
+ def door_open_close_callback(self) -> Optional[DoorOpenCloseCallback]:
513
548
  """
514
- Get or set door open/close callback, the callback is invoked when door
549
+ The door open/close callback, which is invoked when door
515
550
  is opened or closed (if corresponding alert is configured on the
516
551
  device).
517
-
518
- :type: .. py:function:: ()(idx: int, name: str, is_open: bool)
519
552
  """
520
553
  return self._door_open_close_cb
521
554
 
522
555
  @door_open_close_callback.setter
523
- def door_open_close_callback(self, value):
524
- """
525
- Sets callback for door open/close events.
526
- """
556
+ def door_open_close_callback(self, value: DoorOpenCloseCallback) -> None:
527
557
  self._door_open_close_cb = value
528
558
 
529
- async def on_armdisarm(self, state):
559
+ async def on_armdisarm(self, state: G90ArmDisarmTypes) -> None:
530
560
  """
531
- Callback that invoked when the device is armed or disarmed. Please note
532
- the callback is for internal use by the class.
561
+ Invoked when the device is armed or disarmed. Fires corresponding
562
+ callback if set by the user with :attr:`.armdisarm_callback`.
563
+
564
+ Please note the method is for internal use by the class.
533
565
 
534
566
  :param state: Device state (armed, disarmed, armed home)
535
- :type state: :class:`G90ArmDisarmTypes`
536
567
  """
537
568
  if self._sms_alert_when_armed:
538
569
  if state == G90ArmDisarmTypes.DISARM:
@@ -549,27 +580,26 @@ class G90Alarm(G90DeviceNotifications):
549
580
  G90Callback.invoke(self._armdisarm_cb, state)
550
581
 
551
582
  @property
552
- def armdisarm_callback(self):
583
+ def armdisarm_callback(self) -> Optional[ArmDisarmCallback]:
553
584
  """
554
- Get or set device arm/disarm callback, the callback is invoked when
555
- device state changes.
556
-
557
- :type: .. py:function:: ()(state: :class:`.G90ArmDisarmTypes`)
585
+ The device arm/disarm callback, which is invoked when device state
586
+ changes.
558
587
  """
559
588
  return self._armdisarm_cb
560
589
 
561
590
  @armdisarm_callback.setter
562
- def armdisarm_callback(self, value):
591
+ def armdisarm_callback(self, value: ArmDisarmCallback) -> None:
563
592
  self._armdisarm_cb = value
564
593
 
565
- async def on_alarm(self, event_id, zone_name):
594
+ async def on_alarm(self, event_id: int, zone_name: str) -> None:
566
595
  """
567
- Callback that invoked when alarm is triggered. Fires alarm callback if
568
- set by the user with `:property:G90Alarm.alarm_callback`.
569
- Please note the callback is for internal use by the class.
596
+ Invoked when alarm is triggered. Fires corresponding callback if set by
597
+ the user with :attr:`.alarm_callback`.
598
+
599
+ Please note the method is for internal use by the class.
570
600
 
571
- :param int: Index of the sensor triggered alarm
572
- :param str: Sensor name
601
+ :param event_id: Index of the sensor triggered alarm
602
+ :param zone_name: Sensor name
573
603
  """
574
604
  sensor = await self.find_sensor(event_id, zone_name)
575
605
  # The callback is still delivered to the caller even if the sensor
@@ -588,67 +618,63 @@ class G90Alarm(G90DeviceNotifications):
588
618
  )
589
619
 
590
620
  @property
591
- def alarm_callback(self):
621
+ def alarm_callback(self) -> Optional[AlarmCallback]:
592
622
  """
593
- Get or set device alarm callback, the callback is invoked when
594
- device alarm triggers.
595
-
596
- :type:
597
- .. py:function:: ()(
598
- sensor_idx: int, sensor_name: str, extra_data: str|None
599
- )
623
+ The device alarm callback, which is invoked when device alarm triggers.
600
624
  """
601
625
  return self._alarm_cb
602
626
 
603
627
  @alarm_callback.setter
604
- def alarm_callback(self, value):
628
+ def alarm_callback(self, value: AlarmCallback) -> None:
605
629
  self._alarm_cb = value
606
630
 
607
- async def on_low_battery(self, event_id, zone_name):
631
+ async def on_low_battery(self, event_id: int, zone_name: str) -> None:
608
632
  """
609
- Callback that invoked when the sensor reports on low battery. Fires
633
+ Invoked when the sensor reports on low battery. Fires
610
634
  corresponding callback if set by the user with
611
- `:property:G90Alarm.on_low_battery_callback`.
612
- Please note the callback is for internal use by the class.
635
+ :attr:`.on_low_battery_callback`.
636
+
637
+ Please note the method is for internal use by the class.
613
638
 
614
- :param int: Index of the sensor triggered alarm
615
- :param str: Sensor name
639
+ :param event_id: Index of the sensor triggered alarm
640
+ :param zone_name: Sensor name
616
641
  """
642
+ _LOGGER.debug('on_low_battery: %s %s', event_id, zone_name)
617
643
  sensor = await self.find_sensor(event_id, zone_name)
618
644
  if sensor:
645
+ # Set the low battery flag on the sensor
646
+ # pylint: disable=protected-access
647
+ sensor._set_low_battery(True)
619
648
  # Invoke per-sensor callback if provided
620
649
  G90Callback.invoke(sensor.low_battery_callback)
621
650
 
622
651
  G90Callback.invoke(self._low_battery_cb, event_id, zone_name)
623
652
 
624
653
  @property
625
- def low_battery_callback(self):
654
+ def low_battery_callback(self) -> Optional[LowBatteryCallback]:
626
655
  """
627
- Get or set low battery callback, the callback is invoked when sensor
628
- the condition is reported by a sensor.
629
-
630
- :type: .. py:function:: ()(idx, name)
656
+ Low battery callback, which is invoked when sensor reports the
657
+ condition.
631
658
  """
632
659
  return self._low_battery_cb
633
660
 
634
661
  @low_battery_callback.setter
635
- def low_battery_callback(self, value):
662
+ def low_battery_callback(self, value: LowBatteryCallback) -> None:
636
663
  self._low_battery_cb = value
637
664
 
638
- async def listen_device_notifications(self):
665
+ async def listen_device_notifications(self) -> None:
639
666
  """
640
667
  Starts internal listener for device notifications/alerts.
641
-
642
668
  """
643
669
  await self.listen()
644
670
 
645
- def close_device_notifications(self):
671
+ def close_device_notifications(self) -> None:
646
672
  """
647
673
  Closes the listener for device notifications/alerts.
648
674
  """
649
675
  self.close()
650
676
 
651
- async def arm_away(self):
677
+ async def arm_away(self) -> None:
652
678
  """
653
679
  Arms the device in away mode.
654
680
  """
@@ -656,7 +682,7 @@ class G90Alarm(G90DeviceNotifications):
656
682
  await self.command(G90Commands.SETHOSTSTATUS,
657
683
  [state])
658
684
 
659
- async def arm_home(self):
685
+ async def arm_home(self) -> None:
660
686
  """
661
687
  Arms the device in home mode.
662
688
  """
@@ -664,7 +690,7 @@ class G90Alarm(G90DeviceNotifications):
664
690
  await self.command(G90Commands.SETHOSTSTATUS,
665
691
  [state])
666
692
 
667
- async def disarm(self):
693
+ async def disarm(self) -> None:
668
694
  """
669
695
  Disarms the device.
670
696
  """
@@ -673,7 +699,7 @@ class G90Alarm(G90DeviceNotifications):
673
699
  [state])
674
700
 
675
701
  @property
676
- def sms_alert_when_armed(self):
702
+ def sms_alert_when_armed(self) -> bool:
677
703
  """
678
704
  When enabled, allows to save costs on SMS by having corresponding alert
679
705
  enabled only when device is armed.
@@ -681,12 +707,12 @@ class G90Alarm(G90DeviceNotifications):
681
707
  return self._sms_alert_when_armed
682
708
 
683
709
  @sms_alert_when_armed.setter
684
- def sms_alert_when_armed(self, value):
710
+ def sms_alert_when_armed(self, value: bool) -> None:
685
711
  self._sms_alert_when_armed = value
686
712
 
687
713
  async def start_simulating_alerts_from_history(
688
- self, interval=5, history_depth=5
689
- ):
714
+ self, interval: float = 5, history_depth: int = 5
715
+ ) -> None:
690
716
  """
691
717
  Starts the separate task to simulate device alerts from history
692
718
  entries.
@@ -695,9 +721,9 @@ class G90Alarm(G90DeviceNotifications):
695
721
  notifications will not be processed thus resulting in possible
696
722
  duplicated if those could be received from the network.
697
723
 
698
- :param int interval: Interval (in seconds) between polling for newer
724
+ :param interval: Interval (in seconds) between polling for newer
699
725
  history entities
700
- :param int history_depth: Amount of history entries to fetch during
726
+ :param history_depth: Amount of history entries to fetch during
701
727
  each polling cycle
702
728
  """
703
729
  # Remember if device notifications listener has been started already
@@ -710,7 +736,7 @@ class G90Alarm(G90DeviceNotifications):
710
736
  self._simulate_alerts_from_history(interval, history_depth)
711
737
  )
712
738
 
713
- async def stop_simulating_alerts_from_history(self):
739
+ async def stop_simulating_alerts_from_history(self) -> None:
714
740
  """
715
741
  Stops the task simulating device alerts from history entries.
716
742
 
@@ -728,7 +754,9 @@ class G90Alarm(G90DeviceNotifications):
728
754
  if self._alert_simulation_start_listener_back:
729
755
  await self.listen()
730
756
 
731
- async def _simulate_alerts_from_history(self, interval, history_depth):
757
+ async def _simulate_alerts_from_history(
758
+ self, interval: float, history_depth: int
759
+ ) -> None:
732
760
  """
733
761
  Periodically fetches history entries from the device and simulates
734
762
  device alerts off of those.
@@ -736,7 +764,7 @@ class G90Alarm(G90DeviceNotifications):
736
764
  Only the history entries occur after the process is started are
737
765
  handled, to avoid triggering callbacks retrospectively.
738
766
 
739
- See :method:`start_simulating_alerts_from_history` for the parameters.
767
+ See :meth:`.start_simulating_alerts_from_history` for the parameters.
740
768
  """
741
769
  last_history_ts = None
742
770
 
@@ -773,7 +801,7 @@ class G90Alarm(G90DeviceNotifications):
773
801
  for item in reversed(history):
774
802
  # Process only the entries newer than one been recorded as
775
803
  # most recent one
776
- if item.datetime > last_history_ts:
804
+ if last_history_ts and item.datetime > last_history_ts:
777
805
  _LOGGER.debug(
778
806
  'Found newer history entry: %s, simulating alert',
779
807
  repr(item)