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,481 @@
|
|
|
1
|
+
# Copyright (c) 2025 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
|
+
Implements support for notifications/alerts sent by G90 alarm panel.
|
|
23
|
+
"""
|
|
24
|
+
import json
|
|
25
|
+
import logging
|
|
26
|
+
from typing import (
|
|
27
|
+
Optional, List, Any, Callable
|
|
28
|
+
)
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from asyncio.transports import BaseTransport
|
|
31
|
+
from datetime import datetime, timezone
|
|
32
|
+
|
|
33
|
+
from ..callback import G90Callback
|
|
34
|
+
from ..const import (
|
|
35
|
+
G90MessageTypes,
|
|
36
|
+
G90NotificationTypes,
|
|
37
|
+
G90AlertTypes,
|
|
38
|
+
G90AlertStateChangeTypes,
|
|
39
|
+
G90ArmDisarmTypes,
|
|
40
|
+
G90AlertSources,
|
|
41
|
+
G90AlertStates,
|
|
42
|
+
G90RemoteButtonStates,
|
|
43
|
+
)
|
|
44
|
+
from .protocol import G90NotificationProtocol
|
|
45
|
+
|
|
46
|
+
_LOGGER = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class G90Message:
|
|
51
|
+
"""
|
|
52
|
+
Represents the message received from the device.
|
|
53
|
+
|
|
54
|
+
:meta private:
|
|
55
|
+
"""
|
|
56
|
+
code: G90MessageTypes
|
|
57
|
+
data: List[Any]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class G90Notification:
|
|
62
|
+
"""
|
|
63
|
+
Represents the notification received from the device.
|
|
64
|
+
|
|
65
|
+
:meta private:
|
|
66
|
+
"""
|
|
67
|
+
kind: G90NotificationTypes
|
|
68
|
+
data: List[Any]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class G90ZoneInfo:
|
|
73
|
+
"""
|
|
74
|
+
Represents zone details received from the device.
|
|
75
|
+
|
|
76
|
+
:meta private:
|
|
77
|
+
"""
|
|
78
|
+
idx: int
|
|
79
|
+
name: str
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class G90ArmDisarmInfo:
|
|
84
|
+
"""
|
|
85
|
+
Represents the arm/disarm state received from the device.
|
|
86
|
+
|
|
87
|
+
:meta private:
|
|
88
|
+
"""
|
|
89
|
+
state: int
|
|
90
|
+
|
|
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
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class G90DeviceAlert: # pylint: disable=too-many-instance-attributes
|
|
106
|
+
"""
|
|
107
|
+
Represents alert received from the device.
|
|
108
|
+
"""
|
|
109
|
+
type: G90AlertTypes
|
|
110
|
+
event_id: G90AlertStateChangeTypes
|
|
111
|
+
source: G90AlertSources
|
|
112
|
+
state: int
|
|
113
|
+
zone_name: str
|
|
114
|
+
device_id: str
|
|
115
|
+
unix_time: int
|
|
116
|
+
resv4: int
|
|
117
|
+
other: str
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class G90NotificationsBase:
|
|
121
|
+
"""
|
|
122
|
+
Implements support for notifications/alerts sent by alarm panel.
|
|
123
|
+
|
|
124
|
+
There is a basic check to ensure only notifications/alerts from the correct
|
|
125
|
+
device are processed - the check uses the host and port of the device, and
|
|
126
|
+
the device ID (GUID) that is set by the ancestor class that implements the
|
|
127
|
+
commands (e.g. :class:`.G90Alarm`). The latter to work correctly needs a
|
|
128
|
+
command to be performed first, one that fetches device GUID and then stores
|
|
129
|
+
it using :attr:`.device_id` (e.g. :meth:`.G90Alarm.get_host_info`).
|
|
130
|
+
"""
|
|
131
|
+
def __init__(
|
|
132
|
+
self, protocol_factory: Callable[[], G90NotificationProtocol],
|
|
133
|
+
):
|
|
134
|
+
# pylint: disable=too-many-arguments
|
|
135
|
+
self._transport: Optional[BaseTransport] = None
|
|
136
|
+
# Same but for device ID (GUID) - the notifications logic uses it to
|
|
137
|
+
# perform validation, but doesn't set it from messages received (it
|
|
138
|
+
# will diminish the purpose of the validation, should be done by an
|
|
139
|
+
# ancestor class).
|
|
140
|
+
self._device_id: Optional[str] = None
|
|
141
|
+
self._protocol = protocol_factory()
|
|
142
|
+
self._last_device_packet_time: Optional[datetime] = None
|
|
143
|
+
self._last_upstream_packet_time: Optional[datetime] = None
|
|
144
|
+
|
|
145
|
+
def handle_notification(
|
|
146
|
+
self, notification: G90Notification
|
|
147
|
+
) -> None:
|
|
148
|
+
"""
|
|
149
|
+
Handles notification received from the device.
|
|
150
|
+
|
|
151
|
+
:param notification: The notification to handle.
|
|
152
|
+
"""
|
|
153
|
+
# Sensor activity notification
|
|
154
|
+
if notification.kind == G90NotificationTypes.SENSOR_ACTIVITY:
|
|
155
|
+
g90_zone_info = G90ZoneInfo(*notification.data)
|
|
156
|
+
|
|
157
|
+
_LOGGER.debug('Sensor notification: %s', g90_zone_info)
|
|
158
|
+
G90Callback.invoke(
|
|
159
|
+
self._protocol.on_sensor_activity,
|
|
160
|
+
g90_zone_info.idx, g90_zone_info.name
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
# Arm/disarm notification
|
|
166
|
+
if notification.kind == G90NotificationTypes.ARM_DISARM:
|
|
167
|
+
g90_armdisarm_info = G90ArmDisarmInfo(
|
|
168
|
+
*notification.data)
|
|
169
|
+
# Map the state received from the device to corresponding enum
|
|
170
|
+
state = G90ArmDisarmTypes(g90_armdisarm_info.state)
|
|
171
|
+
|
|
172
|
+
_LOGGER.debug('Arm/disarm notification: %s',
|
|
173
|
+
state)
|
|
174
|
+
G90Callback.invoke(self._protocol.on_armdisarm, state)
|
|
175
|
+
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
# An open door is detected when arming
|
|
179
|
+
if notification.kind == G90NotificationTypes.DOOR_OPEN_WHEN_ARMING:
|
|
180
|
+
g90_zone_info = G90ZoneInfo(*notification.data)
|
|
181
|
+
_LOGGER.debug('Door open detected when arming: %s', g90_zone_info)
|
|
182
|
+
G90Callback.invoke(
|
|
183
|
+
self._protocol.on_door_open_when_arming,
|
|
184
|
+
g90_zone_info.idx, g90_zone_info.name
|
|
185
|
+
)
|
|
186
|
+
return
|
|
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
|
+
|
|
203
|
+
_LOGGER.warning('Unknown notification received:'
|
|
204
|
+
' kind %s, data %s',
|
|
205
|
+
notification.kind, notification.data)
|
|
206
|
+
|
|
207
|
+
def _handle_alert_sensor_activity(self, alert: G90DeviceAlert) -> bool:
|
|
208
|
+
"""
|
|
209
|
+
Handles sensor activity alert.
|
|
210
|
+
"""
|
|
211
|
+
if alert.source == G90AlertSources.REMOTE:
|
|
212
|
+
_LOGGER.debug('Remote button press alert: %s', alert)
|
|
213
|
+
G90Callback.invoke(
|
|
214
|
+
self._protocol.on_remote_button_press,
|
|
215
|
+
alert.event_id, alert.zone_name,
|
|
216
|
+
G90RemoteButtonStates(alert.state)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return True
|
|
220
|
+
|
|
221
|
+
if alert.state in (
|
|
222
|
+
G90AlertStates.DOOR_OPEN, G90AlertStates.DOOR_CLOSE
|
|
223
|
+
):
|
|
224
|
+
is_open = (
|
|
225
|
+
alert.source == G90AlertSources.SENSOR
|
|
226
|
+
and alert.state == G90AlertStates.DOOR_OPEN # noqa: W503
|
|
227
|
+
) or alert.source == G90AlertSources.DOORBELL
|
|
228
|
+
|
|
229
|
+
_LOGGER.debug('Door open_close alert: %s', alert)
|
|
230
|
+
G90Callback.invoke(
|
|
231
|
+
self._protocol.on_door_open_close,
|
|
232
|
+
alert.event_id, alert.zone_name, is_open
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
if (
|
|
238
|
+
alert.source == G90AlertSources.SENSOR
|
|
239
|
+
and alert.state == G90AlertStates.LOW_BATTERY # noqa: W503
|
|
240
|
+
):
|
|
241
|
+
_LOGGER.debug('Low battery alert: %s', alert)
|
|
242
|
+
G90Callback.invoke(
|
|
243
|
+
self._protocol.on_low_battery,
|
|
244
|
+
alert.event_id, alert.zone_name
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return True
|
|
248
|
+
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
def handle_alert(
|
|
252
|
+
self, alert: G90DeviceAlert,
|
|
253
|
+
verify_device_id: bool = True
|
|
254
|
+
) -> None:
|
|
255
|
+
"""
|
|
256
|
+
Handles alert received from the device.
|
|
257
|
+
|
|
258
|
+
:param alert: The alert to handle.
|
|
259
|
+
:param verify_device_id: Whether to verify the device ID (GUID) in the
|
|
260
|
+
alert. If set to False, the device ID will not be verified.
|
|
261
|
+
"""
|
|
262
|
+
handled = False
|
|
263
|
+
|
|
264
|
+
# Stop processing when alert is received from the device with different
|
|
265
|
+
# GUID (if enabled)
|
|
266
|
+
if (
|
|
267
|
+
verify_device_id and self.device_id
|
|
268
|
+
and alert.device_id != self.device_id
|
|
269
|
+
):
|
|
270
|
+
_LOGGER.error(
|
|
271
|
+
"Received alert from wrong device: expected '%s', got '%s'",
|
|
272
|
+
self.device_id, alert.device_id
|
|
273
|
+
)
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
if alert.type == G90AlertTypes.SENSOR_ACTIVITY:
|
|
277
|
+
handled = self._handle_alert_sensor_activity(alert)
|
|
278
|
+
|
|
279
|
+
if alert.type == G90AlertTypes.STATE_CHANGE:
|
|
280
|
+
# Define the mapping between device state received in the alert, to
|
|
281
|
+
# common `G90ArmDisarmTypes` enum that is used when setting device
|
|
282
|
+
# arm state and received in the corresponding notifications. The
|
|
283
|
+
# primary reason is to unify state as passed down to the callbacks.
|
|
284
|
+
# The map covers only subset of state changes pertinent to
|
|
285
|
+
# arm/disarm state changes
|
|
286
|
+
alarm_arm_disarm_state_map = {
|
|
287
|
+
G90AlertStateChangeTypes.ARM_HOME: G90ArmDisarmTypes.ARM_HOME,
|
|
288
|
+
G90AlertStateChangeTypes.ARM_AWAY: G90ArmDisarmTypes.ARM_AWAY,
|
|
289
|
+
G90AlertStateChangeTypes.DISARM: G90ArmDisarmTypes.DISARM
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
state = alarm_arm_disarm_state_map.get(alert.event_id, None)
|
|
293
|
+
if state:
|
|
294
|
+
# We received the device state change related to arm/disarm,
|
|
295
|
+
# invoke the corresponding callback
|
|
296
|
+
_LOGGER.debug('Arm/disarm state change: %s', state)
|
|
297
|
+
G90Callback.invoke(self._protocol.on_armdisarm, state)
|
|
298
|
+
|
|
299
|
+
handled = True
|
|
300
|
+
|
|
301
|
+
if alert.type == G90AlertTypes.ALARM:
|
|
302
|
+
# Remote SOS
|
|
303
|
+
if alert.source == G90AlertSources.REMOTE:
|
|
304
|
+
_LOGGER.debug('SOS: %s', alert.zone_name)
|
|
305
|
+
G90Callback.invoke(
|
|
306
|
+
self._protocol.on_sos,
|
|
307
|
+
alert.event_id, alert.zone_name, False
|
|
308
|
+
)
|
|
309
|
+
# Regular alarm
|
|
310
|
+
else:
|
|
311
|
+
is_tampered = alert.state == G90AlertStates.TAMPER
|
|
312
|
+
_LOGGER.debug(
|
|
313
|
+
'Alarm: %s, is tampered: %s', alert.zone_name, is_tampered
|
|
314
|
+
)
|
|
315
|
+
G90Callback.invoke(
|
|
316
|
+
self._protocol.on_alarm,
|
|
317
|
+
alert.event_id, alert.zone_name, is_tampered
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
handled = True
|
|
321
|
+
|
|
322
|
+
# Host SOS
|
|
323
|
+
if alert.type == G90AlertTypes.HOST_SOS:
|
|
324
|
+
zone_name = 'Host SOS'
|
|
325
|
+
|
|
326
|
+
_LOGGER.debug('SOS: Host')
|
|
327
|
+
G90Callback.invoke(
|
|
328
|
+
self._protocol.on_sos, alert.event_id, zone_name, True
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
handled = True
|
|
332
|
+
|
|
333
|
+
if not handled:
|
|
334
|
+
_LOGGER.warning(
|
|
335
|
+
'Unknown alert received: type %s, data %s',
|
|
336
|
+
alert.type, alert
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# pylint:disable=too-many-return-statements
|
|
340
|
+
def handle(self, data: bytes) -> None:
|
|
341
|
+
"""
|
|
342
|
+
Invoked when message is received from the device.
|
|
343
|
+
"""
|
|
344
|
+
try:
|
|
345
|
+
s_data = data.decode('utf-8')
|
|
346
|
+
except UnicodeDecodeError:
|
|
347
|
+
_LOGGER.error('Unable to decode device message from UTF-8')
|
|
348
|
+
return
|
|
349
|
+
|
|
350
|
+
if not s_data.endswith('\0'):
|
|
351
|
+
_LOGGER.error('Missing end marker in data')
|
|
352
|
+
return
|
|
353
|
+
payload = s_data[:-1]
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
message = json.loads(payload)
|
|
357
|
+
g90_message = G90Message(*message)
|
|
358
|
+
except json.JSONDecodeError as exc:
|
|
359
|
+
_LOGGER.error("Unable to parse device message '%s' as JSON: %s",
|
|
360
|
+
payload, exc)
|
|
361
|
+
return
|
|
362
|
+
except TypeError as exc:
|
|
363
|
+
_LOGGER.error("Device message '%s' is malformed: %s",
|
|
364
|
+
payload, exc)
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
# Device notifications
|
|
368
|
+
if g90_message.code == G90MessageTypes.NOTIFICATION:
|
|
369
|
+
try:
|
|
370
|
+
notification_data = G90Notification(*g90_message.data)
|
|
371
|
+
except TypeError as exc:
|
|
372
|
+
_LOGGER.error('Bad notification received: %s', exc)
|
|
373
|
+
return
|
|
374
|
+
self.handle_notification(notification_data)
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
# Device alerts
|
|
378
|
+
if g90_message.code == G90MessageTypes.ALERT:
|
|
379
|
+
try:
|
|
380
|
+
alert_data = G90DeviceAlert(*g90_message.data)
|
|
381
|
+
except TypeError as exc:
|
|
382
|
+
_LOGGER.error('Bad alert received: %s', exc)
|
|
383
|
+
return
|
|
384
|
+
self.handle_alert(alert_data)
|
|
385
|
+
return
|
|
386
|
+
|
|
387
|
+
_LOGGER.warning('Unknown message received: %s', message)
|
|
388
|
+
|
|
389
|
+
async def listen(self) -> None:
|
|
390
|
+
"""
|
|
391
|
+
Listens for notifications/alerts from the device.
|
|
392
|
+
"""
|
|
393
|
+
raise NotImplementedError
|
|
394
|
+
|
|
395
|
+
@property
|
|
396
|
+
def listener_started(self) -> bool:
|
|
397
|
+
"""
|
|
398
|
+
Indicates if the listener of the device notifications has been started.
|
|
399
|
+
"""
|
|
400
|
+
return self._transport is not None
|
|
401
|
+
|
|
402
|
+
async def close(self) -> None:
|
|
403
|
+
"""
|
|
404
|
+
Closes the listener.
|
|
405
|
+
"""
|
|
406
|
+
if self._transport:
|
|
407
|
+
_LOGGER.debug('No longer listening for device notifications')
|
|
408
|
+
self._transport.close()
|
|
409
|
+
self._transport = None
|
|
410
|
+
|
|
411
|
+
@property
|
|
412
|
+
def device_id(self) -> Optional[str]:
|
|
413
|
+
"""
|
|
414
|
+
The ID (GUID) of the panel being communicated with thru commands.
|
|
415
|
+
|
|
416
|
+
Available when any panel command receives it from the device
|
|
417
|
+
(`GETHOSTINFO` local command or Hello / HelloDiscovery cloud ones).
|
|
418
|
+
"""
|
|
419
|
+
return self._device_id
|
|
420
|
+
|
|
421
|
+
@device_id.setter
|
|
422
|
+
def device_id(self, device_id: str) -> None:
|
|
423
|
+
# Under not yet identified circumstances the device ID might be empty
|
|
424
|
+
# string provided by :meth:`G90Alarm.get_host_info` - disallow that
|
|
425
|
+
if not device_id or len(device_id.strip()) == 0:
|
|
426
|
+
_LOGGER.debug(
|
|
427
|
+
'Device ID is empty or contains whitespace only, not setting'
|
|
428
|
+
)
|
|
429
|
+
return
|
|
430
|
+
|
|
431
|
+
self._device_id = device_id
|
|
432
|
+
|
|
433
|
+
def clear_device_id(self) -> None:
|
|
434
|
+
"""
|
|
435
|
+
Clears the device ID.
|
|
436
|
+
"""
|
|
437
|
+
self._device_id = None
|
|
438
|
+
|
|
439
|
+
@property
|
|
440
|
+
def last_device_packet_time(self) -> Optional[datetime]:
|
|
441
|
+
"""
|
|
442
|
+
Returns the timestamp of the last packet received from the device.
|
|
443
|
+
|
|
444
|
+
This property can be used to monitor the communication health with the
|
|
445
|
+
device.
|
|
446
|
+
"""
|
|
447
|
+
return self._last_device_packet_time
|
|
448
|
+
|
|
449
|
+
def set_last_device_packet_time(self) -> None:
|
|
450
|
+
"""
|
|
451
|
+
Updates the timestamp of the last packet received from the device.
|
|
452
|
+
|
|
453
|
+
This method is called internally when a packet is received from the
|
|
454
|
+
device.
|
|
455
|
+
"""
|
|
456
|
+
self._last_device_packet_time = datetime.now(tz=timezone.utc)
|
|
457
|
+
_LOGGER.debug(
|
|
458
|
+
'Last device packet time: %s', self._last_device_packet_time
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
def last_upstream_packet_time(self) -> Optional[datetime]:
|
|
463
|
+
"""
|
|
464
|
+
Returns the timestamp of the last packet sent to the upstream server.
|
|
465
|
+
|
|
466
|
+
This property can be used to monitor the communication health with the
|
|
467
|
+
cloud/upstream server.
|
|
468
|
+
"""
|
|
469
|
+
return self._last_upstream_packet_time
|
|
470
|
+
|
|
471
|
+
def set_last_upstream_packet_time(self) -> None:
|
|
472
|
+
"""
|
|
473
|
+
Updates the timestamp of the last packet sent to the upstream server.
|
|
474
|
+
|
|
475
|
+
This method is called internally when a packet is sent to the upstream
|
|
476
|
+
server.
|
|
477
|
+
"""
|
|
478
|
+
self._last_upstream_packet_time = datetime.now(tz=timezone.utc)
|
|
479
|
+
_LOGGER.debug(
|
|
480
|
+
'Last upstream packet time: %s', self._last_upstream_packet_time
|
|
481
|
+
)
|
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
Defines notification protocol for `NotificationBase` class.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from ..const import (
|
|
26
|
+
G90ArmDisarmTypes,
|
|
27
|
+
G90RemoteButtonStates,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class G90NotificationProtocol:
|
|
32
|
+
"""
|
|
33
|
+
Protocol for notification handling.
|
|
34
|
+
"""
|
|
35
|
+
async def on_armdisarm(self, state: G90ArmDisarmTypes) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Invoked when device is armed or disarmed.
|
|
38
|
+
|
|
39
|
+
:param state: State of the device
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
async def on_sensor_activity(self, idx: int, name: str) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Invoked on sensor activity.
|
|
45
|
+
|
|
46
|
+
:param idx: Index of the sensor.
|
|
47
|
+
:param name: Name of the sensor.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
async def on_door_open_when_arming(
|
|
51
|
+
self, event_id: int, zone_name: str
|
|
52
|
+
) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Invoked when door open is detected when panel is armed.
|
|
55
|
+
|
|
56
|
+
:param event_id: Index of the sensor.
|
|
57
|
+
:param zone_name: Name of the sensor that reports door open.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
async def on_door_open_close(
|
|
61
|
+
self, event_id: int, zone_name: str, is_open: bool
|
|
62
|
+
) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Invoked when door sensor reports it opened or closed.
|
|
65
|
+
|
|
66
|
+
:param event_id: Index of the sensor reporting the event.
|
|
67
|
+
:param zone_name: Name of the sensor that reports door open/close.
|
|
68
|
+
:param is_open: Indicates if the door is open.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
async def on_low_battery(self, event_id: int, zone_name: str) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Invoked when a sensor reports it is low on battery.
|
|
74
|
+
|
|
75
|
+
:param event_id: Index of the sensor.
|
|
76
|
+
:param zone_name: Name of the sensor that reports low battery.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
async def on_alarm(
|
|
80
|
+
self, event_id: int, zone_name: str, is_tampered: bool
|
|
81
|
+
) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Invoked when device triggers the alarm.
|
|
84
|
+
|
|
85
|
+
:param event_id: Index of the sensor.
|
|
86
|
+
:param zone_name: Name of the zone that triggered the alarm.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
async def on_remote_button_press(
|
|
90
|
+
self, event_id: int, zone_name: str, button: G90RemoteButtonStates
|
|
91
|
+
) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Invoked when a remote button is pressed.
|
|
94
|
+
|
|
95
|
+
Please note there will only be call to the method w/o invoking
|
|
96
|
+
:meth:`G90DeviceNotifications.on_sensor_activity`.
|
|
97
|
+
|
|
98
|
+
:param event_id: Index of the sensor associated with the remote.
|
|
99
|
+
:param zone_name: Name of the sensor that reports remote button press.
|
|
100
|
+
:param button: The button pressed on the remote
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
async def on_sos(
|
|
104
|
+
self, event_id: int, zone_name: str, is_host_sos: bool
|
|
105
|
+
) -> None:
|
|
106
|
+
"""
|
|
107
|
+
Invoked when SOS is triggered.
|
|
108
|
+
|
|
109
|
+
Please note that the panel might not set its status to alarm
|
|
110
|
+
internally, so that :meth:`G90DeviceNotifications` might need an
|
|
111
|
+
explicit call in the derived class to simulate that.
|
|
112
|
+
|
|
113
|
+
:param event_id: Index of the sensor.
|
|
114
|
+
:param zone_name: Name of the sensor that reports SOS.
|
|
115
|
+
:param is_host_sos: Indicates if the SOS is host-initiated.
|
|
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
|
+
"""
|
pyg90alarm/py.typed
ADDED
|
File without changes
|