pyg90alarm 2.1.0__py3-none-any.whl → 2.2.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 +28 -5
- pyg90alarm/alarm.py +248 -54
- pyg90alarm/callback.py +41 -3
- pyg90alarm/cloud/messages.py +7 -7
- pyg90alarm/const.py +4 -1
- pyg90alarm/definitions/base.py +247 -0
- pyg90alarm/definitions/devices.py +366 -0
- pyg90alarm/definitions/sensors.py +812 -828
- pyg90alarm/entities/base_entity.py +10 -0
- pyg90alarm/entities/base_list.py +136 -33
- pyg90alarm/entities/device.py +23 -1
- pyg90alarm/entities/device_list.py +99 -1
- pyg90alarm/entities/sensor.py +83 -89
- pyg90alarm/entities/sensor_list.py +136 -3
- pyg90alarm/exceptions.py +24 -0
- pyg90alarm/local/base_cmd.py +34 -12
- pyg90alarm/local/notifications.py +3 -3
- pyg90alarm/notifications/base.py +29 -2
- pyg90alarm/notifications/protocol.py +11 -0
- {pyg90alarm-2.1.0.dist-info → pyg90alarm-2.2.0.dist-info}/METADATA +1 -1
- pyg90alarm-2.2.0.dist-info/RECORD +42 -0
- {pyg90alarm-2.1.0.dist-info → pyg90alarm-2.2.0.dist-info}/WHEEL +1 -1
- pyg90alarm-2.1.0.dist-info/RECORD +0 -40
- {pyg90alarm-2.1.0.dist-info → pyg90alarm-2.2.0.dist-info}/licenses/LICENSE +0 -0
- {pyg90alarm-2.1.0.dist-info → pyg90alarm-2.2.0.dist-info}/top_level.txt +0 -0
pyg90alarm/entities/sensor.py
CHANGED
|
@@ -29,9 +29,17 @@ from typing import (
|
|
|
29
29
|
)
|
|
30
30
|
|
|
31
31
|
from enum import IntEnum, IntFlag
|
|
32
|
-
from ..definitions.
|
|
32
|
+
from ..definitions.base import (
|
|
33
|
+
G90PeripheralDefinition,
|
|
34
|
+
G90PeripheralProtocols, G90PeripheralTypes,
|
|
35
|
+
)
|
|
36
|
+
from ..definitions.sensors import (
|
|
37
|
+
G90SensorDefinitions,
|
|
38
|
+
)
|
|
33
39
|
from ..const import G90Commands
|
|
34
40
|
from .base_entity import G90BaseEntity
|
|
41
|
+
from ..callback import G90CallbackList
|
|
42
|
+
from ..exceptions import G90PeripheralDefinitionNotFound
|
|
35
43
|
if TYPE_CHECKING:
|
|
36
44
|
from ..alarm import (
|
|
37
45
|
G90Alarm, SensorStateCallback, SensorLowBatteryCallback,
|
|
@@ -146,57 +154,6 @@ ALERT_MODES_MAP_BY_VALUE = dict(
|
|
|
146
154
|
)
|
|
147
155
|
|
|
148
156
|
|
|
149
|
-
class G90SensorProtocols(IntEnum):
|
|
150
|
-
"""
|
|
151
|
-
Protocol types for the sensors.
|
|
152
|
-
"""
|
|
153
|
-
RF_1527 = 0
|
|
154
|
-
RF_2262 = 1
|
|
155
|
-
RF_PRIVATE = 2
|
|
156
|
-
RF_SLIDER = 3
|
|
157
|
-
CORD = 5
|
|
158
|
-
WIFI = 4
|
|
159
|
-
USB = 6
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
class G90SensorTypes(IntEnum):
|
|
163
|
-
"""
|
|
164
|
-
Sensor types.
|
|
165
|
-
"""
|
|
166
|
-
DOOR = 1
|
|
167
|
-
GLASS = 2
|
|
168
|
-
GAS = 3
|
|
169
|
-
SMOKE = 4
|
|
170
|
-
SOS = 5
|
|
171
|
-
VIB = 6
|
|
172
|
-
WATER = 7
|
|
173
|
-
INFRARED = 8
|
|
174
|
-
IN_BEAM = 9
|
|
175
|
-
REMOTE = 10
|
|
176
|
-
RFID = 11
|
|
177
|
-
DOORBELL = 12
|
|
178
|
-
BUTTONID = 13
|
|
179
|
-
WATCH = 14
|
|
180
|
-
FINGER_LOCK = 15
|
|
181
|
-
SUBHOST = 16
|
|
182
|
-
REMOTE_2_4G = 17
|
|
183
|
-
CORD_SENSOR = 126
|
|
184
|
-
SOCKET = 128
|
|
185
|
-
SIREN = 129
|
|
186
|
-
CURTAIN = 130
|
|
187
|
-
SLIDINGWIN = 131
|
|
188
|
-
AIRCON = 136
|
|
189
|
-
TV = 137
|
|
190
|
-
NIGHTLIGHT = 138
|
|
191
|
-
SOCKET_2_4G = 140
|
|
192
|
-
SIREN_2_4G = 141
|
|
193
|
-
SWITCH_2_4G = 142
|
|
194
|
-
TOUCH_SWITCH_2_4G = 143
|
|
195
|
-
CURTAIN_2_4G = 144
|
|
196
|
-
CORD_DEV = 254
|
|
197
|
-
UNKNOWN = 255
|
|
198
|
-
|
|
199
|
-
|
|
200
157
|
_LOGGER = logging.getLogger(__name__)
|
|
201
158
|
|
|
202
159
|
|
|
@@ -227,28 +184,45 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
|
|
|
227
184
|
self._parent = parent
|
|
228
185
|
self._subindex = subindex
|
|
229
186
|
self._occupancy = False
|
|
230
|
-
self._state_callback:
|
|
231
|
-
|
|
187
|
+
self._state_callback: G90CallbackList[SensorStateCallback] = (
|
|
188
|
+
G90CallbackList()
|
|
189
|
+
)
|
|
190
|
+
self._low_battery_callback: G90CallbackList[
|
|
191
|
+
SensorLowBatteryCallback
|
|
192
|
+
] = G90CallbackList()
|
|
232
193
|
self._low_battery = False
|
|
233
194
|
self._tampered = False
|
|
234
|
-
self._door_open_when_arming_callback:
|
|
195
|
+
self._door_open_when_arming_callback: G90CallbackList[
|
|
235
196
|
SensorDoorOpenWhenArmingCallback
|
|
236
|
-
] =
|
|
237
|
-
self._tamper_callback:
|
|
197
|
+
] = G90CallbackList()
|
|
198
|
+
self._tamper_callback: G90CallbackList[SensorTamperCallback] = (
|
|
199
|
+
G90CallbackList()
|
|
200
|
+
)
|
|
238
201
|
self._door_open_when_arming = False
|
|
239
202
|
self._proto_idx = proto_idx
|
|
240
203
|
self._extra_data: Any = None
|
|
241
204
|
self._unavailable = False
|
|
205
|
+
self._definition: Optional[G90PeripheralDefinition] = None
|
|
242
206
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
|
252
226
|
|
|
253
227
|
def update(self, obj: G90Sensor) -> None:
|
|
254
228
|
"""
|
|
@@ -272,41 +246,49 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
|
|
|
272
246
|
return f'{self._protocol_data.parent_name}#{self._subindex + 1}'
|
|
273
247
|
|
|
274
248
|
@property
|
|
275
|
-
def state_callback(self) ->
|
|
249
|
+
def state_callback(self) -> G90CallbackList[SensorStateCallback]:
|
|
276
250
|
"""
|
|
277
251
|
Callback that is invoked when the sensor changes its state.
|
|
278
252
|
|
|
279
253
|
:return: Sensor state callback
|
|
254
|
+
|
|
255
|
+
.. seealso:: :attr:`G90Alarm.sensor_callback` for compatiblity notes
|
|
280
256
|
"""
|
|
281
257
|
return self._state_callback
|
|
282
258
|
|
|
283
259
|
@state_callback.setter
|
|
284
260
|
def state_callback(self, value: SensorStateCallback) -> None:
|
|
285
|
-
self._state_callback
|
|
261
|
+
self._state_callback.add(value)
|
|
286
262
|
|
|
287
263
|
@property
|
|
288
|
-
def low_battery_callback(
|
|
264
|
+
def low_battery_callback(
|
|
265
|
+
self
|
|
266
|
+
) -> G90CallbackList[SensorLowBatteryCallback]:
|
|
289
267
|
"""
|
|
290
268
|
Callback that is invoked when the sensor reports on low battery
|
|
291
269
|
condition.
|
|
292
270
|
|
|
293
271
|
:return: Sensor's low battery callback
|
|
272
|
+
|
|
273
|
+
.. seealso:: :attr:`G90Alarm.sensor_callback` for compatiblity notes
|
|
294
274
|
"""
|
|
295
275
|
return self._low_battery_callback
|
|
296
276
|
|
|
297
277
|
@low_battery_callback.setter
|
|
298
278
|
def low_battery_callback(self, value: SensorLowBatteryCallback) -> None:
|
|
299
|
-
self._low_battery_callback
|
|
279
|
+
self._low_battery_callback.add(value)
|
|
300
280
|
|
|
301
281
|
@property
|
|
302
282
|
def door_open_when_arming_callback(
|
|
303
283
|
self
|
|
304
|
-
) ->
|
|
284
|
+
) -> G90CallbackList[SensorDoorOpenWhenArmingCallback]:
|
|
305
285
|
"""
|
|
306
286
|
Callback that is invoked when the sensor reports on open door
|
|
307
287
|
condition when arming.
|
|
308
288
|
|
|
309
289
|
:return: Sensor's door open when arming callback
|
|
290
|
+
|
|
291
|
+
.. seealso:: :attr:`G90Alarm.sensor_callback` for compatiblity notes
|
|
310
292
|
"""
|
|
311
293
|
return self._door_open_when_arming_callback
|
|
312
294
|
|
|
@@ -314,20 +296,22 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
|
|
|
314
296
|
def door_open_when_arming_callback(
|
|
315
297
|
self, value: SensorDoorOpenWhenArmingCallback
|
|
316
298
|
) -> None:
|
|
317
|
-
self._door_open_when_arming_callback
|
|
299
|
+
self._door_open_when_arming_callback.add(value)
|
|
318
300
|
|
|
319
301
|
@property
|
|
320
|
-
def tamper_callback(self) ->
|
|
302
|
+
def tamper_callback(self) -> G90CallbackList[SensorTamperCallback]:
|
|
321
303
|
"""
|
|
322
304
|
Callback that is invoked when the sensor reports being tampered.
|
|
323
305
|
|
|
324
306
|
:return: Sensor's tamper callback
|
|
307
|
+
|
|
308
|
+
.. seealso:: :attr:`G90Alarm.sensor_callback` for compatiblity notes
|
|
325
309
|
"""
|
|
326
310
|
return self._tamper_callback
|
|
327
311
|
|
|
328
312
|
@tamper_callback.setter
|
|
329
313
|
def tamper_callback(self, value: SensorTamperCallback) -> None:
|
|
330
|
-
self._tamper_callback
|
|
314
|
+
self._tamper_callback.add(value)
|
|
331
315
|
|
|
332
316
|
@property
|
|
333
317
|
def occupancy(self) -> bool:
|
|
@@ -355,22 +339,22 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
|
|
|
355
339
|
self._occupancy = value
|
|
356
340
|
|
|
357
341
|
@property
|
|
358
|
-
def protocol(self) ->
|
|
342
|
+
def protocol(self) -> G90PeripheralProtocols:
|
|
359
343
|
"""
|
|
360
344
|
Protocol type of the sensor.
|
|
361
345
|
|
|
362
346
|
:return: Protocol type
|
|
363
347
|
"""
|
|
364
|
-
return
|
|
348
|
+
return G90PeripheralProtocols(self._protocol_data.protocol_id)
|
|
365
349
|
|
|
366
350
|
@property
|
|
367
|
-
def type(self) ->
|
|
351
|
+
def type(self) -> G90PeripheralTypes:
|
|
368
352
|
"""
|
|
369
353
|
Type of the sensor.
|
|
370
354
|
|
|
371
355
|
:return: Sensor type
|
|
372
356
|
"""
|
|
373
|
-
return
|
|
357
|
+
return G90PeripheralTypes(self._protocol_data.type_id)
|
|
374
358
|
|
|
375
359
|
@property
|
|
376
360
|
def subtype(self) -> int:
|
|
@@ -456,6 +440,18 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
|
|
|
456
440
|
"""
|
|
457
441
|
return self._proto_idx
|
|
458
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
|
+
|
|
459
455
|
@property
|
|
460
456
|
def supports_updates(self) -> bool:
|
|
461
457
|
"""
|
|
@@ -463,14 +459,12 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
|
|
|
463
459
|
|
|
464
460
|
:return: Support for updates
|
|
465
461
|
"""
|
|
466
|
-
if not self.
|
|
462
|
+
if not self.definition:
|
|
467
463
|
_LOGGER.warning(
|
|
468
464
|
'Manipulating with user flags for sensor index=%s'
|
|
469
465
|
' is unsupported - no sensor definition for'
|
|
470
|
-
' type=%s, subtype=%s',
|
|
471
|
-
self.index,
|
|
472
|
-
self._protocol_data.type_id,
|
|
473
|
-
self._protocol_data.subtype
|
|
466
|
+
' type=%s, subtype=%s, protocol=%s',
|
|
467
|
+
self.index, self.type, self.subtype, self.protocol
|
|
474
468
|
)
|
|
475
469
|
return False
|
|
476
470
|
return True
|
|
@@ -498,7 +492,7 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
|
|
|
498
492
|
"""
|
|
499
493
|
Indicates if the sensor is wireless.
|
|
500
494
|
"""
|
|
501
|
-
return self.protocol not in (
|
|
495
|
+
return self.protocol not in (G90PeripheralProtocols.CORD,)
|
|
502
496
|
|
|
503
497
|
@property
|
|
504
498
|
def is_low_battery(self) -> bool:
|
|
@@ -597,7 +591,7 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
|
|
|
597
591
|
# Checking private attribute directly, since `mypy` doesn't recognize
|
|
598
592
|
# the check for sensor definition is done over
|
|
599
593
|
# `self.supports_updates` property
|
|
600
|
-
if not self.
|
|
594
|
+
if not self.definition:
|
|
601
595
|
return
|
|
602
596
|
|
|
603
597
|
if value & ~G90SensorUserFlags.USER_SETTABLE:
|
|
@@ -685,11 +679,11 @@ class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
|
|
|
685
679
|
user_flags_data=self._protocol_data.user_flags_data,
|
|
686
680
|
baudrate=self._protocol_data.baudrate,
|
|
687
681
|
protocol_id=self._protocol_data.protocol_id,
|
|
688
|
-
reserved_data=self.
|
|
682
|
+
reserved_data=self.definition.reserved_data,
|
|
689
683
|
node_count=self._protocol_data.node_count,
|
|
690
|
-
rx=self.
|
|
691
|
-
tx=self.
|
|
692
|
-
private_data=self.
|
|
684
|
+
rx=self.definition.rx,
|
|
685
|
+
tx=self.definition.tx,
|
|
686
|
+
private_data=self.definition.private_data,
|
|
693
687
|
)
|
|
694
688
|
# Modify the sensor
|
|
695
689
|
await self._parent.command(
|
|
@@ -20,19 +20,32 @@
|
|
|
20
20
|
"""
|
|
21
21
|
Sensor list.
|
|
22
22
|
"""
|
|
23
|
-
from
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
from typing import AsyncGenerator, Optional, TYPE_CHECKING
|
|
25
|
+
import logging
|
|
26
|
+
import asyncio
|
|
26
27
|
|
|
27
28
|
from .sensor import G90Sensor
|
|
28
29
|
from .base_list import G90BaseList
|
|
29
30
|
from ..const import G90Commands
|
|
31
|
+
from ..exceptions import G90EntityRegistrationError, G90Error
|
|
32
|
+
from ..definitions.sensors import G90SensorDefinitions
|
|
33
|
+
from ..entities.sensor import G90SensorUserFlags
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from ..alarm import G90Alarm
|
|
37
|
+
|
|
38
|
+
_LOGGER = logging.getLogger(__name__)
|
|
30
39
|
|
|
31
40
|
|
|
32
41
|
class G90SensorList(G90BaseList[G90Sensor]):
|
|
33
42
|
"""
|
|
34
43
|
Sensor list class.
|
|
35
44
|
"""
|
|
45
|
+
def __init__(self, parent: G90Alarm) -> None:
|
|
46
|
+
super().__init__(parent)
|
|
47
|
+
self._sensor_change_future: Optional[asyncio.Future[G90Sensor]] = None
|
|
48
|
+
|
|
36
49
|
async def _fetch(self) -> AsyncGenerator[G90Sensor, None]:
|
|
37
50
|
"""
|
|
38
51
|
Fetch the list of sensors from the panel.
|
|
@@ -48,3 +61,123 @@ class G90SensorList(G90BaseList[G90Sensor]):
|
|
|
48
61
|
*entity.data, parent=self._parent, subindex=0,
|
|
49
62
|
proto_idx=entity.proto_idx
|
|
50
63
|
)
|
|
64
|
+
|
|
65
|
+
async def sensor_change_callback(
|
|
66
|
+
self, idx: int, name: str, added: bool
|
|
67
|
+
) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Sensor change callback.
|
|
70
|
+
|
|
71
|
+
Should be invoked from corresponding panel's notification handler to
|
|
72
|
+
finish the registration process.
|
|
73
|
+
|
|
74
|
+
:param idx: Sensor index.
|
|
75
|
+
:param name: Sensor name.
|
|
76
|
+
:param added: True if the sensor was added, False if removed.
|
|
77
|
+
"""
|
|
78
|
+
_LOGGER.debug(
|
|
79
|
+
"Sensor change callback: name='%s', index=%d, added=%s",
|
|
80
|
+
name, idx, added
|
|
81
|
+
)
|
|
82
|
+
# The method depends on the future to be created before it is called
|
|
83
|
+
if not self._sensor_change_future:
|
|
84
|
+
raise G90EntityRegistrationError(
|
|
85
|
+
"Sensor change callback called without a future"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Update the sensor list to get the latest data, unfortunately there is
|
|
89
|
+
# no panel command to get just a single sensor
|
|
90
|
+
try:
|
|
91
|
+
await self.update()
|
|
92
|
+
except G90Error as err:
|
|
93
|
+
_LOGGER.debug(
|
|
94
|
+
"Failed to update the sensor list: %s", err
|
|
95
|
+
)
|
|
96
|
+
# Indicate the error to the caller
|
|
97
|
+
self._sensor_change_future.set_exception(err)
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
# Attempt to find the sensor just changed by its index
|
|
101
|
+
if not (found := await self.find_by_idx(
|
|
102
|
+
idx, exclude_unavailable=False
|
|
103
|
+
)):
|
|
104
|
+
msg = (
|
|
105
|
+
f"Failed to find the added sensor '{name}'"
|
|
106
|
+
f' at index {idx}'
|
|
107
|
+
)
|
|
108
|
+
_LOGGER.debug(msg)
|
|
109
|
+
# Indicate the error to the caller
|
|
110
|
+
self._sensor_change_future.set_exception(
|
|
111
|
+
G90EntityRegistrationError(msg)
|
|
112
|
+
)
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
# Provide the found sensor as the future result so that the caller
|
|
116
|
+
# can use it
|
|
117
|
+
self._sensor_change_future.set_result(found)
|
|
118
|
+
|
|
119
|
+
async def register(
|
|
120
|
+
self, definition_name: str,
|
|
121
|
+
room_id: int, timeout: float, name: Optional[str] = None,
|
|
122
|
+
) -> G90Sensor:
|
|
123
|
+
"""
|
|
124
|
+
Registers sensor to the panel.
|
|
125
|
+
|
|
126
|
+
:param definition_name: Sensor definition name.
|
|
127
|
+
:param room_id: Room ID to assign the sensor to.
|
|
128
|
+
:param timeout: Timeout for the registration process.
|
|
129
|
+
:param name: Optional name for the sensor, if not provided, the
|
|
130
|
+
definition name will be used.
|
|
131
|
+
:raises G90EntityRegistrationError: If the registration fails.
|
|
132
|
+
:return: G90Sensor: The registered sensor entity.
|
|
133
|
+
"""
|
|
134
|
+
sensor_definition = G90SensorDefinitions.get_by_name(definition_name)
|
|
135
|
+
dev_name = name or sensor_definition.name
|
|
136
|
+
|
|
137
|
+
# Future is needed to coordinate the registration process with panel
|
|
138
|
+
# notifications (see `sensor_change_callback()` method)
|
|
139
|
+
self._sensor_change_future = asyncio.get_running_loop().create_future()
|
|
140
|
+
|
|
141
|
+
# Register the sensor with the panel
|
|
142
|
+
await self._parent.command(
|
|
143
|
+
G90Commands.ADDSENSOR, [
|
|
144
|
+
dev_name,
|
|
145
|
+
# Registering sensor requires to provide a free index from
|
|
146
|
+
# panel point of view
|
|
147
|
+
await self.find_free_idx(),
|
|
148
|
+
room_id,
|
|
149
|
+
sensor_definition.type,
|
|
150
|
+
sensor_definition.subtype,
|
|
151
|
+
sensor_definition.timeout,
|
|
152
|
+
# Newly registered sensors are enabled by default and set to
|
|
153
|
+
# alarm in away and home modes
|
|
154
|
+
G90SensorUserFlags.ENABLED
|
|
155
|
+
| G90SensorUserFlags.ALERT_WHEN_AWAY_AND_HOME,
|
|
156
|
+
sensor_definition.baudrate,
|
|
157
|
+
sensor_definition.protocol,
|
|
158
|
+
sensor_definition.reserved_data,
|
|
159
|
+
sensor_definition.node_count,
|
|
160
|
+
sensor_definition.rx,
|
|
161
|
+
sensor_definition.tx,
|
|
162
|
+
sensor_definition.private_data
|
|
163
|
+
]
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Waiting for the registration to finish, where panel will send
|
|
167
|
+
# corresponding notification processed by `sensor_change_callback()`
|
|
168
|
+
# method, which manipulates the future created above.
|
|
169
|
+
done, _ = await asyncio.wait(
|
|
170
|
+
[self._sensor_change_future], timeout=timeout
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if self._sensor_change_future not in done:
|
|
174
|
+
msg = f"Failed to learn the device '{dev_name}', timed out"
|
|
175
|
+
_LOGGER.debug(msg)
|
|
176
|
+
raise G90EntityRegistrationError(msg)
|
|
177
|
+
|
|
178
|
+
# Propagate any exception that might have occurred in
|
|
179
|
+
# `sensor_change_callback()` method during the registration process
|
|
180
|
+
self._sensor_change_future.exception()
|
|
181
|
+
|
|
182
|
+
# Return the registered sensor entity
|
|
183
|
+
return self._sensor_change_future.result()
|
pyg90alarm/exceptions.py
CHANGED
|
@@ -37,3 +37,27 @@ class G90TimeoutError(asyncio.TimeoutError): # pylint:disable=R0903
|
|
|
37
37
|
Raised when particular package class to report an operation (typically
|
|
38
38
|
device command) has timed out.
|
|
39
39
|
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class G90CommandFailure(G90Error):
|
|
43
|
+
"""
|
|
44
|
+
Raised when a command to the alarm panel reports failure.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class G90CommandError(G90Error):
|
|
49
|
+
"""
|
|
50
|
+
Raised when a command to the alarm panel reports an error.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class G90EntityRegistrationError(G90Error):
|
|
55
|
+
"""
|
|
56
|
+
Raised when registering an entity to the alarm panel fails.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class G90PeripheralDefinitionNotFound(G90Error):
|
|
61
|
+
"""
|
|
62
|
+
Raised when a peripheral definition is not found.
|
|
63
|
+
"""
|
pyg90alarm/local/base_cmd.py
CHANGED
|
@@ -30,7 +30,9 @@ from asyncio.protocols import DatagramProtocol
|
|
|
30
30
|
from asyncio.transports import DatagramTransport, BaseTransport
|
|
31
31
|
from typing import Optional, Tuple, List, Any
|
|
32
32
|
from dataclasses import dataclass
|
|
33
|
-
from ..exceptions import (
|
|
33
|
+
from ..exceptions import (
|
|
34
|
+
G90Error, G90TimeoutError, G90CommandFailure, G90CommandError
|
|
35
|
+
)
|
|
34
36
|
from ..const import G90Commands
|
|
35
37
|
|
|
36
38
|
|
|
@@ -168,19 +170,39 @@ class G90BaseCommand(DatagramProtocol):
|
|
|
168
170
|
if not data.endswith('IEND\0'):
|
|
169
171
|
raise G90Error('Missing end marker in data')
|
|
170
172
|
payload = data[6:-5]
|
|
171
|
-
_LOGGER.debug("Decoded from wire:
|
|
173
|
+
_LOGGER.debug("Decoded from wire: string '%s'", payload)
|
|
174
|
+
|
|
175
|
+
if not payload:
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
# Panel may report the last command has failed
|
|
179
|
+
if payload == 'fail':
|
|
180
|
+
raise G90CommandFailure(
|
|
181
|
+
f"Command {self._code.name}"
|
|
182
|
+
f" (code={self._code.value}) failed"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Also, panel may report an error supplying specific reason, e.g.
|
|
186
|
+
# command and its arguments that have failed
|
|
187
|
+
if payload.startswith('error'):
|
|
188
|
+
error = payload[5:]
|
|
189
|
+
raise G90CommandError(
|
|
190
|
+
f"Command {self._code.name}"
|
|
191
|
+
f" (code={self._code.value}) failed"
|
|
192
|
+
f" with error: '{error}'")
|
|
172
193
|
|
|
173
194
|
resp = None
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
195
|
+
try:
|
|
196
|
+
resp = json.loads(payload, strict=False)
|
|
197
|
+
except json.JSONDecodeError as exc:
|
|
198
|
+
raise G90Error(
|
|
199
|
+
f"Unable to parse response as JSON: '{payload}'"
|
|
200
|
+
) from exc
|
|
201
|
+
|
|
202
|
+
if not isinstance(resp, list):
|
|
203
|
+
raise G90Error(
|
|
204
|
+
f"Malformed response, 'list' expected: '{payload}'"
|
|
205
|
+
)
|
|
184
206
|
|
|
185
207
|
if resp is not None:
|
|
186
208
|
self._resp = G90Header(*resp)
|
|
@@ -42,12 +42,12 @@ class G90LocalNotifications(G90NotificationsBase, DatagramProtocol):
|
|
|
42
42
|
There is a basic check to ensure only notifications/alerts from the correct
|
|
43
43
|
device are processed - the check uses the host and port of the device, and
|
|
44
44
|
the device ID (GUID) that is set by the ancestor class that implements the
|
|
45
|
-
commands (e.g. :class
|
|
45
|
+
commands (e.g. :class:`.G90Alarm`). The latter to work correctly needs a
|
|
46
46
|
command to be performed first, one that fetches device GUID and then stores
|
|
47
|
-
it using :attr:`.device_id` (e.g. :meth
|
|
47
|
+
it using :attr:`.device_id` (e.g. :meth:`.G90Alarm.get_host_info`).
|
|
48
48
|
|
|
49
49
|
:param protocol_factory: A callable that returns a new instance of the
|
|
50
|
-
:class
|
|
50
|
+
:class:`.G90NotificationProtocol` class.
|
|
51
51
|
:param port: The port on which the device is listening for notifications.
|
|
52
52
|
:param host: The host on which the device is listening for notifications.
|
|
53
53
|
:param local_port: The port on which the local host is listening for
|
pyg90alarm/notifications/base.py
CHANGED
|
@@ -89,6 +89,18 @@ class G90ArmDisarmInfo:
|
|
|
89
89
|
state: int
|
|
90
90
|
|
|
91
91
|
|
|
92
|
+
@dataclass
|
|
93
|
+
class G90SensorChangeInfo:
|
|
94
|
+
"""
|
|
95
|
+
Represents the sensor added notification received from the device.
|
|
96
|
+
|
|
97
|
+
:meta private:
|
|
98
|
+
"""
|
|
99
|
+
idx: int
|
|
100
|
+
name: str
|
|
101
|
+
action: int # 1 - if added, 2 - for update (?)
|
|
102
|
+
|
|
103
|
+
|
|
92
104
|
@dataclass
|
|
93
105
|
class G90DeviceAlert: # pylint: disable=too-many-instance-attributes
|
|
94
106
|
"""
|
|
@@ -112,9 +124,9 @@ class G90NotificationsBase:
|
|
|
112
124
|
There is a basic check to ensure only notifications/alerts from the correct
|
|
113
125
|
device are processed - the check uses the host and port of the device, and
|
|
114
126
|
the device ID (GUID) that is set by the ancestor class that implements the
|
|
115
|
-
commands (e.g. :class
|
|
127
|
+
commands (e.g. :class:`.G90Alarm`). The latter to work correctly needs a
|
|
116
128
|
command to be performed first, one that fetches device GUID and then stores
|
|
117
|
-
it using :attr:`.device_id` (e.g. :meth
|
|
129
|
+
it using :attr:`.device_id` (e.g. :meth:`.G90Alarm.get_host_info`).
|
|
118
130
|
"""
|
|
119
131
|
def __init__(
|
|
120
132
|
self, protocol_factory: Callable[[], G90NotificationProtocol],
|
|
@@ -173,6 +185,21 @@ class G90NotificationsBase:
|
|
|
173
185
|
)
|
|
174
186
|
return
|
|
175
187
|
|
|
188
|
+
# Sensor has been added or removed
|
|
189
|
+
if notification.kind == G90NotificationTypes.SENSOR_CHANGE:
|
|
190
|
+
g90_sensor_info = G90SensorChangeInfo(*notification.data)
|
|
191
|
+
sensor_added = g90_sensor_info.action == 1
|
|
192
|
+
_LOGGER.debug(
|
|
193
|
+
'Sensor change notification, added=%s: %s',
|
|
194
|
+
sensor_added, g90_sensor_info
|
|
195
|
+
)
|
|
196
|
+
G90Callback.invoke(
|
|
197
|
+
self._protocol.on_sensor_change,
|
|
198
|
+
g90_sensor_info.idx, g90_sensor_info.name,
|
|
199
|
+
sensor_added
|
|
200
|
+
)
|
|
201
|
+
return
|
|
202
|
+
|
|
176
203
|
_LOGGER.warning('Unknown notification received:'
|
|
177
204
|
' kind %s, data %s',
|
|
178
205
|
notification.kind, notification.data)
|
|
@@ -114,3 +114,14 @@ class G90NotificationProtocol:
|
|
|
114
114
|
:param zone_name: Name of the sensor that reports SOS.
|
|
115
115
|
:param is_host_sos: Indicates if the SOS is host-initiated.
|
|
116
116
|
"""
|
|
117
|
+
|
|
118
|
+
async def on_sensor_change(
|
|
119
|
+
self, sensor_idx: int, sensor_name: str, added: bool
|
|
120
|
+
) -> None:
|
|
121
|
+
"""
|
|
122
|
+
Invoked when a sensor is added or changed on the panel.
|
|
123
|
+
|
|
124
|
+
:param sensor_idx: Index of the sensor.
|
|
125
|
+
:param sensor_name: Name of the sensor.
|
|
126
|
+
:param added: True if the sensor was added.
|
|
127
|
+
"""
|