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
pyg90alarm/alarm.py
ADDED
|
@@ -0,0 +1,1274 @@
|
|
|
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
|
+
# pylint: disable=too-many-lines
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
Provides interface to G90 alarm panel.
|
|
24
|
+
|
|
25
|
+
.. note:: Both local protocol (referred to as 1.2) and cloud one
|
|
26
|
+
(mentioned as 1.1) are supported.
|
|
27
|
+
|
|
28
|
+
The next example queries the device with IP address `10.10.10.250` for the
|
|
29
|
+
information - the product name, protocol version, HW versions and such.
|
|
30
|
+
|
|
31
|
+
>>> import asyncio
|
|
32
|
+
>>> from pyg90alarm import G90Alarm
|
|
33
|
+
>>> async def main():
|
|
34
|
+
g90 = G90Alarm(host='10.10.10.250')
|
|
35
|
+
s = await g90.host_info
|
|
36
|
+
print(s)
|
|
37
|
+
>>> asyncio.run(main())
|
|
38
|
+
G90HostInfo(host_guid='<...>',
|
|
39
|
+
product_name='TSV018-C3SIA',
|
|
40
|
+
wifi_protocol_version='1.2',
|
|
41
|
+
cloud_protocol_version='1.1',
|
|
42
|
+
mcu_hw_version='206',
|
|
43
|
+
wifi_hw_version='206',
|
|
44
|
+
gsm_status=3,
|
|
45
|
+
wifi_status=3,
|
|
46
|
+
reserved1=0,
|
|
47
|
+
reserved2=0,
|
|
48
|
+
band_frequency='4264',
|
|
49
|
+
gsm_signal_level=50,
|
|
50
|
+
wifi_signal_level=100)
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
from __future__ import annotations
|
|
54
|
+
import asyncio
|
|
55
|
+
from asyncio import Task
|
|
56
|
+
from datetime import datetime
|
|
57
|
+
import logging
|
|
58
|
+
from typing import (
|
|
59
|
+
TYPE_CHECKING, Any, List, Optional, AsyncGenerator,
|
|
60
|
+
Callable, Coroutine, Union
|
|
61
|
+
)
|
|
62
|
+
from .const import (
|
|
63
|
+
G90Commands, REMOTE_PORT,
|
|
64
|
+
REMOTE_TARGETED_DISCOVERY_PORT,
|
|
65
|
+
LOCAL_TARGETED_DISCOVERY_PORT,
|
|
66
|
+
LOCAL_NOTIFICATIONS_HOST,
|
|
67
|
+
LOCAL_NOTIFICATIONS_PORT,
|
|
68
|
+
CLOUD_NOTIFICATIONS_HOST,
|
|
69
|
+
CLOUD_NOTIFICATIONS_PORT,
|
|
70
|
+
REMOTE_CLOUD_HOST,
|
|
71
|
+
REMOTE_CLOUD_PORT,
|
|
72
|
+
DEVICE_REGISTRATION_TIMEOUT,
|
|
73
|
+
ROOM_ID,
|
|
74
|
+
G90ArmDisarmTypes,
|
|
75
|
+
G90RemoteButtonStates,
|
|
76
|
+
)
|
|
77
|
+
from .local.base_cmd import (G90BaseCommand, G90BaseCommandData)
|
|
78
|
+
from .local.paginated_result import G90PaginatedResult, G90PaginatedResponse
|
|
79
|
+
from .entities.base_list import ListChangeCallback
|
|
80
|
+
from .entities.sensor import G90Sensor
|
|
81
|
+
from .entities.sensor_list import G90SensorList
|
|
82
|
+
from .entities.device import G90Device
|
|
83
|
+
from .entities.device_list import G90DeviceList
|
|
84
|
+
from .definitions.base import (
|
|
85
|
+
G90PeripheralTypes
|
|
86
|
+
)
|
|
87
|
+
from .notifications.protocol import (
|
|
88
|
+
G90NotificationProtocol
|
|
89
|
+
)
|
|
90
|
+
from .notifications.base import G90NotificationsBase
|
|
91
|
+
from .local.notifications import G90LocalNotifications
|
|
92
|
+
from .local.discovery import G90Discovery, G90DiscoveredDevice
|
|
93
|
+
from .local.targeted_discovery import (
|
|
94
|
+
G90TargetedDiscovery, G90DiscoveredDeviceTargeted,
|
|
95
|
+
)
|
|
96
|
+
from .local.host_info import G90HostInfo
|
|
97
|
+
from .local.host_status import G90HostStatus
|
|
98
|
+
from .local.config import (G90AlertConfig, G90AlertConfigFlags)
|
|
99
|
+
from .local.history import G90History
|
|
100
|
+
from .local.user_data_crc import G90UserDataCRC
|
|
101
|
+
from .callback import G90Callback, G90CallbackList
|
|
102
|
+
from .exceptions import G90Error, G90TimeoutError
|
|
103
|
+
from .cloud.notifications import G90CloudNotifications
|
|
104
|
+
|
|
105
|
+
_LOGGER = logging.getLogger(__name__)
|
|
106
|
+
|
|
107
|
+
if TYPE_CHECKING:
|
|
108
|
+
# Type alias for the callback functions available to the user, should be
|
|
109
|
+
# compatible with `G90Callback.Callback` type, since `G90Callback.invoke`
|
|
110
|
+
# is used to invoke them
|
|
111
|
+
AlarmCallback = Union[
|
|
112
|
+
Callable[[int, str, Any], None],
|
|
113
|
+
Callable[[int, str, Any], Coroutine[None, None, None]]
|
|
114
|
+
]
|
|
115
|
+
DoorOpenCloseCallback = Union[
|
|
116
|
+
Callable[[int, str, bool], None],
|
|
117
|
+
Callable[[int, str, bool], Coroutine[None, None, None]]
|
|
118
|
+
]
|
|
119
|
+
SensorCallback = Union[
|
|
120
|
+
Callable[[int, str, bool], None],
|
|
121
|
+
Callable[[int, str, bool], Coroutine[None, None, None]]
|
|
122
|
+
]
|
|
123
|
+
LowBatteryCallback = Union[
|
|
124
|
+
Callable[[int, str], None],
|
|
125
|
+
Callable[[int, str], Coroutine[None, None, None]]
|
|
126
|
+
]
|
|
127
|
+
ArmDisarmCallback = Union[
|
|
128
|
+
Callable[[G90ArmDisarmTypes], None],
|
|
129
|
+
Callable[[G90ArmDisarmTypes], Coroutine[None, None, None]]
|
|
130
|
+
]
|
|
131
|
+
SosCallback = Union[
|
|
132
|
+
Callable[[int, str, bool], None],
|
|
133
|
+
Callable[[int, str, bool], Coroutine[None, None, None]]
|
|
134
|
+
]
|
|
135
|
+
RemoteButtonPressCallback = Union[
|
|
136
|
+
Callable[[int, str, G90RemoteButtonStates], None],
|
|
137
|
+
Callable[
|
|
138
|
+
[int, str, G90RemoteButtonStates], Coroutine[None, None, None]
|
|
139
|
+
]
|
|
140
|
+
]
|
|
141
|
+
DoorOpenWhenArmingCallback = Union[
|
|
142
|
+
Callable[[int, str], None],
|
|
143
|
+
Callable[[int, str], Coroutine[None, None, None]]
|
|
144
|
+
]
|
|
145
|
+
TamperCallback = Union[
|
|
146
|
+
Callable[[int, str], None],
|
|
147
|
+
Callable[[int, str], Coroutine[None, None, None]]
|
|
148
|
+
]
|
|
149
|
+
# Sensor-related callbacks for `G90Sensor` class - despite that class
|
|
150
|
+
# stores them, the invocation is done by the `G90Alarm` class hence these
|
|
151
|
+
# are defined here
|
|
152
|
+
SensorStateCallback = Union[
|
|
153
|
+
Callable[[bool], None],
|
|
154
|
+
Callable[[bool], Coroutine[None, None, None]]
|
|
155
|
+
]
|
|
156
|
+
SensorLowBatteryCallback = Union[
|
|
157
|
+
Callable[[], None],
|
|
158
|
+
Callable[[], Coroutine[None, None, None]]
|
|
159
|
+
]
|
|
160
|
+
SensorDoorOpenWhenArmingCallback = Union[
|
|
161
|
+
Callable[[], None],
|
|
162
|
+
Callable[[], Coroutine[None, None, None]]
|
|
163
|
+
]
|
|
164
|
+
SensorTamperCallback = Union[
|
|
165
|
+
Callable[[], None],
|
|
166
|
+
Callable[[], Coroutine[None, None, None]]
|
|
167
|
+
]
|
|
168
|
+
SensorChangeCallback = Union[
|
|
169
|
+
Callable[[int, str, bool], None],
|
|
170
|
+
Callable[[int, str, bool], Coroutine[None, None, None]]
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# pylint: disable=too-many-public-methods
|
|
175
|
+
class G90Alarm(G90NotificationProtocol):
|
|
176
|
+
|
|
177
|
+
"""
|
|
178
|
+
Allows to interact with G90 alarm panel.
|
|
179
|
+
|
|
180
|
+
:param host: Hostname or IP address of the alarm panel. Since the
|
|
181
|
+
protocol is UDP-based it is ok to use broadcast or directed broadcast
|
|
182
|
+
addresses, such as `255.255.255.255` or `10.10.10.255` (the latter assumes
|
|
183
|
+
the device is on `10.10.10.0/24` network)
|
|
184
|
+
:param port: The UDP port of the device where it listens for the
|
|
185
|
+
protocol commands on WiFi interface, currently the devices don't allow it
|
|
186
|
+
to be customized
|
|
187
|
+
:param reset_occupancy_interval: The interval upon that the sensors are
|
|
188
|
+
simulated to go into inactive state.
|
|
189
|
+
"""
|
|
190
|
+
# pylint: disable=too-many-instance-attributes,too-many-arguments
|
|
191
|
+
def __init__(
|
|
192
|
+
self, host: str, port: int = REMOTE_PORT,
|
|
193
|
+
reset_occupancy_interval: float = 3.0
|
|
194
|
+
) -> None:
|
|
195
|
+
self._host: str = host
|
|
196
|
+
self._port: int = port
|
|
197
|
+
self._notifications: Optional[G90NotificationsBase] = None
|
|
198
|
+
self._sensors = G90SensorList(self)
|
|
199
|
+
# The callback will be invoked when sensor list changes, e.g. sensor is
|
|
200
|
+
# added or updated
|
|
201
|
+
self._sensors.list_change_callback = self.on_sensor_list_change
|
|
202
|
+
self._devices = G90DeviceList(self)
|
|
203
|
+
# Similarly for the device list
|
|
204
|
+
self._devices.list_change_callback = self.on_device_list_change
|
|
205
|
+
self._sensor_cb: G90CallbackList[SensorCallback] = G90CallbackList()
|
|
206
|
+
self._armdisarm_cb: G90CallbackList[ArmDisarmCallback] = (
|
|
207
|
+
G90CallbackList()
|
|
208
|
+
)
|
|
209
|
+
self._door_open_close_cb: G90CallbackList[DoorOpenCloseCallback] = (
|
|
210
|
+
G90CallbackList()
|
|
211
|
+
)
|
|
212
|
+
self._alarm_cb: G90CallbackList[AlarmCallback] = G90CallbackList()
|
|
213
|
+
self._low_battery_cb: G90CallbackList[LowBatteryCallback] = (
|
|
214
|
+
G90CallbackList()
|
|
215
|
+
)
|
|
216
|
+
self._sos_cb: G90CallbackList[SosCallback] = G90CallbackList()
|
|
217
|
+
self._remote_button_press_cb: G90CallbackList[
|
|
218
|
+
RemoteButtonPressCallback
|
|
219
|
+
] = G90CallbackList()
|
|
220
|
+
self._door_open_when_arming_cb: G90CallbackList[
|
|
221
|
+
DoorOpenWhenArmingCallback
|
|
222
|
+
] = G90CallbackList()
|
|
223
|
+
self._tamper_cb: G90CallbackList[TamperCallback] = G90CallbackList()
|
|
224
|
+
self._sensor_list_change_cb: G90CallbackList[
|
|
225
|
+
ListChangeCallback[G90Sensor]
|
|
226
|
+
] = G90CallbackList()
|
|
227
|
+
self._device_list_change_cb: G90CallbackList[
|
|
228
|
+
ListChangeCallback[G90Device]
|
|
229
|
+
] = G90CallbackList()
|
|
230
|
+
self._reset_occupancy_interval = reset_occupancy_interval
|
|
231
|
+
self._alert_config = G90AlertConfig(self)
|
|
232
|
+
self._sms_alert_when_armed = False
|
|
233
|
+
self._alert_simulation_task: Optional[Task[Any]] = None
|
|
234
|
+
self._alert_simulation_start_listener_back = False
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def host(self) -> str:
|
|
238
|
+
"""
|
|
239
|
+
Returns the hostname or IP address of the alarm panel.
|
|
240
|
+
|
|
241
|
+
This is the address used for communication with the device.
|
|
242
|
+
"""
|
|
243
|
+
return self._host
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def port(self) -> int:
|
|
247
|
+
"""
|
|
248
|
+
Returns the UDP port number used to communicate with the alarm panel.
|
|
249
|
+
|
|
250
|
+
By default, this is set to the standard G90 protocol port.
|
|
251
|
+
"""
|
|
252
|
+
return self._port
|
|
253
|
+
|
|
254
|
+
async def command(
|
|
255
|
+
self, code: G90Commands, data: Optional[G90BaseCommandData] = None
|
|
256
|
+
) -> G90BaseCommandData:
|
|
257
|
+
"""
|
|
258
|
+
Invokes a command against the alarm panel.
|
|
259
|
+
|
|
260
|
+
:param code: Command code
|
|
261
|
+
:param data: Command data
|
|
262
|
+
:return: The result of command invocation
|
|
263
|
+
"""
|
|
264
|
+
cmd: G90BaseCommand = await G90BaseCommand(
|
|
265
|
+
self._host, self._port, code, data).process()
|
|
266
|
+
return cmd.result
|
|
267
|
+
|
|
268
|
+
def paginated_result(
|
|
269
|
+
self, code: G90Commands, start: int = 1, end: Optional[int] = None
|
|
270
|
+
) -> AsyncGenerator[G90PaginatedResponse, None]:
|
|
271
|
+
"""
|
|
272
|
+
Returns asynchronous generator for a paginated command, that is -
|
|
273
|
+
command operating on a range of records.
|
|
274
|
+
|
|
275
|
+
:param code: Command code
|
|
276
|
+
:param start: Starting record position (one-based)
|
|
277
|
+
:param end: Ending record position (one-based)
|
|
278
|
+
:return: Asynchronous generator over the result of command invocation.
|
|
279
|
+
"""
|
|
280
|
+
return G90PaginatedResult(
|
|
281
|
+
self._host, self._port, code, start, end
|
|
282
|
+
).process()
|
|
283
|
+
|
|
284
|
+
@classmethod
|
|
285
|
+
async def discover(cls) -> List[G90DiscoveredDevice]:
|
|
286
|
+
"""
|
|
287
|
+
Initiates discovering devices available in the same network segment, by
|
|
288
|
+
using global broadcast address as the destination.
|
|
289
|
+
|
|
290
|
+
:return: List of discovered devices
|
|
291
|
+
"""
|
|
292
|
+
cmd: G90Discovery = await G90Discovery(
|
|
293
|
+
port=REMOTE_PORT,
|
|
294
|
+
host='255.255.255.255'
|
|
295
|
+
).process()
|
|
296
|
+
return cmd.devices
|
|
297
|
+
|
|
298
|
+
@classmethod
|
|
299
|
+
async def targeted_discover(
|
|
300
|
+
cls, device_id: str
|
|
301
|
+
) -> List[G90DiscoveredDeviceTargeted]:
|
|
302
|
+
"""
|
|
303
|
+
Initiates discovering devices available in the same network segment
|
|
304
|
+
using targeted protocol, that is - specifying target device GUID in the
|
|
305
|
+
request, so only the specific device should respond to the query.
|
|
306
|
+
|
|
307
|
+
:param device_id: GUID of the target device to discover
|
|
308
|
+
:return: List of discovered devices
|
|
309
|
+
"""
|
|
310
|
+
cmd = await G90TargetedDiscovery(
|
|
311
|
+
device_id=device_id,
|
|
312
|
+
port=REMOTE_TARGETED_DISCOVERY_PORT,
|
|
313
|
+
local_port=LOCAL_TARGETED_DISCOVERY_PORT,
|
|
314
|
+
host='255.255.255.255'
|
|
315
|
+
).process()
|
|
316
|
+
return cmd.devices
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
async def sensors(self) -> List[G90Sensor]:
|
|
320
|
+
"""
|
|
321
|
+
Returns the list of sensors configured in the device. Please note
|
|
322
|
+
it doesn't update those from the panel except initially when the list
|
|
323
|
+
if empty.
|
|
324
|
+
|
|
325
|
+
:return: List of sensors
|
|
326
|
+
"""
|
|
327
|
+
return await self._sensors.entities
|
|
328
|
+
|
|
329
|
+
async def get_sensors(self) -> List[G90Sensor]:
|
|
330
|
+
"""
|
|
331
|
+
Provides list of sensors configured in the device, updating them from
|
|
332
|
+
panel on each call.
|
|
333
|
+
|
|
334
|
+
:return: List of sensors
|
|
335
|
+
"""
|
|
336
|
+
return await self._sensors.update()
|
|
337
|
+
|
|
338
|
+
async def find_sensor(
|
|
339
|
+
self, idx: int, name: str, exclude_unavailable: bool = True
|
|
340
|
+
) -> Optional[G90Sensor]:
|
|
341
|
+
"""
|
|
342
|
+
Finds sensor by index and name.
|
|
343
|
+
|
|
344
|
+
:param idx: Sensor index
|
|
345
|
+
:param name: Sensor name
|
|
346
|
+
:param exclude_unavailable: Flag indicating if unavailable sensors
|
|
347
|
+
should be excluded from the search
|
|
348
|
+
:return: Sensor instance
|
|
349
|
+
"""
|
|
350
|
+
return await self._sensors.find(idx, name, exclude_unavailable)
|
|
351
|
+
|
|
352
|
+
async def register_sensor(
|
|
353
|
+
self, definition_name: str, name: Optional[str] = None,
|
|
354
|
+
timeout: float = DEVICE_REGISTRATION_TIMEOUT
|
|
355
|
+
) -> G90Sensor:
|
|
356
|
+
"""
|
|
357
|
+
Registers the sensor with the panel.
|
|
358
|
+
|
|
359
|
+
:param definition_name: Name of the sensor definition to register
|
|
360
|
+
:param name: Optional name of the sensor to register, if not provided
|
|
361
|
+
the name will be taken from the definition
|
|
362
|
+
:param timeout: Timeout for the registration process, in seconds
|
|
363
|
+
:return: Sensor instance
|
|
364
|
+
"""
|
|
365
|
+
return await self._sensors.register(
|
|
366
|
+
definition_name, ROOM_ID, timeout, name
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
async def devices(self) -> List[G90Device]:
|
|
371
|
+
"""
|
|
372
|
+
Returns the list of devices (switches) configured in the device. Please
|
|
373
|
+
note it doesn't update those from the panel except initially when
|
|
374
|
+
the list if empty.
|
|
375
|
+
|
|
376
|
+
:return: List of devices
|
|
377
|
+
"""
|
|
378
|
+
return await self._devices.entities
|
|
379
|
+
|
|
380
|
+
async def get_devices(self) -> List[G90Device]:
|
|
381
|
+
"""
|
|
382
|
+
Provides list of devices (switches) configured in the device, updating
|
|
383
|
+
them from panel on each call. Multi-node devices, those
|
|
384
|
+
having multiple ports, are expanded into corresponding number of
|
|
385
|
+
resulting entries.
|
|
386
|
+
|
|
387
|
+
:return: List of devices
|
|
388
|
+
"""
|
|
389
|
+
return await self._devices.update()
|
|
390
|
+
|
|
391
|
+
async def find_device(
|
|
392
|
+
self, idx: int, name: str, exclude_unavailable: bool = True
|
|
393
|
+
) -> Optional[G90Device]:
|
|
394
|
+
"""
|
|
395
|
+
Finds device by index and name.
|
|
396
|
+
|
|
397
|
+
:param idx: Device index
|
|
398
|
+
:param name: Device name
|
|
399
|
+
:param exclude_unavailable: Flag indicating if unavailable devices
|
|
400
|
+
should be excluded from the search
|
|
401
|
+
:return: Device instance
|
|
402
|
+
"""
|
|
403
|
+
return await self._devices.find(idx, name, exclude_unavailable)
|
|
404
|
+
|
|
405
|
+
async def register_device(
|
|
406
|
+
self, definition_name: str, name: Optional[str] = None,
|
|
407
|
+
timeout: float = DEVICE_REGISTRATION_TIMEOUT
|
|
408
|
+
) -> G90Device:
|
|
409
|
+
"""
|
|
410
|
+
Registers device (relay, switch) with the panel.
|
|
411
|
+
|
|
412
|
+
:param definition_name: Name of the device definition to register
|
|
413
|
+
:param name: Optional name of the device to register, if not provided
|
|
414
|
+
the name will be taken from the definition
|
|
415
|
+
:param timeout: Timeout for the registration process, in seconds
|
|
416
|
+
:return: Device instance
|
|
417
|
+
"""
|
|
418
|
+
return await self._devices.register(
|
|
419
|
+
definition_name, ROOM_ID, timeout, name
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
@property
|
|
423
|
+
async def host_info(self) -> G90HostInfo:
|
|
424
|
+
"""
|
|
425
|
+
Property over new :meth:`.get_host_info` method, retained for
|
|
426
|
+
compatibility.
|
|
427
|
+
"""
|
|
428
|
+
return await self.get_host_info()
|
|
429
|
+
|
|
430
|
+
async def get_host_info(self) -> G90HostInfo:
|
|
431
|
+
"""
|
|
432
|
+
Provides the device information (for example hardware versions, signal
|
|
433
|
+
levels etc.).
|
|
434
|
+
|
|
435
|
+
:return: Device information
|
|
436
|
+
"""
|
|
437
|
+
res = await self.command(G90Commands.GETHOSTINFO)
|
|
438
|
+
info = G90HostInfo(*res)
|
|
439
|
+
if self._notifications:
|
|
440
|
+
self._notifications.device_id = info.host_guid
|
|
441
|
+
return info
|
|
442
|
+
|
|
443
|
+
@property
|
|
444
|
+
async def host_status(self) -> G90HostStatus:
|
|
445
|
+
"""
|
|
446
|
+
Property over new :meth:`.get_host_status` method, retained for
|
|
447
|
+
compatibility.
|
|
448
|
+
"""
|
|
449
|
+
return await self.get_host_status()
|
|
450
|
+
|
|
451
|
+
async def get_host_status(self) -> G90HostStatus:
|
|
452
|
+
"""
|
|
453
|
+
Provides the device status (for example, armed or disarmed, configured
|
|
454
|
+
phone number, product name etc.).
|
|
455
|
+
|
|
456
|
+
:return: Device information
|
|
457
|
+
|
|
458
|
+
"""
|
|
459
|
+
res = await self.command(G90Commands.GETHOSTSTATUS)
|
|
460
|
+
return G90HostStatus(*res)
|
|
461
|
+
|
|
462
|
+
@property
|
|
463
|
+
def alert_config(self) -> G90AlertConfig:
|
|
464
|
+
"""
|
|
465
|
+
Provides alert configuration object.
|
|
466
|
+
|
|
467
|
+
:return: Alert configuration object
|
|
468
|
+
"""
|
|
469
|
+
return self._alert_config
|
|
470
|
+
|
|
471
|
+
async def get_alert_config(self) -> G90AlertConfigFlags:
|
|
472
|
+
"""
|
|
473
|
+
Provides alert configuration flags, retained for compatibility - using
|
|
474
|
+
:attr:`alert_config` and :class:`.G90AlertConfig` is preferred.
|
|
475
|
+
|
|
476
|
+
:return: The alerts configured
|
|
477
|
+
"""
|
|
478
|
+
return await self.alert_config.flags
|
|
479
|
+
|
|
480
|
+
async def set_alert_config(self, flags: G90AlertConfigFlags) -> None:
|
|
481
|
+
"""
|
|
482
|
+
Sets the alert configuration flags, retained for compatibility - using
|
|
483
|
+
:attr:`alert_config` and :class:`.G90AlertConfig` is preferred.
|
|
484
|
+
"""
|
|
485
|
+
await self.alert_config.set(flags)
|
|
486
|
+
|
|
487
|
+
@property
|
|
488
|
+
async def user_data_crc(self) -> G90UserDataCRC:
|
|
489
|
+
"""
|
|
490
|
+
Property over new :meth:`.get_user_data_crc` method, retained for
|
|
491
|
+
compatibility.
|
|
492
|
+
"""
|
|
493
|
+
return await self.get_user_data_crc()
|
|
494
|
+
|
|
495
|
+
async def get_user_data_crc(self) -> G90UserDataCRC:
|
|
496
|
+
"""
|
|
497
|
+
Retieves checksums (CRC) for different on-device databases (history,
|
|
498
|
+
sensors etc.). Might be used to detect if there is a change in a
|
|
499
|
+
particular database.
|
|
500
|
+
|
|
501
|
+
.. note:: Note that due to a bug in the firmware CRC for sensors and
|
|
502
|
+
device databases change on each call even if there were no changes
|
|
503
|
+
|
|
504
|
+
:return: Checksums for different databases
|
|
505
|
+
"""
|
|
506
|
+
res = await self.command(G90Commands.GETUSERDATACRC)
|
|
507
|
+
return G90UserDataCRC(*res)
|
|
508
|
+
|
|
509
|
+
async def history(
|
|
510
|
+
self, start: int = 1, count: int = 1
|
|
511
|
+
) -> List[G90History]:
|
|
512
|
+
"""
|
|
513
|
+
Retrieves event history from the device.
|
|
514
|
+
|
|
515
|
+
:param start: Starting record number (one-based)
|
|
516
|
+
:param count: Number of records to retrieve
|
|
517
|
+
:return: List of history entries
|
|
518
|
+
"""
|
|
519
|
+
res = self.paginated_result(G90Commands.GETHISTORY,
|
|
520
|
+
start, count)
|
|
521
|
+
|
|
522
|
+
# Sort the history entries from older to newer - device typically does
|
|
523
|
+
# that, but apparently that is not guaranteed
|
|
524
|
+
return sorted(
|
|
525
|
+
[G90History(*x.data) async for x in res],
|
|
526
|
+
key=lambda x: x.datetime, reverse=True
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
async def on_sensor_activity(
|
|
530
|
+
self, idx: int, name: str, occupancy: bool = True
|
|
531
|
+
) -> None:
|
|
532
|
+
"""
|
|
533
|
+
Invoked both for sensor notifications and door open/close
|
|
534
|
+
alerts, since the logic for both is same and could be reused.
|
|
535
|
+
Fires corresponding callback if set by the user with
|
|
536
|
+
:attr:`.sensor_callback`.
|
|
537
|
+
|
|
538
|
+
Please note the method is for internal use by the class.
|
|
539
|
+
|
|
540
|
+
:param idx: The index of the sensor the callback is invoked for.
|
|
541
|
+
Please note the index is a property of sensor, not the direct index of
|
|
542
|
+
:attr:`sensors` array
|
|
543
|
+
:param name: The name of the sensor, along with the `idx` parameter
|
|
544
|
+
it is used to look the sensor up from the :attr:`sensors` list
|
|
545
|
+
:param occupancy: The flag indicating the target sensor state
|
|
546
|
+
(=occupancy), will always be `True` for callbacks invoked from alarm
|
|
547
|
+
panel notifications, and reflects actual sensor state for device
|
|
548
|
+
alerts (only for `door` type sensors, if door open/close alerts are
|
|
549
|
+
enabled)
|
|
550
|
+
"""
|
|
551
|
+
_LOGGER.debug('on_sensor_activity: %s %s %s', idx, name, occupancy)
|
|
552
|
+
sensor = await self.find_sensor(idx, name)
|
|
553
|
+
if sensor:
|
|
554
|
+
# Reset the low battery flag since the sensor reports activity,
|
|
555
|
+
# implying it has sufficient battery power
|
|
556
|
+
# pylint: disable=protected-access
|
|
557
|
+
sensor._set_low_battery(False)
|
|
558
|
+
# Set the sensor occupancy
|
|
559
|
+
# pylint: disable=protected-access
|
|
560
|
+
sensor._set_occupancy(occupancy)
|
|
561
|
+
|
|
562
|
+
# Emulate turning off the occupancy - most of sensors will not
|
|
563
|
+
# notify the device of that, nor the device would emit such
|
|
564
|
+
# notification itself
|
|
565
|
+
def reset_sensor_occupancy(sensor: G90Sensor) -> None:
|
|
566
|
+
sensor._set_occupancy(False)
|
|
567
|
+
sensor.state_callback.invoke(sensor.occupancy)
|
|
568
|
+
|
|
569
|
+
alert_config_flags = await self.alert_config.flags
|
|
570
|
+
# Determine if door close notifications are available for the given
|
|
571
|
+
# sensor
|
|
572
|
+
door_close_alert_enabled = (
|
|
573
|
+
G90AlertConfigFlags.DOOR_CLOSE in alert_config_flags
|
|
574
|
+
)
|
|
575
|
+
# The condition intentionally doesn't account for cord sensors of
|
|
576
|
+
# subtype door, since those won't send door open/close alerts, only
|
|
577
|
+
# notifications
|
|
578
|
+
sensor_is_door = sensor.type == G90PeripheralTypes.DOOR
|
|
579
|
+
|
|
580
|
+
# Alarm panel could emit door close alerts (if enabled) for sensors
|
|
581
|
+
# of type `door`, and such event will be used to reset the
|
|
582
|
+
# occupancy for the given sensor. Otherwise, the sensor closing
|
|
583
|
+
# event will be emulated
|
|
584
|
+
if not door_close_alert_enabled or not sensor_is_door:
|
|
585
|
+
_LOGGER.debug("Sensor '%s' is not a door (type %s),"
|
|
586
|
+
' or door close alert is disabled'
|
|
587
|
+
' (alert config flags %s) or is a cord sensor,'
|
|
588
|
+
' closing event will be emulated upon'
|
|
589
|
+
' %s seconds',
|
|
590
|
+
name, sensor.type,
|
|
591
|
+
alert_config_flags,
|
|
592
|
+
self._reset_occupancy_interval)
|
|
593
|
+
G90Callback.invoke_delayed(
|
|
594
|
+
self._reset_occupancy_interval,
|
|
595
|
+
reset_sensor_occupancy, sensor)
|
|
596
|
+
|
|
597
|
+
# Invoke per-sensor callback if provided
|
|
598
|
+
sensor.state_callback.invoke(occupancy)
|
|
599
|
+
|
|
600
|
+
# Invoke global callback if provided
|
|
601
|
+
self._sensor_cb.invoke(idx, name, occupancy)
|
|
602
|
+
|
|
603
|
+
@property
|
|
604
|
+
def sensor_callback(self) -> G90CallbackList[SensorCallback]:
|
|
605
|
+
"""
|
|
606
|
+
Sensor activity callback, which is invoked when sensor activates.
|
|
607
|
+
|
|
608
|
+
Setting the property will add the callback to the list of (retained for
|
|
609
|
+
compatilibity with earlier package versions), or
|
|
610
|
+
:class:`.G90CallbackList` instance could be accessed over the
|
|
611
|
+
property - `G90Alarm(...).sensor_callback.add(callback)` or
|
|
612
|
+
`G90Alarm(...).sensor_callback.remove(callback)` methods could be used
|
|
613
|
+
to add or remove the callback, respectively.
|
|
614
|
+
"""
|
|
615
|
+
return self._sensor_cb
|
|
616
|
+
|
|
617
|
+
@sensor_callback.setter
|
|
618
|
+
def sensor_callback(self, value: SensorCallback) -> None:
|
|
619
|
+
self._sensor_cb.add(value)
|
|
620
|
+
|
|
621
|
+
async def on_door_open_close(
|
|
622
|
+
self, event_id: int, zone_name: str, is_open: bool
|
|
623
|
+
) -> None:
|
|
624
|
+
"""
|
|
625
|
+
Invoked when door open/close alert comes from the alarm
|
|
626
|
+
panel. Fires corresponding callback if set by the user with
|
|
627
|
+
:attr:`.door_open_close_callback`.
|
|
628
|
+
|
|
629
|
+
Please note the method is for internal use by the class.
|
|
630
|
+
|
|
631
|
+
.. seealso:: :meth:`.on_sensor_activity` method for arguments
|
|
632
|
+
"""
|
|
633
|
+
# Same internal callback is reused both for door open/close alerts and
|
|
634
|
+
# sensor notifications. The former adds reporting when a door is
|
|
635
|
+
# closed, since the notifications aren't sent for such events
|
|
636
|
+
await self.on_sensor_activity(event_id, zone_name, is_open)
|
|
637
|
+
# Invoke user specified callback if any
|
|
638
|
+
self._door_open_close_cb.invoke(event_id, zone_name, is_open)
|
|
639
|
+
|
|
640
|
+
@property
|
|
641
|
+
def door_open_close_callback(
|
|
642
|
+
self
|
|
643
|
+
) -> G90CallbackList[DoorOpenCloseCallback]:
|
|
644
|
+
"""
|
|
645
|
+
The door open/close callback, which is invoked when door
|
|
646
|
+
is opened or closed (if corresponding alert is configured on the
|
|
647
|
+
device).
|
|
648
|
+
|
|
649
|
+
.. seealso:: :attr:`.sensor_callback` for compatiblity notes
|
|
650
|
+
"""
|
|
651
|
+
return self._door_open_close_cb
|
|
652
|
+
|
|
653
|
+
@door_open_close_callback.setter
|
|
654
|
+
def door_open_close_callback(self, value: DoorOpenCloseCallback) -> None:
|
|
655
|
+
self._door_open_close_cb.add(value)
|
|
656
|
+
|
|
657
|
+
async def on_armdisarm(self, state: G90ArmDisarmTypes) -> None:
|
|
658
|
+
"""
|
|
659
|
+
Invoked when the device is armed or disarmed. Fires corresponding
|
|
660
|
+
callback if set by the user with :attr:`.armdisarm_callback`.
|
|
661
|
+
|
|
662
|
+
Please note the method is for internal use by the class.
|
|
663
|
+
|
|
664
|
+
:param state: Device state (armed, disarmed, armed home)
|
|
665
|
+
"""
|
|
666
|
+
if self._sms_alert_when_armed:
|
|
667
|
+
await self.alert_config.set_flag(
|
|
668
|
+
G90AlertConfigFlags.SMS_PUSH,
|
|
669
|
+
state in (
|
|
670
|
+
G90ArmDisarmTypes.ARM_AWAY, G90ArmDisarmTypes.ARM_HOME
|
|
671
|
+
)
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
# Reset the tampered and door open when arming flags on all sensors
|
|
675
|
+
# having those set
|
|
676
|
+
for sensor in await self.sensors:
|
|
677
|
+
if sensor.is_tampered:
|
|
678
|
+
# pylint: disable=protected-access
|
|
679
|
+
sensor._set_tampered(False)
|
|
680
|
+
if sensor.is_door_open_when_arming:
|
|
681
|
+
# pylint: disable=protected-access
|
|
682
|
+
sensor._set_door_open_when_arming(False)
|
|
683
|
+
|
|
684
|
+
self._armdisarm_cb.invoke(state)
|
|
685
|
+
|
|
686
|
+
@property
|
|
687
|
+
def armdisarm_callback(self) -> G90CallbackList[ArmDisarmCallback]:
|
|
688
|
+
"""
|
|
689
|
+
The device arm/disarm callback, which is invoked when device state
|
|
690
|
+
changes.
|
|
691
|
+
|
|
692
|
+
.. seealso:: :attr:`.sensor_callback` for compatiblity notes
|
|
693
|
+
"""
|
|
694
|
+
return self._armdisarm_cb
|
|
695
|
+
|
|
696
|
+
@armdisarm_callback.setter
|
|
697
|
+
def armdisarm_callback(self, value: ArmDisarmCallback) -> None:
|
|
698
|
+
self._armdisarm_cb.add(value)
|
|
699
|
+
|
|
700
|
+
async def on_alarm(
|
|
701
|
+
self, event_id: int, zone_name: str, is_tampered: bool
|
|
702
|
+
) -> None:
|
|
703
|
+
"""
|
|
704
|
+
Invoked when alarm is triggered. Fires corresponding callback if set by
|
|
705
|
+
the user with :attr:`.alarm_callback`.
|
|
706
|
+
|
|
707
|
+
Please note the method is for internal use by the class.
|
|
708
|
+
|
|
709
|
+
:param event_id: Index of the sensor triggered alarm
|
|
710
|
+
:param zone_name: Sensor name
|
|
711
|
+
"""
|
|
712
|
+
sensor = await self.find_sensor(event_id, zone_name)
|
|
713
|
+
extra_data = None
|
|
714
|
+
if sensor:
|
|
715
|
+
# The callback is still delivered to the caller even if the sensor
|
|
716
|
+
# isn't found, only `extra_data` is skipped. That is to ensure the
|
|
717
|
+
# important callback isn't filtered
|
|
718
|
+
extra_data = sensor.extra_data
|
|
719
|
+
|
|
720
|
+
# Invoke the sensor activity callback to set the sensor occupancy
|
|
721
|
+
# if sensor is known, but only if that isn't already set - it helps
|
|
722
|
+
# when device notifications on triggerring sensor's activity aren't
|
|
723
|
+
# receveid by a reason
|
|
724
|
+
if not sensor.occupancy:
|
|
725
|
+
await self.on_sensor_activity(event_id, zone_name, True)
|
|
726
|
+
|
|
727
|
+
if is_tampered:
|
|
728
|
+
# Set the tampered flag on the sensor
|
|
729
|
+
# pylint: disable=protected-access
|
|
730
|
+
sensor._set_tampered(True)
|
|
731
|
+
|
|
732
|
+
# Invoke per-sensor callback if provided
|
|
733
|
+
sensor.tamper_callback.invoke()
|
|
734
|
+
|
|
735
|
+
# Invoke global tamper callback if provided and the sensor is tampered
|
|
736
|
+
if is_tampered:
|
|
737
|
+
self._tamper_cb.invoke(event_id, zone_name)
|
|
738
|
+
|
|
739
|
+
self._alarm_cb.invoke(event_id, zone_name, extra_data)
|
|
740
|
+
|
|
741
|
+
@property
|
|
742
|
+
def alarm_callback(self) -> G90CallbackList[AlarmCallback]:
|
|
743
|
+
"""
|
|
744
|
+
The device alarm callback, which is invoked when device alarm triggers.
|
|
745
|
+
|
|
746
|
+
.. seealso:: :attr:`.sensor_callback` for compatiblity notes
|
|
747
|
+
"""
|
|
748
|
+
return self._alarm_cb
|
|
749
|
+
|
|
750
|
+
@alarm_callback.setter
|
|
751
|
+
def alarm_callback(self, value: AlarmCallback) -> None:
|
|
752
|
+
self._alarm_cb.add(value)
|
|
753
|
+
|
|
754
|
+
async def on_low_battery(self, event_id: int, zone_name: str) -> None:
|
|
755
|
+
"""
|
|
756
|
+
Invoked when the sensor reports on low battery. Fires
|
|
757
|
+
corresponding callback if set by the user with
|
|
758
|
+
:attr:`.on_low_battery_callback`.
|
|
759
|
+
|
|
760
|
+
Please note the method is for internal use by the class.
|
|
761
|
+
|
|
762
|
+
:param event_id: Index of the sensor triggered alarm
|
|
763
|
+
:param zone_name: Sensor name
|
|
764
|
+
"""
|
|
765
|
+
_LOGGER.debug('on_low_battery: %s %s', event_id, zone_name)
|
|
766
|
+
sensor = await self.find_sensor(event_id, zone_name)
|
|
767
|
+
if sensor:
|
|
768
|
+
# Set the low battery flag on the sensor
|
|
769
|
+
# pylint: disable=protected-access
|
|
770
|
+
sensor._set_low_battery(True)
|
|
771
|
+
# Invoke per-sensor callback if provided
|
|
772
|
+
sensor.low_battery_callback.invoke()
|
|
773
|
+
|
|
774
|
+
self._low_battery_cb.invoke(event_id, zone_name)
|
|
775
|
+
|
|
776
|
+
@property
|
|
777
|
+
def low_battery_callback(self) -> G90CallbackList[LowBatteryCallback]:
|
|
778
|
+
"""
|
|
779
|
+
Low battery callback, which is invoked when sensor reports the
|
|
780
|
+
condition.
|
|
781
|
+
|
|
782
|
+
.. seealso:: :attr:`.sensor_callback` for compatiblity notes
|
|
783
|
+
"""
|
|
784
|
+
return self._low_battery_cb
|
|
785
|
+
|
|
786
|
+
@low_battery_callback.setter
|
|
787
|
+
def low_battery_callback(self, value: LowBatteryCallback) -> None:
|
|
788
|
+
self._low_battery_cb.add(value)
|
|
789
|
+
|
|
790
|
+
async def on_sos(
|
|
791
|
+
self, event_id: int, zone_name: str, is_host_sos: bool
|
|
792
|
+
) -> None:
|
|
793
|
+
"""
|
|
794
|
+
Invoked when SOS alert is triggered. Fires corresponding callback if
|
|
795
|
+
set by the user with :attr:`.sos_callback`.
|
|
796
|
+
|
|
797
|
+
Please note the method is for internal use by the class.
|
|
798
|
+
|
|
799
|
+
:param event_id: Index of the sensor triggered alarm
|
|
800
|
+
:param zone_name: Sensor name
|
|
801
|
+
:param is_host_sos:
|
|
802
|
+
Flag indicating if the SOS alert is triggered by the panel itself
|
|
803
|
+
(host)
|
|
804
|
+
"""
|
|
805
|
+
_LOGGER.debug('on_sos: %s %s %s', event_id, zone_name, is_host_sos)
|
|
806
|
+
self._sos_cb.invoke(event_id, zone_name, is_host_sos)
|
|
807
|
+
|
|
808
|
+
# Also report the event as alarm for unification, hard-coding the
|
|
809
|
+
# sensor name in case of host SOS
|
|
810
|
+
await self.on_alarm(
|
|
811
|
+
event_id, zone_name='Host SOS' if is_host_sos else zone_name,
|
|
812
|
+
is_tampered=False
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
if not is_host_sos:
|
|
816
|
+
# Also report the remote button press for SOS - the panel will not
|
|
817
|
+
# send corresponding alert
|
|
818
|
+
await self.on_remote_button_press(
|
|
819
|
+
event_id, zone_name, G90RemoteButtonStates.SOS
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
@property
|
|
823
|
+
def sos_callback(self) -> G90CallbackList[SosCallback]:
|
|
824
|
+
"""
|
|
825
|
+
SOS callback, which is invoked when SOS alert is triggered.
|
|
826
|
+
|
|
827
|
+
.. seealso:: :attr:`.sensor_callback` for compatiblity notes
|
|
828
|
+
"""
|
|
829
|
+
return self._sos_cb
|
|
830
|
+
|
|
831
|
+
@sos_callback.setter
|
|
832
|
+
def sos_callback(self, value: SosCallback) -> None:
|
|
833
|
+
self._sos_cb.add(value)
|
|
834
|
+
|
|
835
|
+
async def on_remote_button_press(
|
|
836
|
+
self, event_id: int, zone_name: str, button: G90RemoteButtonStates
|
|
837
|
+
) -> None:
|
|
838
|
+
"""
|
|
839
|
+
Invoked when remote button is pressed. Fires corresponding callback if
|
|
840
|
+
set by the user with :attr:`.remote_button_press_callback`.
|
|
841
|
+
|
|
842
|
+
Please note the method is for internal use by the class.
|
|
843
|
+
|
|
844
|
+
:param event_id: Index of the sensor triggered alarm
|
|
845
|
+
:param zone_name: Sensor name
|
|
846
|
+
:param button: The button pressed
|
|
847
|
+
"""
|
|
848
|
+
_LOGGER.debug(
|
|
849
|
+
'on_remote_button_press: %s %s %s', event_id, zone_name, button
|
|
850
|
+
)
|
|
851
|
+
self._remote_button_press_cb.invoke(event_id, zone_name, button)
|
|
852
|
+
|
|
853
|
+
# Also report the event as sensor activity for unification (remote is
|
|
854
|
+
# just a special type of the sensor)
|
|
855
|
+
await self.on_sensor_activity(event_id, zone_name, True)
|
|
856
|
+
|
|
857
|
+
@property
|
|
858
|
+
def remote_button_press_callback(
|
|
859
|
+
self
|
|
860
|
+
) -> G90CallbackList[RemoteButtonPressCallback]:
|
|
861
|
+
"""
|
|
862
|
+
Remote button press callback, which is invoked when remote button is
|
|
863
|
+
pressed.
|
|
864
|
+
|
|
865
|
+
.. seealso:: :attr:`.sensor_callback` for compatiblity notes
|
|
866
|
+
"""
|
|
867
|
+
return self._remote_button_press_cb
|
|
868
|
+
|
|
869
|
+
@remote_button_press_callback.setter
|
|
870
|
+
def remote_button_press_callback(
|
|
871
|
+
self, value: RemoteButtonPressCallback
|
|
872
|
+
) -> None:
|
|
873
|
+
self._remote_button_press_cb.add(value)
|
|
874
|
+
|
|
875
|
+
async def on_door_open_when_arming(
|
|
876
|
+
self, event_id: int, zone_name: str
|
|
877
|
+
) -> None:
|
|
878
|
+
"""
|
|
879
|
+
Invoked when door is open when arming the device. Fires corresponding
|
|
880
|
+
callback if set by the user with
|
|
881
|
+
:attr:`.door_open_when_arming_callback`.
|
|
882
|
+
|
|
883
|
+
Please note the method is for internal use by the class.
|
|
884
|
+
|
|
885
|
+
:param event_id: The index of the sensor being active when the panel
|
|
886
|
+
is being armed.
|
|
887
|
+
:param zone_name: The name of the sensor
|
|
888
|
+
"""
|
|
889
|
+
_LOGGER.debug('on_door_open_when_arming: %s %s', event_id, zone_name)
|
|
890
|
+
sensor = await self.find_sensor(event_id, zone_name)
|
|
891
|
+
if sensor:
|
|
892
|
+
# Set the low battery flag on the sensor
|
|
893
|
+
# pylint: disable=protected-access
|
|
894
|
+
sensor._set_door_open_when_arming(True)
|
|
895
|
+
# Invoke per-sensor callback if provided
|
|
896
|
+
sensor.door_open_when_arming_callback.invoke()
|
|
897
|
+
|
|
898
|
+
self._door_open_when_arming_cb.invoke(event_id, zone_name)
|
|
899
|
+
|
|
900
|
+
@property
|
|
901
|
+
def door_open_when_arming_callback(
|
|
902
|
+
self
|
|
903
|
+
) -> G90CallbackList[DoorOpenWhenArmingCallback]:
|
|
904
|
+
"""
|
|
905
|
+
Door open when arming callback, which is invoked when sensor reports
|
|
906
|
+
the condition.
|
|
907
|
+
|
|
908
|
+
.. seealso:: :attr:`.sensor_callback` for compatiblity notes
|
|
909
|
+
"""
|
|
910
|
+
return self._door_open_when_arming_cb
|
|
911
|
+
|
|
912
|
+
@door_open_when_arming_callback.setter
|
|
913
|
+
def door_open_when_arming_callback(
|
|
914
|
+
self, value: DoorOpenWhenArmingCallback
|
|
915
|
+
) -> None:
|
|
916
|
+
self._door_open_when_arming_cb.add(value)
|
|
917
|
+
|
|
918
|
+
@property
|
|
919
|
+
def tamper_callback(self) -> G90CallbackList[TamperCallback]:
|
|
920
|
+
"""
|
|
921
|
+
Tamper callback, which is invoked when sensor reports the condition.
|
|
922
|
+
"""
|
|
923
|
+
return self._tamper_cb
|
|
924
|
+
|
|
925
|
+
@tamper_callback.setter
|
|
926
|
+
def tamper_callback(self, value: TamperCallback) -> None:
|
|
927
|
+
self._tamper_cb.add(value)
|
|
928
|
+
|
|
929
|
+
async def on_sensor_change(
|
|
930
|
+
self, sensor_idx: int, sensor_name: str, added: bool
|
|
931
|
+
) -> None:
|
|
932
|
+
"""
|
|
933
|
+
Invoked when sensor is added or removed from the device.
|
|
934
|
+
|
|
935
|
+
There is no user-visible callback assoiciated with this method, those
|
|
936
|
+
will be handled by `on_sensor_list_change()` method.
|
|
937
|
+
|
|
938
|
+
Please note the method is for internal use by the class.
|
|
939
|
+
|
|
940
|
+
:param sensor_idx: The index of the sensor being added/removed.
|
|
941
|
+
:param sensor_name: The name of the sensor.
|
|
942
|
+
:param added: Flag indicating if the sensor is added or removed
|
|
943
|
+
"""
|
|
944
|
+
_LOGGER.debug(
|
|
945
|
+
'on_sensor_change: idx=%s name=%s added=%s',
|
|
946
|
+
sensor_idx, sensor_name, added
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
# Invoke internal callback for sensor list to finish the registration
|
|
950
|
+
# process
|
|
951
|
+
G90Callback.invoke(
|
|
952
|
+
self._sensors.sensor_change_callback,
|
|
953
|
+
sensor_idx, sensor_name, added
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
@property
|
|
957
|
+
def sensor_list_change_callback(
|
|
958
|
+
self
|
|
959
|
+
) -> G90CallbackList[ListChangeCallback[G90Sensor]]:
|
|
960
|
+
"""
|
|
961
|
+
Sensor list change callback, which is invoked when sensor list
|
|
962
|
+
changes.
|
|
963
|
+
|
|
964
|
+
.. seealso:: :attr:`.sensor_callback` for compatiblity notes
|
|
965
|
+
"""
|
|
966
|
+
return self._sensor_list_change_cb
|
|
967
|
+
|
|
968
|
+
@sensor_list_change_callback.setter
|
|
969
|
+
def sensor_list_change_callback(
|
|
970
|
+
self, value: ListChangeCallback[G90Sensor]
|
|
971
|
+
) -> None:
|
|
972
|
+
self._sensor_list_change_cb.add(value)
|
|
973
|
+
|
|
974
|
+
async def on_sensor_list_change(
|
|
975
|
+
self, sensor: G90Sensor, added: bool
|
|
976
|
+
) -> None:
|
|
977
|
+
"""
|
|
978
|
+
Invoked when sensor list is changed.
|
|
979
|
+
|
|
980
|
+
Fires corresponding callback if set by the user with
|
|
981
|
+
:attr:`.sensor_list_change_callback`.
|
|
982
|
+
Please note the method is for internal use by the class.
|
|
983
|
+
|
|
984
|
+
:param sensor: The sensor being added or removed
|
|
985
|
+
:param added: Flag indicating if the sensor is added or removed
|
|
986
|
+
"""
|
|
987
|
+
_LOGGER.debug(
|
|
988
|
+
'on_sensor_list_change: %s added=%s', repr(sensor), added
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
self._sensor_list_change_cb.invoke(sensor, added)
|
|
992
|
+
|
|
993
|
+
@property
|
|
994
|
+
def device_list_change_callback(
|
|
995
|
+
self
|
|
996
|
+
) -> G90CallbackList[ListChangeCallback[G90Device]]:
|
|
997
|
+
"""
|
|
998
|
+
Device list change callback, which is invoked when device list
|
|
999
|
+
changes.
|
|
1000
|
+
|
|
1001
|
+
.. seealso:: :attr:`.sensor_callback` for compatiblity notes
|
|
1002
|
+
"""
|
|
1003
|
+
return self._device_list_change_cb
|
|
1004
|
+
|
|
1005
|
+
@device_list_change_callback.setter
|
|
1006
|
+
def device_list_change_callback(
|
|
1007
|
+
self, value: ListChangeCallback[G90Device]
|
|
1008
|
+
) -> None:
|
|
1009
|
+
self._device_list_change_cb.add(value)
|
|
1010
|
+
|
|
1011
|
+
async def on_device_list_change(
|
|
1012
|
+
self, device: G90Device, added: bool
|
|
1013
|
+
) -> None:
|
|
1014
|
+
"""
|
|
1015
|
+
Invoked when device list is changed.
|
|
1016
|
+
|
|
1017
|
+
Fires corresponding callback if set by the user with
|
|
1018
|
+
:attr:`.device_list_change_callback`.
|
|
1019
|
+
|
|
1020
|
+
Please note the method is for internal use by the class.
|
|
1021
|
+
|
|
1022
|
+
:param device: The device being added or removed
|
|
1023
|
+
:param added: Flag indicating if the device is added or removed
|
|
1024
|
+
"""
|
|
1025
|
+
_LOGGER.debug(
|
|
1026
|
+
'on_device_list_change: %s added=%s', repr(device), added
|
|
1027
|
+
)
|
|
1028
|
+
|
|
1029
|
+
self._device_list_change_cb.invoke(device, added)
|
|
1030
|
+
|
|
1031
|
+
async def listen_notifications(self) -> None:
|
|
1032
|
+
"""
|
|
1033
|
+
Starts internal listener for device notifications/alerts.
|
|
1034
|
+
"""
|
|
1035
|
+
if self._notifications:
|
|
1036
|
+
await self._notifications.listen()
|
|
1037
|
+
|
|
1038
|
+
async def close_notifications(self) -> None:
|
|
1039
|
+
"""
|
|
1040
|
+
Closes the listener for device notifications/alerts.
|
|
1041
|
+
"""
|
|
1042
|
+
if self._notifications:
|
|
1043
|
+
await self._notifications.close()
|
|
1044
|
+
|
|
1045
|
+
async def arm_away(self) -> None:
|
|
1046
|
+
"""
|
|
1047
|
+
Arms the device in away mode.
|
|
1048
|
+
"""
|
|
1049
|
+
state = G90ArmDisarmTypes.ARM_AWAY
|
|
1050
|
+
await self.command(G90Commands.SETHOSTSTATUS,
|
|
1051
|
+
[state])
|
|
1052
|
+
|
|
1053
|
+
async def arm_home(self) -> None:
|
|
1054
|
+
"""
|
|
1055
|
+
Arms the device in home mode.
|
|
1056
|
+
"""
|
|
1057
|
+
state = G90ArmDisarmTypes.ARM_HOME
|
|
1058
|
+
await self.command(G90Commands.SETHOSTSTATUS,
|
|
1059
|
+
[state])
|
|
1060
|
+
|
|
1061
|
+
async def disarm(self) -> None:
|
|
1062
|
+
"""
|
|
1063
|
+
Disarms the device.
|
|
1064
|
+
"""
|
|
1065
|
+
state = G90ArmDisarmTypes.DISARM
|
|
1066
|
+
await self.command(G90Commands.SETHOSTSTATUS,
|
|
1067
|
+
[state])
|
|
1068
|
+
|
|
1069
|
+
@property
|
|
1070
|
+
def sms_alert_when_armed(self) -> bool:
|
|
1071
|
+
"""
|
|
1072
|
+
When enabled, allows to save costs on SMS by having corresponding alert
|
|
1073
|
+
enabled only when device is armed.
|
|
1074
|
+
"""
|
|
1075
|
+
return self._sms_alert_when_armed
|
|
1076
|
+
|
|
1077
|
+
@sms_alert_when_armed.setter
|
|
1078
|
+
def sms_alert_when_armed(self, value: bool) -> None:
|
|
1079
|
+
self._sms_alert_when_armed = value
|
|
1080
|
+
|
|
1081
|
+
async def start_simulating_alerts_from_history(
|
|
1082
|
+
self, interval: float = 5, history_depth: int = 5
|
|
1083
|
+
) -> None:
|
|
1084
|
+
"""
|
|
1085
|
+
Starts the separate task to simulate device alerts from history
|
|
1086
|
+
entries.
|
|
1087
|
+
|
|
1088
|
+
The listener for device notifications will be stopped, so device
|
|
1089
|
+
notifications will not be processed thus resulting in possible
|
|
1090
|
+
duplicated if those could be received from the network.
|
|
1091
|
+
|
|
1092
|
+
:param interval: Interval (in seconds) between polling for newer
|
|
1093
|
+
history entities
|
|
1094
|
+
:param history_depth: Amount of history entries to fetch during
|
|
1095
|
+
each polling cycle
|
|
1096
|
+
"""
|
|
1097
|
+
# Remember if device notifications listener has been started already
|
|
1098
|
+
self._alert_simulation_start_listener_back = (
|
|
1099
|
+
self._notifications is not None
|
|
1100
|
+
and self._notifications.listener_started
|
|
1101
|
+
)
|
|
1102
|
+
# And then stop it
|
|
1103
|
+
await self.close_notifications()
|
|
1104
|
+
|
|
1105
|
+
# Start the task
|
|
1106
|
+
self._alert_simulation_task = asyncio.create_task(
|
|
1107
|
+
self._simulate_alerts_from_history(interval, history_depth)
|
|
1108
|
+
)
|
|
1109
|
+
|
|
1110
|
+
async def stop_simulating_alerts_from_history(self) -> None:
|
|
1111
|
+
"""
|
|
1112
|
+
Stops the task simulating device alerts from history entries.
|
|
1113
|
+
|
|
1114
|
+
The listener for device notifications will be started back, if it was
|
|
1115
|
+
running when simulation has been started.
|
|
1116
|
+
"""
|
|
1117
|
+
# Stop the task simulating the device alerts from history if it was
|
|
1118
|
+
# running
|
|
1119
|
+
if self._alert_simulation_task:
|
|
1120
|
+
self._alert_simulation_task.cancel()
|
|
1121
|
+
self._alert_simulation_task = None
|
|
1122
|
+
|
|
1123
|
+
# Start device notifications listener back if it was running when
|
|
1124
|
+
# simulated alerts have been enabled
|
|
1125
|
+
if (
|
|
1126
|
+
self._notifications
|
|
1127
|
+
and self._alert_simulation_start_listener_back
|
|
1128
|
+
):
|
|
1129
|
+
await self._notifications.listen()
|
|
1130
|
+
|
|
1131
|
+
async def _simulate_alerts_from_history(
|
|
1132
|
+
self, interval: float, history_depth: int
|
|
1133
|
+
) -> None:
|
|
1134
|
+
"""
|
|
1135
|
+
Periodically fetches history entries from the device and simulates
|
|
1136
|
+
device alerts off of those.
|
|
1137
|
+
|
|
1138
|
+
Only the history entries occur after the process is started are
|
|
1139
|
+
handled, to avoid triggering callbacks retrospectively.
|
|
1140
|
+
|
|
1141
|
+
See :meth:`.start_simulating_alerts_from_history` for the parameters.
|
|
1142
|
+
"""
|
|
1143
|
+
dummy_notifications = G90NotificationsBase(
|
|
1144
|
+
protocol_factory=lambda: self
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
last_history_ts = None
|
|
1148
|
+
|
|
1149
|
+
_LOGGER.debug(
|
|
1150
|
+
'Simulating device alerts from history:'
|
|
1151
|
+
' interval %s, history depth %s',
|
|
1152
|
+
interval, history_depth
|
|
1153
|
+
)
|
|
1154
|
+
while True:
|
|
1155
|
+
try:
|
|
1156
|
+
# Retrieve the history entries of the specified amount - full
|
|
1157
|
+
# history retrieval might be an unnecessary long operation
|
|
1158
|
+
history = await self.history(count=history_depth)
|
|
1159
|
+
|
|
1160
|
+
# Initial iteration where no timestamp of most recent history
|
|
1161
|
+
# entry is recorded - do that and skip to next iteration, since
|
|
1162
|
+
# it isn't yet known what entries would be considered as new
|
|
1163
|
+
# ones.
|
|
1164
|
+
# Empty history will skip recording the timestamp and the
|
|
1165
|
+
# looping over entries below, effectively skipping to next
|
|
1166
|
+
# iteration
|
|
1167
|
+
if not last_history_ts and history:
|
|
1168
|
+
# First entry in the list is assumed to be the most recent
|
|
1169
|
+
# one
|
|
1170
|
+
last_history_ts = history[0].datetime
|
|
1171
|
+
_LOGGER.debug(
|
|
1172
|
+
'Initial time stamp of last history entry: %s',
|
|
1173
|
+
last_history_ts
|
|
1174
|
+
)
|
|
1175
|
+
continue
|
|
1176
|
+
|
|
1177
|
+
# Process history entries from older to newer to preserve the
|
|
1178
|
+
# order of happenings
|
|
1179
|
+
for item in reversed(history):
|
|
1180
|
+
# Process only the entries newer than one been recorded as
|
|
1181
|
+
# most recent one
|
|
1182
|
+
if last_history_ts and item.datetime > last_history_ts:
|
|
1183
|
+
_LOGGER.debug(
|
|
1184
|
+
'Found newer history entry: %s, simulating alert',
|
|
1185
|
+
repr(item)
|
|
1186
|
+
)
|
|
1187
|
+
# Send the history entry down the device notification
|
|
1188
|
+
# code as alert, as if it came from the device and its
|
|
1189
|
+
# notifications port
|
|
1190
|
+
dummy_notifications.handle_alert(
|
|
1191
|
+
item.as_device_alert(),
|
|
1192
|
+
# Skip verifying device GUID, since history entry
|
|
1193
|
+
# don't have it
|
|
1194
|
+
verify_device_id=False
|
|
1195
|
+
)
|
|
1196
|
+
|
|
1197
|
+
# Record the entry as most recent one
|
|
1198
|
+
last_history_ts = item.datetime
|
|
1199
|
+
_LOGGER.debug(
|
|
1200
|
+
'Time stamp of last history entry: %s',
|
|
1201
|
+
last_history_ts
|
|
1202
|
+
)
|
|
1203
|
+
except (G90Error, G90TimeoutError) as exc:
|
|
1204
|
+
_LOGGER.debug(
|
|
1205
|
+
'Error interacting with device, ignoring %s', repr(exc)
|
|
1206
|
+
)
|
|
1207
|
+
except Exception as exc:
|
|
1208
|
+
_LOGGER.error(
|
|
1209
|
+
'Exception simulating device alerts from history: %s',
|
|
1210
|
+
repr(exc)
|
|
1211
|
+
)
|
|
1212
|
+
raise exc
|
|
1213
|
+
|
|
1214
|
+
# Sleep to next iteration
|
|
1215
|
+
await asyncio.sleep(interval)
|
|
1216
|
+
|
|
1217
|
+
async def use_local_notifications(
|
|
1218
|
+
self, notifications_local_host: str = LOCAL_NOTIFICATIONS_HOST,
|
|
1219
|
+
notifications_local_port: int = LOCAL_NOTIFICATIONS_PORT
|
|
1220
|
+
) -> None:
|
|
1221
|
+
"""
|
|
1222
|
+
Switches to use local notifications for device alerts.
|
|
1223
|
+
"""
|
|
1224
|
+
await self.close_notifications()
|
|
1225
|
+
|
|
1226
|
+
self._notifications = G90LocalNotifications(
|
|
1227
|
+
protocol_factory=lambda: self,
|
|
1228
|
+
host=self._host,
|
|
1229
|
+
port=self._port,
|
|
1230
|
+
local_host=notifications_local_host,
|
|
1231
|
+
local_port=notifications_local_port
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
# pylint: disable=too-many-positional-arguments
|
|
1235
|
+
async def use_cloud_notifications(
|
|
1236
|
+
self, cloud_local_host: str = CLOUD_NOTIFICATIONS_HOST,
|
|
1237
|
+
cloud_local_port: int = CLOUD_NOTIFICATIONS_PORT,
|
|
1238
|
+
upstream_host: Optional[str] = REMOTE_CLOUD_HOST,
|
|
1239
|
+
upstream_port: Optional[int] = REMOTE_CLOUD_PORT,
|
|
1240
|
+
keep_single_connection: bool = True
|
|
1241
|
+
) -> None:
|
|
1242
|
+
"""
|
|
1243
|
+
Switches to use cloud notifications for device alerts.
|
|
1244
|
+
"""
|
|
1245
|
+
await self.close_notifications()
|
|
1246
|
+
|
|
1247
|
+
self._notifications = G90CloudNotifications(
|
|
1248
|
+
protocol_factory=lambda: self,
|
|
1249
|
+
upstream_host=upstream_host,
|
|
1250
|
+
upstream_port=upstream_port,
|
|
1251
|
+
local_host=cloud_local_host,
|
|
1252
|
+
local_port=cloud_local_port,
|
|
1253
|
+
keep_single_connection=keep_single_connection
|
|
1254
|
+
)
|
|
1255
|
+
|
|
1256
|
+
@property
|
|
1257
|
+
def last_device_packet_time(self) -> Optional[datetime]:
|
|
1258
|
+
"""
|
|
1259
|
+
Returns the time of the last packet received from the device.
|
|
1260
|
+
"""
|
|
1261
|
+
if not self._notifications:
|
|
1262
|
+
return None
|
|
1263
|
+
|
|
1264
|
+
return self._notifications.last_device_packet_time
|
|
1265
|
+
|
|
1266
|
+
@property
|
|
1267
|
+
def last_upstream_packet_time(self) -> Optional[datetime]:
|
|
1268
|
+
"""
|
|
1269
|
+
Returns the time of the last packet received from the upstream server.
|
|
1270
|
+
"""
|
|
1271
|
+
if not self._notifications:
|
|
1272
|
+
return None
|
|
1273
|
+
|
|
1274
|
+
return self._notifications.last_upstream_packet_time
|