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,891 @@
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
+ Provides interface to sensors of G90 alarm panel.
23
+ """
24
+ from __future__ import annotations
25
+ import logging
26
+ from dataclasses import dataclass, asdict, astuple
27
+ from typing import (
28
+ Any, Optional, TYPE_CHECKING, Dict
29
+ )
30
+
31
+ from enum import IntEnum, IntFlag
32
+ from ..definitions.base import (
33
+ G90PeripheralDefinition,
34
+ G90PeripheralProtocols, G90PeripheralTypes,
35
+ )
36
+ from ..definitions.sensors import (
37
+ G90SensorDefinitions,
38
+ )
39
+ from ..const import G90Commands
40
+ from .base_entity import G90BaseEntity
41
+ from ..callback import G90CallbackList
42
+ from ..exceptions import G90PeripheralDefinitionNotFound
43
+ if TYPE_CHECKING:
44
+ from ..alarm import (
45
+ G90Alarm, SensorStateCallback, SensorLowBatteryCallback,
46
+ SensorDoorOpenWhenArmingCallback, SensorTamperCallback,
47
+ )
48
+
49
+
50
+ @dataclass
51
+ class G90SensorCommonData: # pylint:disable=too-many-instance-attributes
52
+ """
53
+ Common protocol fields across read and write operations.
54
+
55
+ :meta private:
56
+ """
57
+ parent_name: str
58
+ index: int
59
+ room_id: int
60
+ type_id: int
61
+ subtype: int
62
+ timeout: int
63
+ user_flags_data: int
64
+ baudrate: int
65
+ protocol_id: int
66
+ reserved_data: int
67
+ node_count: int
68
+
69
+
70
+ @dataclass
71
+ class G90SensorIncomingData(G90SensorCommonData):
72
+ """
73
+ Incoming (read operation) protocol fields.
74
+
75
+ :meta private:
76
+ """
77
+ mask: int
78
+ private_data: str
79
+
80
+
81
+ @dataclass
82
+ class G90SensorOutgoingData(G90SensorCommonData):
83
+ """
84
+ Outgoing (write operation) protocol fields.
85
+
86
+ :meta private:
87
+ """
88
+ rx: int # pylint:disable=invalid-name
89
+ tx: int # pylint:disable=invalid-name
90
+ private_data: str
91
+
92
+
93
+ class G90SensorReservedFlags(IntFlag):
94
+ """
95
+ Reserved flags of the sensor.
96
+ """
97
+ NONE = 0
98
+ CAN_READ = 16
99
+ CAN_READ_EXT = 32
100
+ CAN_WRITE = 1
101
+
102
+
103
+ class G90SensorUserFlags(IntFlag):
104
+ """
105
+ User flags of the sensor.
106
+ """
107
+ NONE = 0
108
+ ENABLED = 1
109
+ ARM_DELAY = 2
110
+ DETECT_DOOR = 4
111
+ DOOR_CHIME = 8
112
+ INDEPENDENT_ZONE = 16
113
+ ALERT_WHEN_AWAY_AND_HOME = 32
114
+ ALERT_WHEN_AWAY = 64
115
+ SUPPORTS_UPDATING_SUBTYPE = 512 # Only relevant for cord sensors
116
+ # Flags that can be set by the user
117
+ USER_SETTABLE = (
118
+ ENABLED
119
+ | ARM_DELAY
120
+ | DETECT_DOOR
121
+ | DOOR_CHIME
122
+ | INDEPENDENT_ZONE
123
+ | ALERT_WHEN_AWAY_AND_HOME
124
+ | ALERT_WHEN_AWAY
125
+ )
126
+
127
+
128
+ class G90SensorAlertModes(IntEnum):
129
+ """
130
+ Dedicated alert modes for the sensors (subset of user flags).
131
+ """
132
+ ALERT_ALWAYS = 0
133
+ ALERT_WHEN_AWAY = 1
134
+ ALERT_WHEN_AWAY_AND_HOME = 2
135
+
136
+
137
+ # Mapping of relevant user flags to alert modes
138
+ ALERT_MODES_MAP_BY_FLAG = {
139
+ # No 'when away' or 'when away and home' flag set means 'alert always
140
+ G90SensorUserFlags.NONE:
141
+ G90SensorAlertModes.ALERT_ALWAYS,
142
+ G90SensorUserFlags.ALERT_WHEN_AWAY:
143
+ G90SensorAlertModes.ALERT_WHEN_AWAY,
144
+ G90SensorUserFlags.ALERT_WHEN_AWAY_AND_HOME:
145
+ G90SensorAlertModes.ALERT_WHEN_AWAY_AND_HOME,
146
+ }
147
+
148
+ # Reversed mapping of alert modes to corresponding user flags
149
+ ALERT_MODES_MAP_BY_VALUE = dict(
150
+ zip(
151
+ ALERT_MODES_MAP_BY_FLAG.values(),
152
+ ALERT_MODES_MAP_BY_FLAG.keys()
153
+ )
154
+ )
155
+
156
+
157
+ _LOGGER = logging.getLogger(__name__)
158
+
159
+
160
+ # pylint: disable=too-many-public-methods
161
+ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
162
+ """
163
+ Interacts with sensor on G90 alarm panel.
164
+
165
+ :param args: Pass-through positional arguments for for interpreting
166
+ protocol fields
167
+ :param parent: Instance of :class:`.G90Alarm` the sensor is associated
168
+ with
169
+ :type parent: :class:`.G90Alarm`
170
+ :param int subindex: Index of the sensor within multi-channel devices
171
+ (those having multiple nodes)
172
+ :param int proto_idx: Index of the sensor within list of sensors as
173
+ retrieved from the alarm panel
174
+ :param kwargs: Pass-through keyword arguments for for interpreting protocol
175
+ fields
176
+ """
177
+ def __init__(
178
+ self, *args: Any, parent: G90Alarm, subindex: int, proto_idx: int,
179
+ **kwargs: Any
180
+ ) -> None:
181
+ self._protocol_incoming_data_kls = G90SensorIncomingData
182
+ self._protocol_outgoing_data_kls = G90SensorOutgoingData
183
+ self._protocol_data = self._protocol_incoming_data_kls(*args, **kwargs)
184
+ self._parent = parent
185
+ self._subindex = subindex
186
+ self._occupancy = False
187
+ self._state_callback: G90CallbackList[SensorStateCallback] = (
188
+ G90CallbackList()
189
+ )
190
+ self._low_battery_callback: G90CallbackList[
191
+ SensorLowBatteryCallback
192
+ ] = G90CallbackList()
193
+ self._low_battery = False
194
+ self._tampered = False
195
+ self._door_open_when_arming_callback: G90CallbackList[
196
+ SensorDoorOpenWhenArmingCallback
197
+ ] = G90CallbackList()
198
+ self._tamper_callback: G90CallbackList[SensorTamperCallback] = (
199
+ G90CallbackList()
200
+ )
201
+ self._door_open_when_arming = False
202
+ self._proto_idx = proto_idx
203
+ self._extra_data: Any = None
204
+ self._unavailable = False
205
+ self._definition: Optional[G90PeripheralDefinition] = None
206
+
207
+ @property
208
+ def definition(self) -> Optional[G90PeripheralDefinition]:
209
+ """
210
+ Returns the definition for the sensor.
211
+
212
+ :return: Sensor definition
213
+ """
214
+ if not self._definition:
215
+ # No definition has been cached, try to find it by type, subtype
216
+ # and protocol
217
+ try:
218
+ self._definition = (
219
+ G90SensorDefinitions.get_by_id(
220
+ self.type, self.subtype, self.protocol
221
+ )
222
+ )
223
+ except G90PeripheralDefinitionNotFound:
224
+ return None
225
+ return self._definition
226
+
227
+ def update(self, obj: G90Sensor) -> None:
228
+ """
229
+ Updates sensor from another instance.
230
+
231
+ :param obj: Sensor instance to update from
232
+ """
233
+ self._protocol_data = obj.protocol_data
234
+ self._proto_idx = obj.proto_idx
235
+
236
+ @property
237
+ def name(self) -> str:
238
+ """
239
+ Sensor name, accounting for multi-channel entities (single
240
+ protocol entity results in multiple :class:`.G90Sensor` instances).
241
+
242
+ :return: Sensor name
243
+ """
244
+ if self._protocol_data.node_count == 1:
245
+ return self._protocol_data.parent_name
246
+ return f'{self._protocol_data.parent_name}#{self._subindex + 1}'
247
+
248
+ @property
249
+ def state_callback(self) -> G90CallbackList[SensorStateCallback]:
250
+ """
251
+ Callback that is invoked when the sensor changes its state.
252
+
253
+ :return: Sensor state callback
254
+
255
+ .. seealso:: :attr:`G90Alarm.sensor_callback` for compatiblity notes
256
+ """
257
+ return self._state_callback
258
+
259
+ @state_callback.setter
260
+ def state_callback(self, value: SensorStateCallback) -> None:
261
+ self._state_callback.add(value)
262
+
263
+ @property
264
+ def low_battery_callback(
265
+ self
266
+ ) -> G90CallbackList[SensorLowBatteryCallback]:
267
+ """
268
+ Callback that is invoked when the sensor reports on low battery
269
+ condition.
270
+
271
+ :return: Sensor's low battery callback
272
+
273
+ .. seealso:: :attr:`G90Alarm.sensor_callback` for compatiblity notes
274
+ """
275
+ return self._low_battery_callback
276
+
277
+ @low_battery_callback.setter
278
+ def low_battery_callback(self, value: SensorLowBatteryCallback) -> None:
279
+ self._low_battery_callback.add(value)
280
+
281
+ @property
282
+ def door_open_when_arming_callback(
283
+ self
284
+ ) -> G90CallbackList[SensorDoorOpenWhenArmingCallback]:
285
+ """
286
+ Callback that is invoked when the sensor reports on open door
287
+ condition when arming.
288
+
289
+ :return: Sensor's door open when arming callback
290
+
291
+ .. seealso:: :attr:`G90Alarm.sensor_callback` for compatiblity notes
292
+ """
293
+ return self._door_open_when_arming_callback
294
+
295
+ @door_open_when_arming_callback.setter
296
+ def door_open_when_arming_callback(
297
+ self, value: SensorDoorOpenWhenArmingCallback
298
+ ) -> None:
299
+ self._door_open_when_arming_callback.add(value)
300
+
301
+ @property
302
+ def tamper_callback(self) -> G90CallbackList[SensorTamperCallback]:
303
+ """
304
+ Callback that is invoked when the sensor reports being tampered.
305
+
306
+ :return: Sensor's tamper callback
307
+
308
+ .. seealso:: :attr:`G90Alarm.sensor_callback` for compatiblity notes
309
+ """
310
+ return self._tamper_callback
311
+
312
+ @tamper_callback.setter
313
+ def tamper_callback(self, value: SensorTamperCallback) -> None:
314
+ self._tamper_callback.add(value)
315
+
316
+ @property
317
+ def occupancy(self) -> bool:
318
+ """
319
+ Occupancy (occupied/not occupied, or triggered/not triggered)
320
+ for the sensor.
321
+
322
+ :return: Sensor occupancy
323
+ """
324
+ return self._occupancy
325
+
326
+ def _set_occupancy(self, value: bool) -> None:
327
+ """
328
+ Sets occupancy state of the sensor.
329
+ Intentionally private, as occupancy state is derived from
330
+ notifications/alerts.
331
+
332
+ :param value: Occupancy state
333
+ """
334
+ _LOGGER.debug(
335
+ "Setting occupancy for sensor index=%s: '%s' %s"
336
+ " (previous value: %s)",
337
+ self.index, self.name, value, self._occupancy
338
+ )
339
+ self._occupancy = value
340
+
341
+ @property
342
+ def protocol(self) -> G90PeripheralProtocols:
343
+ """
344
+ Protocol type of the sensor.
345
+
346
+ :return: Protocol type
347
+ """
348
+ return G90PeripheralProtocols(self._protocol_data.protocol_id)
349
+
350
+ @property
351
+ def type(self) -> G90PeripheralTypes:
352
+ """
353
+ Type of the sensor.
354
+
355
+ :return: Sensor type
356
+ """
357
+ return G90PeripheralTypes(self._protocol_data.type_id)
358
+
359
+ @property
360
+ def subtype(self) -> int:
361
+ """
362
+ Sub-type of the sensor.
363
+
364
+ :return: Sensor sub-type
365
+ """
366
+ return self._protocol_data.subtype
367
+
368
+ @property
369
+ def reserved(self) -> G90SensorReservedFlags:
370
+ """
371
+ Reserved flags (read/write mode) for the sensor.
372
+
373
+ :return: Reserved flags
374
+ """
375
+ return G90SensorReservedFlags(self._protocol_data.reserved_data)
376
+
377
+ @property
378
+ def user_flag(self) -> G90SensorUserFlags:
379
+ """
380
+ User flags for the sensor, retained for compatibility - please use
381
+ `:attr:user_flags` instead.
382
+
383
+ :return: User flags
384
+ """
385
+ return self.user_flags
386
+
387
+ @property
388
+ def user_flags(self) -> G90SensorUserFlags:
389
+ """
390
+ User flags for the sensor (disabled/enabled, arming type etc).
391
+
392
+ :return: User flags
393
+ """
394
+ return G90SensorUserFlags(self._protocol_data.user_flags_data)
395
+
396
+ @property
397
+ def node_count(self) -> int:
398
+ """
399
+ Number of nodes (channels) for the sensor.
400
+
401
+ :return: Number of nodes
402
+ """
403
+ return self._protocol_data.node_count
404
+
405
+ @property
406
+ def parent(self) -> G90Alarm:
407
+ """
408
+ Parent instance of alarm panel class the sensor is associated
409
+ with.
410
+
411
+ :return: Parent instance
412
+ """
413
+ return self._parent
414
+
415
+ @property
416
+ def index(self) -> int:
417
+ """
418
+ Index (internal position) of the sensor in the alarm panel.
419
+
420
+ :return: Internal sensor position
421
+ """
422
+ return self._protocol_data.index
423
+
424
+ @property
425
+ def subindex(self) -> int:
426
+ """
427
+ Index of the sensor within multi-node device.
428
+
429
+ :return: Index of sensor in multi-node device.
430
+ """
431
+ return self._subindex
432
+
433
+ @property
434
+ def proto_idx(self) -> int:
435
+ """
436
+ Index of the sensor within list of sensors as retrieved from the alarm
437
+ panel.
438
+
439
+ :return: Index of sensor in list of sensors.
440
+ """
441
+ return self._proto_idx
442
+
443
+ @property
444
+ def type_name(self) -> Optional[str]:
445
+ """
446
+ Type of the sensor.
447
+
448
+ :return: Type
449
+ """
450
+ if not self.definition:
451
+ return None
452
+
453
+ return self.definition.name
454
+
455
+ @property
456
+ def supports_updates(self) -> bool:
457
+ """
458
+ Indicates if the sensor supports updates.
459
+
460
+ :return: Support for updates
461
+ """
462
+ if not self.definition:
463
+ _LOGGER.warning(
464
+ 'Manipulating with user flags for sensor index=%s'
465
+ ' is unsupported - no sensor definition for'
466
+ ' type=%s, subtype=%s, protocol=%s',
467
+ self.index, self.type, self.subtype, self.protocol
468
+ )
469
+ return False
470
+ return True
471
+
472
+ @property
473
+ def supports_enable_disable(self) -> bool:
474
+ """
475
+ Indicates if disabling/enabling the sensor is supported.
476
+
477
+ :return: Support for enabling/disabling the sensor
478
+ """
479
+ return self.supports_updates
480
+
481
+ @property
482
+ def protocol_data(self) -> G90SensorIncomingData:
483
+ """
484
+ Protocol data of the sensor.
485
+
486
+ :return: Protocol data
487
+ """
488
+ return self._protocol_data
489
+
490
+ @property
491
+ def is_wireless(self) -> bool:
492
+ """
493
+ Indicates if the sensor is wireless.
494
+ """
495
+ return self.protocol not in (G90PeripheralProtocols.CORD,)
496
+
497
+ @property
498
+ def is_low_battery(self) -> bool:
499
+ """
500
+ Indicates if the sensor is reporting low battery.
501
+
502
+ The condition is cleared when the sensor reports activity (i.e. is no
503
+ longer low on battery as it is able to report the activity).
504
+ """
505
+ return self._low_battery
506
+
507
+ def _set_low_battery(self, value: bool) -> None:
508
+ """
509
+ Sets low battery state of the sensor.
510
+
511
+ Intentionally private, as low battery state is derived from
512
+ notifications/alerts.
513
+
514
+ :param value: Low battery state
515
+ """
516
+ _LOGGER.debug(
517
+ "Setting low battery for sensor index=%s '%s': %s"
518
+ " (previous value: %s)",
519
+ self.index, self.name, value, self._low_battery
520
+ )
521
+ self._low_battery = value
522
+
523
+ @property
524
+ def is_tampered(self) -> bool:
525
+ """
526
+ Indicates if the sensor has been tampered.
527
+
528
+ The condition is cleared when panel is armed/disarmed next time.
529
+ """
530
+ return self._tampered
531
+
532
+ def _set_tampered(self, value: bool) -> None:
533
+ """
534
+ Sets tamper state of the sensor.
535
+
536
+ Intentionally private, as tamper state is derived from
537
+ notifications/alerts.
538
+
539
+ :param value: Tamper state
540
+ """
541
+ _LOGGER.debug(
542
+ "Setting tamper for sensor index=%s '%s': %s"
543
+ " (previous value: %s)",
544
+ self.index, self.name, value, self._tampered
545
+ )
546
+ self._tampered = value
547
+
548
+ @property
549
+ def is_door_open_when_arming(self) -> bool:
550
+ """
551
+ Indicates if the sensor reports on open door when arming.
552
+
553
+ The condition is cleared when panel is armed/disarmed next time.
554
+ """
555
+ return self._door_open_when_arming
556
+
557
+ def _set_door_open_when_arming(self, value: bool) -> None:
558
+ """
559
+ Sets door open state of the sensor when arming.
560
+
561
+ Intentionally private, as door open state is derived from
562
+ notifications/alerts.
563
+
564
+ :param value: Door open state
565
+ """
566
+ _LOGGER.debug(
567
+ "Setting door open when arming for sensor index=%s '%s': %s"
568
+ " (previous value: %s)",
569
+ self.index, self.name, value, self._door_open_when_arming
570
+ )
571
+ self._door_open_when_arming = value
572
+
573
+ async def set_user_flag(self, value: G90SensorUserFlags) -> None:
574
+ """
575
+ Sets user flags of the sensor, retained for compatibility - please use
576
+ `:meth:set_user_flags` instead.
577
+ """
578
+ await self.set_user_flags(value)
579
+
580
+ async def set_user_flags(self, value: G90SensorUserFlags) -> None:
581
+ """
582
+ Sets user flags of the sensor.
583
+
584
+ :param value: User flags to set, values other than
585
+ :attr:`.G90SensorUserFlags.USER_SETTABLE` will be ignored and
586
+ preserved from existing sensor flags.
587
+ """
588
+ if not self.supports_updates:
589
+ return
590
+
591
+ # Checking private attribute directly, since `mypy` doesn't recognize
592
+ # the check for sensor definition is done over
593
+ # `self.supports_updates` property
594
+ if not self.definition:
595
+ return
596
+
597
+ if value & ~G90SensorUserFlags.USER_SETTABLE:
598
+ _LOGGER.warning(
599
+ 'User flags for sensor index=%s contain non-user settable'
600
+ ' flags, those will be ignored: %s',
601
+ self.index, repr(value & ~G90SensorUserFlags.USER_SETTABLE)
602
+ )
603
+
604
+ # Refresh actual sensor data from the alarm panel before modifying it.
605
+ # This implies the sensor is at the same position within sensor list
606
+ # (`_proto_index`) as it has been read initially from the alarm panel
607
+ # when instantiated.
608
+ _LOGGER.debug(
609
+ 'Refreshing sensor at index=%s, position in protocol list=%s',
610
+ self.index, self.proto_idx
611
+ )
612
+ sensors_result = self.parent.paginated_result(
613
+ G90Commands.GETSENSORLIST,
614
+ start=self.proto_idx, end=self.proto_idx
615
+ )
616
+ sensors = [x.data async for x in sensors_result]
617
+
618
+ # Abort if sensor is not found
619
+ if not sensors:
620
+ _LOGGER.error(
621
+ 'Sensor index=%s not found when attempting to set its'
622
+ ' user flag',
623
+ self.index,
624
+ )
625
+ return
626
+
627
+ # Compare actual sensor data from what the sensor has been instantiated
628
+ # from, and abort the operation if out-of-band changes are detected.
629
+ sensor_data = sensors[0]
630
+ if self._protocol_incoming_data_kls(
631
+ *sensor_data
632
+ ) != self._protocol_data:
633
+ _LOGGER.error(
634
+ "Sensor index=%s '%s' has been changed externally,"
635
+ " refusing to alter its user flag",
636
+ self.index,
637
+ self.name
638
+ )
639
+ return
640
+
641
+ prev_user_flags = self.user_flags
642
+
643
+ # Re-instantiate the protocol data with modified user flags
644
+ _data = asdict(self._protocol_data)
645
+ _data['user_flags_data'] = (
646
+ # Preserve flags that are not user-settable
647
+ self.user_flags & ~G90SensorUserFlags.USER_SETTABLE
648
+ ) | (
649
+ # Combine them with the new user-settable flags
650
+ value & G90SensorUserFlags.USER_SETTABLE
651
+ )
652
+ self._protocol_data = self._protocol_incoming_data_kls(**_data)
653
+
654
+ if self.user_flags == prev_user_flags:
655
+ _LOGGER.debug(
656
+ 'Sensor index=%s: user flags %s have not changed,'
657
+ ' skipping update',
658
+ self._protocol_data.index, repr(prev_user_flags)
659
+ )
660
+ return
661
+
662
+ _LOGGER.debug(
663
+ 'Sensor index=%s: previous user flags %s, resulting flags %s',
664
+ self._protocol_data.index,
665
+ repr(prev_user_flags),
666
+ repr(self.user_flags)
667
+ )
668
+
669
+ # Generate protocol data from write operation, deriving values either
670
+ # from fields read from the sensor, or from the sensor definition - not
671
+ # all fields are present during read, only in definition.
672
+ outgoing_data = self._protocol_outgoing_data_kls(
673
+ parent_name=self._protocol_data.parent_name,
674
+ index=self._protocol_data.index,
675
+ room_id=self._protocol_data.room_id,
676
+ type_id=self._protocol_data.type_id,
677
+ subtype=self._protocol_data.subtype,
678
+ timeout=self._protocol_data.timeout,
679
+ user_flags_data=self._protocol_data.user_flags_data,
680
+ baudrate=self._protocol_data.baudrate,
681
+ protocol_id=self._protocol_data.protocol_id,
682
+ reserved_data=self.definition.reserved_data,
683
+ node_count=self._protocol_data.node_count,
684
+ rx=self.definition.rx,
685
+ tx=self.definition.tx,
686
+ private_data=self.definition.private_data,
687
+ )
688
+ # Modify the sensor
689
+ await self._parent.command(
690
+ G90Commands.SETSINGLESENSOR, list(astuple(outgoing_data))
691
+ )
692
+
693
+ def get_flag(self, flag: G90SensorUserFlags) -> bool:
694
+ """
695
+ Gets the user flag for the sensor.
696
+
697
+ :param flag: User flag to get
698
+ :return: User flag value
699
+ """
700
+ return flag in self.user_flag
701
+
702
+ async def set_flag(
703
+ self, flag: G90SensorUserFlags, value: bool
704
+ ) -> None:
705
+ """
706
+ Sets the user flag for the sensor.
707
+
708
+ :param flag: User flag to set
709
+ :param value: New value for the user flag
710
+ """
711
+ # Skip updating the flag if it has the desired value
712
+ if self.get_flag(flag) == value:
713
+ _LOGGER.debug(
714
+ 'Sensor index=%s: user flag %s has not changed,'
715
+ ' skipping update',
716
+ self._protocol_data.index, repr(flag)
717
+ )
718
+ return
719
+
720
+ # Invert corresponding user flag and set it
721
+ user_flag = self.user_flag ^ flag
722
+ await self.set_user_flag(user_flag)
723
+
724
+ @property
725
+ def enabled(self) -> bool:
726
+ """
727
+ Indicates if the sensor is enabled, using `:meth:get_user_flag` instead
728
+ is preferred.
729
+
730
+ :return: If sensor is enabled
731
+ """
732
+ return self.get_flag(G90SensorUserFlags.ENABLED)
733
+
734
+ async def set_enabled(self, value: bool) -> None:
735
+ """
736
+ Sets the sensor enabled/disabled, using `:meth:set_user_flag` instead
737
+ is preferred.
738
+
739
+ :param value: New the sensor should be enabled
740
+ """
741
+ await self.set_flag(G90SensorUserFlags.ENABLED, value)
742
+
743
+ @property
744
+ def alert_mode(self) -> G90SensorAlertModes:
745
+ """
746
+ Alert mode for the sensor.
747
+
748
+ :return: Alert mode
749
+ """
750
+ # Filter out irrelevant flags
751
+ mode = self.user_flag & (
752
+ G90SensorUserFlags.ALERT_WHEN_AWAY
753
+ | G90SensorUserFlags.ALERT_WHEN_AWAY_AND_HOME
754
+ )
755
+ # Map the relevant user flags to alert mode
756
+ result = ALERT_MODES_MAP_BY_FLAG.get(mode, None)
757
+
758
+ if result is None:
759
+ raise ValueError(
760
+ f"Unknown alert mode for sensor {self.name}: {mode}"
761
+ f" (user flag: {self.user_flag})"
762
+ )
763
+
764
+ return result
765
+
766
+ async def set_alert_mode(self, value: G90SensorAlertModes) -> None:
767
+ """
768
+ Sets the sensor alert mode.
769
+ """
770
+ # Skip update if the value is already set to the requested one
771
+ if self.alert_mode == value:
772
+ _LOGGER.debug(
773
+ 'Sensor index=%s: alert mode %s has not changed,'
774
+ ' skipping update',
775
+ self._protocol_data.index, repr(value)
776
+ )
777
+ return
778
+
779
+ # Map the alert mode to user flag value
780
+ result = ALERT_MODES_MAP_BY_VALUE.get(value, None)
781
+
782
+ if result is None:
783
+ raise ValueError(
784
+ f"Attempting to set alert mode for sensor {self.name} to"
785
+ f" unknown value '{value}'"
786
+ )
787
+
788
+ # Add the mapped value over the user flags, filtering out previous
789
+ # value of the alert mode
790
+ user_flags = self.user_flag & ~(
791
+ G90SensorUserFlags.ALERT_WHEN_AWAY
792
+ | G90SensorUserFlags.ALERT_WHEN_AWAY_AND_HOME
793
+ ) | result
794
+ # Set the updated user flags
795
+ await self.set_user_flags(user_flags)
796
+
797
+ @property
798
+ def extra_data(self) -> Any:
799
+ """
800
+ Extra data for the sensor, that can be used to store
801
+ caller-specific information and will be carried by the sensor instance.
802
+ """
803
+ return self._extra_data
804
+
805
+ @extra_data.setter
806
+ def extra_data(self, val: Any) -> None:
807
+ self._extra_data = val
808
+
809
+ @property
810
+ def is_unavailable(self) -> bool:
811
+ """
812
+ Indicates if the sensor is unavailable (e.g. has been removed).
813
+ """
814
+ return self._unavailable
815
+
816
+ @is_unavailable.setter
817
+ def is_unavailable(self, value: bool) -> None:
818
+ self._unavailable = value
819
+
820
+ async def delete(self) -> None:
821
+ """
822
+ Deletes the sensor from the alarm panel.
823
+ """
824
+ _LOGGER.debug("Deleting sensor: %s", self)
825
+
826
+ # Mark the sensor as unavailable
827
+ self.is_unavailable = True
828
+ # Delete the sensor from the alarm panel
829
+ await self.parent.command(
830
+ G90Commands.DELSENSOR, [self.index]
831
+ )
832
+
833
+ def _asdict(self) -> Dict[str, Any]:
834
+ """
835
+ Returns dictionary representation of the sensor.
836
+
837
+ :return: Dictionary representation
838
+ """
839
+ return {
840
+ 'name': self.name,
841
+ 'type': self.type,
842
+ 'subtype': self.subtype,
843
+ 'index': self.index,
844
+ 'protocol_index': self.proto_idx,
845
+ 'subindex': self.subindex,
846
+ 'node_count': self.node_count,
847
+ 'protocol': self.protocol,
848
+ 'occupancy': self.occupancy,
849
+ 'user_flag': self.user_flag,
850
+ 'reserved': self.reserved,
851
+ 'extra_data': self.extra_data,
852
+ 'enabled': self.get_flag(G90SensorUserFlags.ENABLED),
853
+ 'detect_door': self.get_flag(G90SensorUserFlags.DETECT_DOOR),
854
+ 'door_chime': self.get_flag(G90SensorUserFlags.DOOR_CHIME),
855
+ 'independent_zone': self.get_flag(
856
+ G90SensorUserFlags.INDEPENDENT_ZONE
857
+ ),
858
+ 'arm_delay': self.get_flag(G90SensorUserFlags.ARM_DELAY),
859
+ 'alert_mode': self.alert_mode,
860
+ 'supports_updates': self.supports_updates,
861
+ 'is_wireless': self.is_wireless,
862
+ 'is_low_battery': self.is_low_battery,
863
+ 'is_tampered': self.is_tampered,
864
+ 'is_door_open_when_arming': self.is_door_open_when_arming,
865
+ 'is_unavailable': self.is_unavailable,
866
+ }
867
+
868
+ def __repr__(self) -> str:
869
+ """
870
+ Returns string representation of the sensor.
871
+
872
+ :return: String representation
873
+ """
874
+ return super().__repr__() + f'({repr(self._asdict())})'
875
+
876
+ def __eq__(self, value: object) -> bool:
877
+ """
878
+ Compares the sensor with another object.
879
+
880
+ :param value: Object to compare with
881
+ :return: If the sensor is equal to the object
882
+ """
883
+ if not isinstance(value, G90Sensor):
884
+ return False
885
+
886
+ return (
887
+ self.type == value.type
888
+ and self.subtype == value.subtype
889
+ and self.index == value.index
890
+ and self.name == value.name
891
+ )