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.
@@ -21,48 +21,67 @@
21
21
  """
22
22
  Provides interface to sensors of G90 alarm panel.
23
23
  """
24
-
24
+ from __future__ import annotations
25
25
  import logging
26
- from collections import namedtuple
26
+ from dataclasses import dataclass, asdict, astuple
27
+ from typing import (
28
+ Any, Optional, TYPE_CHECKING, Dict
29
+ )
30
+
27
31
  from enum import IntEnum, IntFlag
28
- from ..definitions.sensors import SENSOR_DEFINITIONS
32
+ from ..definitions.sensors import SENSOR_DEFINITIONS, SensorDefinition
29
33
  from ..const import G90Commands
34
+ if TYPE_CHECKING:
35
+ from ..alarm import G90Alarm, SensorStateCallback, SensorLowBatteryCallback
30
36
 
31
- # Common protocol fields across read and write operations
32
- COMMON_FIELDS = [
33
- 'parent_name',
34
- 'index',
35
- 'room_id',
36
- 'type_id',
37
- 'subtype',
38
- 'timeout',
39
- 'user_flag_data',
40
- 'baudrate',
41
- 'protocol_id',
42
- 'reserved_data',
43
- 'node_count',
44
- ]
45
-
46
- # Incoming (read operation) protocol fields
47
- INCOMING_FIELDS = COMMON_FIELDS + [
48
- 'mask',
49
- 'private_data',
50
- ]
51
-
52
- # Outgoing (write operation) protocol fields
53
- OUTGOING_FIELDS = COMMON_FIELDS + [
54
- 'rx',
55
- 'tx',
56
- 'private_data',
57
- ]
58
37
 
38
+ @dataclass
39
+ class G90SensorCommonData: # pylint:disable=too-many-instance-attributes
40
+ """
41
+ Common protocol fields across read and write operations.
59
42
 
60
- class G90SensorReservedFlags(IntFlag):
43
+ :meta private:
61
44
  """
62
- Reserved flags of the sensor.
45
+ parent_name: str
46
+ index: int
47
+ room_id: int
48
+ type_id: int
49
+ subtype: int
50
+ timeout: int
51
+ user_flag_data: int
52
+ baudrate: int
53
+ protocol_id: int
54
+ reserved_data: int
55
+ node_count: int
56
+
57
+
58
+ @dataclass
59
+ class G90SensorIncomingData(G90SensorCommonData):
60
+ """
61
+ Incoming (read operation) protocol fields.
63
62
 
64
63
  :meta private:
65
64
  """
65
+ mask: int
66
+ private_data: str
67
+
68
+
69
+ @dataclass
70
+ class G90SensorOutgoingData(G90SensorCommonData):
71
+ """
72
+ Outgoing (write operation) protocol fields.
73
+
74
+ :meta private:
75
+ """
76
+ rx: int # pylint:disable=invalid-name
77
+ tx: int # pylint:disable=invalid-name
78
+ private_data: str
79
+
80
+
81
+ class G90SensorReservedFlags(IntFlag):
82
+ """
83
+ Reserved flags of the sensor.
84
+ """
66
85
  NONE = 0
67
86
  CAN_READ = 16
68
87
  CAN_READ_EXT = 32
@@ -72,8 +91,6 @@ class G90SensorReservedFlags(IntFlag):
72
91
  class G90SensorUserFlags(IntFlag):
73
92
  """
74
93
  User flags of the sensor.
75
-
76
- :meta private:
77
94
  """
78
95
  NONE = 0
79
96
  ENABLED = 1
@@ -89,8 +106,6 @@ class G90SensorUserFlags(IntFlag):
89
106
  class G90SensorProtocols(IntEnum):
90
107
  """
91
108
  Protocol types for the sensors.
92
-
93
- :meta private:
94
109
  """
95
110
  RF_1527 = 0
96
111
  RF_2262 = 1
@@ -104,8 +119,6 @@ class G90SensorProtocols(IntEnum):
104
119
  class G90SensorTypes(IntEnum):
105
120
  """
106
121
  Sensor types.
107
-
108
- :meta private:
109
122
  """
110
123
  DOOR = 1
111
124
  GLASS = 2
@@ -160,23 +173,23 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
160
173
  :param kwargs: Pass-through keyword arguments for for interpreting protocol
161
174
  fields
162
175
  """
163
- def __init__(self, *args, parent, subindex, proto_idx, **kwargs):
164
- self._protocol_incoming_data_kls = (
165
- namedtuple('G90SensorIncomingData', INCOMING_FIELDS)
166
- )
167
- self._protocol_outgoing_data_kls = (
168
- namedtuple('G90SensorOutgoingData', OUTGOING_FIELDS)
169
- )
176
+ def __init__(
177
+ self, *args: Any, parent: G90Alarm, subindex: int, proto_idx: int,
178
+ **kwargs: Any
179
+ ) -> None:
180
+ self._protocol_incoming_data_kls = G90SensorIncomingData
181
+ self._protocol_outgoing_data_kls = G90SensorOutgoingData
170
182
  self._protocol_data = self._protocol_incoming_data_kls(*args, **kwargs)
171
183
  self._parent = parent
172
184
  self._subindex = subindex
173
185
  self._occupancy = False
174
- self._state_callback = None
175
- self._low_battery_callback = None
186
+ self._state_callback: Optional[SensorStateCallback] = None
187
+ self._low_battery_callback: Optional[SensorLowBatteryCallback] = None
188
+ self._low_battery = False
176
189
  self._proto_idx = proto_idx
177
- self._extra_data = None
190
+ self._extra_data: Any = None
178
191
 
179
- self._definition = None
192
+ self._definition: Optional[SensorDefinition] = None
180
193
  # Get sensor definition corresponds to the sensor type/subtype if any
181
194
  for s_def in SENSOR_DEFINITIONS:
182
195
  if (
@@ -187,184 +200,208 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
187
200
  break
188
201
 
189
202
  @property
190
- def name(self):
203
+ def name(self) -> str:
191
204
  """
192
- Returns sensor name, accounting for multi-channel entities (single
205
+ Sensor name, accounting for multi-channel entities (single
193
206
  protocol entity results in multiple :class:`.G90Sensor` instances).
194
207
 
195
208
  :return: Sensor name
196
- :rtype: str
197
209
  """
198
210
  if self._protocol_data.node_count == 1:
199
211
  return self._protocol_data.parent_name
200
212
  return f'{self._protocol_data.parent_name}#{self._subindex + 1}'
201
213
 
202
214
  @property
203
- def state_callback(self):
215
+ def state_callback(self) -> Optional[SensorStateCallback]:
204
216
  """
205
- Returns state callback the sensor might have set.
217
+ Callback that is invoked when the sensor changes its state.
206
218
 
207
219
  :return: Sensor state callback
208
- :rtype: object
209
220
  """
210
221
  return self._state_callback
211
222
 
212
223
  @state_callback.setter
213
- def state_callback(self, value):
214
- """
215
- Sets callback for the state changes of the sensor.
216
-
217
- :param object value: Sensor state callback
218
- """
224
+ def state_callback(self, value: SensorStateCallback) -> None:
219
225
  self._state_callback = value
220
226
 
221
227
  @property
222
- def low_battery_callback(self):
228
+ def low_battery_callback(self) -> Optional[SensorLowBatteryCallback]:
223
229
  """
224
- Returns callback the sensor might have set for low battery condition.
230
+ Callback that is invoked when the sensor reports on low battery
231
+ condition.
225
232
 
226
233
  :return: Sensor's low battery callback
227
- :rtype: object
228
234
  """
229
235
  return self._low_battery_callback
230
236
 
231
237
  @low_battery_callback.setter
232
- def low_battery_callback(self, value):
233
- """
234
- Sets callback for the low battery condition reported by the sensor.
235
-
236
- :param object value: Sensor's low battery callback
237
- """
238
+ def low_battery_callback(self, value: SensorLowBatteryCallback) -> None:
238
239
  self._low_battery_callback = value
239
240
 
240
241
  @property
241
- def occupancy(self):
242
+ def occupancy(self) -> bool:
242
243
  """
243
244
  Occupancy (occupied/not occupied, or triggered/not triggered)
244
245
  for the sensor.
245
246
 
246
247
  :return: Sensor occupancy
247
- :rtype: bool
248
248
  """
249
249
  return self._occupancy
250
250
 
251
- @occupancy.setter
252
- def occupancy(self, value):
251
+ def _set_occupancy(self, value: bool) -> None:
253
252
  """
254
- Sets occupancy state for the sensor.
253
+ Sets occupancy state of the sensor.
254
+ Intentionally private, as occupancy state is derived from
255
+ notifications/alerts.
255
256
 
256
- :param bool value: Sensor occupancy
257
+ :param value: Occupancy state
257
258
  """
259
+ _LOGGER.debug(
260
+ "Setting occupancy for sensor index=%s: '%s' %s"
261
+ " (previous value: %s)",
262
+ self.index, self.name, value, self._occupancy
263
+ )
258
264
  self._occupancy = value
259
265
 
260
266
  @property
261
- def protocol(self):
267
+ def protocol(self) -> G90SensorProtocols:
262
268
  """
263
- Returns protocol type of the sensor.
269
+ Protocol type of the sensor.
264
270
 
265
271
  :return: Protocol type
266
- :rtype: int
267
272
  """
268
273
  return G90SensorProtocols(self._protocol_data.protocol_id)
269
274
 
270
275
  @property
271
- def type(self):
276
+ def type(self) -> G90SensorTypes:
272
277
  """
273
- Returns type of the sensor.
278
+ Type of the sensor.
274
279
 
275
280
  :return: Sensor type
276
- :rtype: int
277
281
  """
278
282
  return G90SensorTypes(self._protocol_data.type_id)
279
283
 
280
284
  @property
281
- def reserved(self):
285
+ def subtype(self) -> int:
286
+ """
287
+ Sub-type of the sensor.
288
+
289
+ :return: Sensor sub-type
282
290
  """
283
- Returns reserved flags (read/write mode) for the sensor.
291
+ return self._protocol_data.subtype
292
+
293
+ @property
294
+ def reserved(self) -> G90SensorReservedFlags:
295
+ """
296
+ Reserved flags (read/write mode) for the sensor.
284
297
 
285
298
  :return: Reserved flags
286
- :rtype: int
287
299
  """
288
300
  return G90SensorReservedFlags(self._protocol_data.reserved_data)
289
301
 
290
302
  @property
291
- def user_flag(self):
303
+ def user_flag(self) -> G90SensorUserFlags:
292
304
  """
293
- Returns user flags for the sensor (disabled/enabled, arming type etc).
305
+ User flags for the sensor (disabled/enabled, arming type etc).
294
306
 
295
307
  :return: User flags
296
- :rtype: int
297
308
  """
298
309
  return G90SensorUserFlags(self._protocol_data.user_flag_data)
299
310
 
300
311
  @property
301
- def node_count(self):
312
+ def node_count(self) -> int:
302
313
  """
303
- Returns number of nodes (channels) for the sensor.
314
+ Number of nodes (channels) for the sensor.
304
315
 
305
316
  :return: Number of nodes
306
- :rtype: int
307
317
  """
308
318
  return self._protocol_data.node_count
309
319
 
310
320
  @property
311
- def parent(self):
321
+ def parent(self) -> G90Alarm:
312
322
  """
313
- Returns parent :class:`.G90Alarm` instance the sensor is associated
323
+ Parent instance of alarm panel class the sensor is associated
314
324
  with.
315
325
 
316
- :return: Parent :class:`.G90Alarm` instance
317
- :rtype: :class:`.G90Alarm`
326
+ :return: Parent instance
318
327
  """
319
328
  return self._parent
320
329
 
321
330
  @property
322
- def index(self):
331
+ def index(self) -> int:
323
332
  """
324
- Returns index (internal position) of the sensor in the G90 alarm panel.
333
+ Index (internal position) of the sensor in the alarm panel.
325
334
 
326
335
  :return: Internal sensor position
327
- :rtype: int
328
336
  """
329
337
  return self._protocol_data.index
330
338
 
331
339
  @property
332
- def subindex(self):
340
+ def subindex(self) -> int:
333
341
  """
334
- Returns index of the sensor within multi-node device.
342
+ Index of the sensor within multi-node device.
335
343
 
336
344
  :return: Index of sensor in multi-node device.
337
- :rtype: int
338
345
  """
339
346
  return self._subindex
340
347
 
341
348
  @property
342
- def supports_enable_disable(self):
349
+ def supports_enable_disable(self) -> bool:
343
350
  """
344
351
  Indicates if disabling/enabling the sensor is supported.
345
352
 
346
353
  :return: Support for enabling/disabling the sensor
347
- :rtype: bool
348
354
  """
349
355
  return self._definition is not None
350
356
 
351
357
  @property
352
- def enabled(self):
358
+ def is_wireless(self) -> bool:
359
+ """
360
+ Indicates if the sensor is wireless.
361
+ """
362
+ return self.protocol not in (G90SensorProtocols.CORD,)
363
+
364
+ @property
365
+ def is_low_battery(self) -> bool:
366
+ """
367
+ Indicates if the sensor is reporting low battery.
368
+ """
369
+ return self._low_battery
370
+
371
+ def _set_low_battery(self, value: bool) -> None:
372
+ """
373
+ Sets low battery state of the sensor.
374
+ Intentionally private, as low battery state is derived from
375
+ notifications/alerts.
376
+
377
+ :param value: Low battery state
378
+ """
379
+ _LOGGER.debug(
380
+ "Setting low battery for sensor index=%s '%s': %s"
381
+ " (previous value: %s)",
382
+ self.index, self.name, value, self._low_battery
383
+ )
384
+ self._low_battery = value
385
+
386
+ @property
387
+ def enabled(self) -> bool:
353
388
  """
354
389
  Indicates if the sensor is enabled.
355
390
 
356
391
  :return: If sensor is enabled
357
- :rtype: bool
358
392
  """
359
- return self.user_flag & G90SensorUserFlags.ENABLED
393
+ return self.user_flag & G90SensorUserFlags.ENABLED != 0
360
394
 
361
- async def set_enabled(self, value):
395
+ async def set_enabled(self, value: bool) -> None:
362
396
  """
363
397
  Sets disabled/enabled state of the sensor.
364
398
 
365
- :param bool value: Whether to enable or disable the sensor
399
+ :param value: Whether to enable or disable the sensor
366
400
  """
367
- if not self.supports_enable_disable:
401
+ # Checking private attribute directly, since `mypy` doesn't recognize
402
+ # the check for sensor definition to be defined if done over
403
+ # `self.supports_enable_disable` property
404
+ if not self._definition:
368
405
  _LOGGER.warning(
369
406
  'Manipulating with enable/disable for sensor index=%s'
370
407
  ' is unsupported - no sensor definition for'
@@ -388,7 +425,7 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
388
425
  G90Commands.GETSENSORLIST,
389
426
  start=self._proto_idx, end=self._proto_idx
390
427
  )
391
- sensors = [x async for x in sensors_result]
428
+ sensors = [x.data async for x in sensors_result]
392
429
 
393
430
  # Abort if sensor is not found
394
431
  if not sensors:
@@ -401,7 +438,7 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
401
438
 
402
439
  # Compare actual sensor data from what the sensor has been instantiated
403
440
  # from, and abort the operation if out-of-band changes are detected.
404
- _sensor_pos, sensor_data = sensors[0]
441
+ sensor_data = sensors[0]
405
442
  if self._protocol_incoming_data_kls(
406
443
  *sensor_data
407
444
  ) != self._protocol_data:
@@ -422,7 +459,7 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
422
459
  user_flag &= ~G90SensorUserFlags.ENABLED
423
460
 
424
461
  # Re-instantiate the protocol data with modified user flags
425
- _data = self._protocol_data._asdict()
462
+ _data = asdict(self._protocol_data)
426
463
  _data['user_flag_data'] = user_flag
427
464
  self._protocol_data = self._protocol_incoming_data_kls(**_data)
428
465
 
@@ -454,32 +491,49 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
454
491
  )
455
492
  # Modify the sensor
456
493
  await self._parent.command(
457
- G90Commands.SETSINGLESENSOR, list(outgoing_data)
494
+ G90Commands.SETSINGLESENSOR, list(astuple(outgoing_data))
458
495
  )
459
496
 
460
497
  @property
461
- def extra_data(self):
498
+ def extra_data(self) -> Any:
462
499
  """
463
- Sets extra data for the sensor, that can be used to store
500
+ Extra data for the sensor, that can be used to store
464
501
  caller-specific information and will be carried by the sensor instance.
465
502
  """
466
503
  return self._extra_data
467
504
 
468
505
  @extra_data.setter
469
- def extra_data(self, val):
506
+ def extra_data(self, val: Any) -> None:
470
507
  self._extra_data = val
471
508
 
472
- def __repr__(self):
509
+ def _asdict(self) -> Dict[str, Any]:
510
+ """
511
+ Returns dictionary representation of the sensor.
512
+
513
+ :return: Dictionary representation
514
+ """
515
+ return {
516
+ 'name': self.name,
517
+ 'type': self.type,
518
+ 'subtype': self.subtype,
519
+ 'index': self.index,
520
+ 'subindex': self.subindex,
521
+ 'node_count': self.node_count,
522
+ 'protocol': self.protocol,
523
+ 'occupancy': self.occupancy,
524
+ 'user_flag': self.user_flag,
525
+ 'reserved': self.reserved,
526
+ 'extra_data': self.extra_data,
527
+ 'enabled': self.enabled,
528
+ 'supports_enable_disable': self.supports_enable_disable,
529
+ 'is_wireless': self.is_wireless,
530
+ 'is_low_battery': self.is_low_battery,
531
+ }
532
+
533
+ def __repr__(self) -> str:
473
534
  """
474
535
  Returns string representation of the sensor.
475
536
 
476
537
  :return: String representation
477
- :rtype: str
478
- """
479
- return super().__repr__() + f"(name='{str(self.name)}'" \
480
- f' type={str(self.type)}' \
481
- f' protocol={str(self.protocol)}' \
482
- f' occupancy={self.occupancy}' \
483
- f' user flag={str(self.user_flag)}' \
484
- f' reserved={str(self.reserved)}' \
485
- f" extra_data={str(self.extra_data)})"
538
+ """
539
+ return super().__repr__() + f'({repr(self._asdict())})'
pyg90alarm/history.py CHANGED
@@ -21,9 +21,9 @@
21
21
  """
22
22
  History protocol entity.
23
23
  """
24
-
24
+ from typing import Any, Optional, Dict
25
+ from dataclasses import dataclass
25
26
  from datetime import datetime, timezone
26
- from collections import namedtuple
27
27
  from .const import (
28
28
  G90AlertTypes,
29
29
  G90AlertSources,
@@ -65,52 +65,50 @@ states_mapping = {
65
65
  G90HistoryStates.WIFI_DISCONNECTED,
66
66
  }
67
67
 
68
- INCOMING_FIELDS = [
69
- 'type',
70
- 'event_id',
71
- 'source',
72
- 'state',
73
- 'sensor_name',
74
- 'unix_time',
75
- 'other',
76
- ]
77
- # Class representing the data incoming from the device
78
- ProtocolData = namedtuple('ProtocolData', INCOMING_FIELDS)
68
+
69
+ @dataclass
70
+ class ProtocolData:
71
+ """
72
+ Class representing the data incoming from the device
73
+
74
+ :meta private:
75
+ """
76
+ type: G90AlertTypes
77
+ event_id: G90AlertStateChangeTypes
78
+ source: G90AlertSources
79
+ state: int
80
+ sensor_name: str
81
+ unix_time: int
82
+ other: str
79
83
 
80
84
 
81
85
  class G90History:
82
86
  """
83
- tbd
87
+ Represents a history entry from the alarm panel.
84
88
  """
85
- def __init__(self, *args, **kwargs):
89
+ def __init__(self, *args: Any, **kwargs: Any):
86
90
  self._protocol_data = ProtocolData(*args, **kwargs)
87
91
 
88
92
  @property
89
- def datetime(self):
93
+ def datetime(self) -> datetime:
90
94
  """
91
95
  Date/time of the history entry.
92
-
93
- :rtype: :class:`datetime.datetime`
94
96
  """
95
97
  return datetime.fromtimestamp(
96
98
  self._protocol_data.unix_time, tz=timezone.utc
97
99
  )
98
100
 
99
101
  @property
100
- def type(self):
102
+ def type(self) -> G90AlertTypes:
101
103
  """
102
104
  Type of the history entry.
103
-
104
- :rtype: :class:`.G90AlertTypes`
105
105
  """
106
106
  return G90AlertTypes(self._protocol_data.type)
107
107
 
108
108
  @property
109
- def state(self):
109
+ def state(self) -> G90HistoryStates:
110
110
  """
111
111
  State for the history entry.
112
-
113
- :rtype: :class:`.G90HistoryStates`
114
112
  """
115
113
  # Door open/close type, mapped against `G90AlertStates` using `state`
116
114
  # incoming field
@@ -140,11 +138,9 @@ class G90History:
140
138
  )
141
139
 
142
140
  @property
143
- def source(self):
141
+ def source(self) -> G90AlertSources:
144
142
  """
145
143
  Source of the history entry.
146
-
147
- :rtype: :class:`.G90AlertSources`
148
144
  """
149
145
  # Device state changes or open/close events are mapped against
150
146
  # `G90AlertSources` using `source` incoming field
@@ -162,22 +158,18 @@ class G90History:
162
158
  return G90AlertSources.DEVICE
163
159
 
164
160
  @property
165
- def sensor_name(self):
161
+ def sensor_name(self) -> Optional[str]:
166
162
  """
167
163
  Name of the sensor related to the history entry, might be empty if none
168
164
  associated.
169
-
170
- :rtype: str|None
171
165
  """
172
166
  return self._protocol_data.sensor_name or None
173
167
 
174
168
  @property
175
- def sensor_idx(self):
169
+ def sensor_idx(self) -> Optional[int]:
176
170
  """
177
171
  ID of the sensor related to the history entry, might be empty if none
178
172
  associated.
179
-
180
- :rtype: str|None
181
173
  """
182
174
  # Sensor ID will only be available if entry source is a sensor
183
175
  if self.source == G90AlertSources.SENSOR:
@@ -185,12 +177,10 @@ class G90History:
185
177
 
186
178
  return None
187
179
 
188
- def as_device_alert(self):
180
+ def as_device_alert(self) -> G90DeviceAlert:
189
181
  """
190
182
  Returns the history entry represented as device alert structure,
191
183
  suitable for :meth:`G90DeviceNotifications._handle_alert`.
192
-
193
- :rtype: :class:`.G90DeviceAlert`
194
184
  """
195
185
  return G90DeviceAlert(
196
186
  type=self._protocol_data.type,
@@ -198,21 +188,27 @@ class G90History:
198
188
  source=self._protocol_data.source,
199
189
  state=self._protocol_data.state,
200
190
  zone_name=self._protocol_data.sensor_name,
201
- device_id=None,
191
+ device_id='',
202
192
  unix_time=self._protocol_data.unix_time,
203
- resv4=None,
193
+ resv4=0,
204
194
  other=self._protocol_data.other
205
195
  )
206
196
 
207
- def __repr__(self):
197
+ def _asdict(self) -> Dict[str, Any]:
208
198
  """
209
- Textural representation of the history entry.
199
+ Returns the history entry as dictionary.
200
+ """
201
+ return {
202
+ 'type': self.type,
203
+ 'source': self.source,
204
+ 'state': self.state,
205
+ 'sensor_name': self.sensor_name,
206
+ 'sensor_idx': self.sensor_idx,
207
+ 'datetime': self.datetime,
208
+ }
210
209
 
211
- :rtype: str
210
+ def __repr__(self) -> str:
211
+ """
212
+ Textural representation of the history entry.
212
213
  """
213
- return f'type={repr(self.type)}' \
214
- + f' source={repr(self.source)}' \
215
- + f' state={repr(self.state)}' \
216
- + f' sensor_name={self.sensor_name}' \
217
- + f' sensor_idx={self.sensor_idx}' \
218
- + f' datetime={repr(self.datetime)}'
214
+ return super().__repr__() + f'({repr(self._asdict())})'