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.
- pyg90alarm/__init__.py +84 -0
- pyg90alarm/alarm.py +1274 -0
- pyg90alarm/callback.py +146 -0
- pyg90alarm/cloud/__init__.py +31 -0
- pyg90alarm/cloud/const.py +56 -0
- pyg90alarm/cloud/messages.py +593 -0
- pyg90alarm/cloud/notifications.py +410 -0
- pyg90alarm/cloud/protocol.py +518 -0
- pyg90alarm/const.py +273 -0
- pyg90alarm/definitions/__init__.py +3 -0
- pyg90alarm/definitions/base.py +247 -0
- pyg90alarm/definitions/devices.py +366 -0
- pyg90alarm/definitions/sensors.py +843 -0
- pyg90alarm/entities/__init__.py +3 -0
- pyg90alarm/entities/base_entity.py +93 -0
- pyg90alarm/entities/base_list.py +268 -0
- pyg90alarm/entities/device.py +97 -0
- pyg90alarm/entities/device_list.py +156 -0
- pyg90alarm/entities/sensor.py +891 -0
- pyg90alarm/entities/sensor_list.py +183 -0
- pyg90alarm/exceptions.py +63 -0
- pyg90alarm/local/__init__.py +0 -0
- pyg90alarm/local/base_cmd.py +293 -0
- pyg90alarm/local/config.py +157 -0
- pyg90alarm/local/discovery.py +103 -0
- pyg90alarm/local/history.py +272 -0
- pyg90alarm/local/host_info.py +89 -0
- pyg90alarm/local/host_status.py +52 -0
- pyg90alarm/local/notifications.py +117 -0
- pyg90alarm/local/paginated_cmd.py +132 -0
- pyg90alarm/local/paginated_result.py +135 -0
- pyg90alarm/local/targeted_discovery.py +162 -0
- pyg90alarm/local/user_data_crc.py +46 -0
- pyg90alarm/notifications/__init__.py +0 -0
- pyg90alarm/notifications/base.py +481 -0
- pyg90alarm/notifications/protocol.py +127 -0
- pyg90alarm/py.typed +0 -0
- pyg90alarm-2.3.0.dist-info/METADATA +277 -0
- pyg90alarm-2.3.0.dist-info/RECORD +42 -0
- pyg90alarm-2.3.0.dist-info/WHEEL +5 -0
- pyg90alarm-2.3.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
)
|