pyg90alarm 1.19.0__py3-none-any.whl → 2.0.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 +5 -5
- pyg90alarm/alarm.py +159 -114
- pyg90alarm/cloud/__init__.py +31 -0
- pyg90alarm/cloud/const.py +56 -0
- pyg90alarm/cloud/messages.py +593 -0
- pyg90alarm/cloud/notifications.py +409 -0
- pyg90alarm/cloud/protocol.py +518 -0
- pyg90alarm/const.py +5 -0
- pyg90alarm/entities/base_entity.py +83 -0
- pyg90alarm/entities/base_list.py +165 -0
- pyg90alarm/entities/device_list.py +58 -0
- pyg90alarm/entities/sensor.py +63 -3
- pyg90alarm/entities/sensor_list.py +50 -0
- pyg90alarm/local/__init__.py +0 -0
- pyg90alarm/{base_cmd.py → local/base_cmd.py} +3 -6
- pyg90alarm/{discovery.py → local/discovery.py} +1 -1
- pyg90alarm/{history.py → local/history.py} +4 -2
- pyg90alarm/{host_status.py → local/host_status.py} +1 -1
- pyg90alarm/local/notifications.py +116 -0
- pyg90alarm/{paginated_cmd.py → local/paginated_cmd.py} +2 -2
- pyg90alarm/{paginated_result.py → local/paginated_result.py} +1 -1
- pyg90alarm/{targeted_discovery.py → local/targeted_discovery.py} +2 -2
- pyg90alarm/notifications/__init__.py +0 -0
- pyg90alarm/{device_notifications.py → notifications/base.py} +115 -173
- pyg90alarm/notifications/protocol.py +116 -0
- {pyg90alarm-1.19.0.dist-info → pyg90alarm-2.0.0.dist-info}/METADATA +112 -18
- pyg90alarm-2.0.0.dist-info/RECORD +40 -0
- {pyg90alarm-1.19.0.dist-info → pyg90alarm-2.0.0.dist-info}/WHEEL +1 -1
- pyg90alarm-1.19.0.dist-info/RECORD +0 -27
- /pyg90alarm/{config.py → local/config.py} +0 -0
- /pyg90alarm/{host_info.py → local/host_info.py} +0 -0
- /pyg90alarm/{user_data_crc.py → local/user_data_crc.py} +0 -0
- {pyg90alarm-1.19.0.dist-info → pyg90alarm-2.0.0.dist-info/licenses}/LICENSE +0 -0
- {pyg90alarm-1.19.0.dist-info → pyg90alarm-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,593 @@
|
|
|
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
|
+
Cloud message implementations for G90 alarm systems.
|
|
23
|
+
|
|
24
|
+
This module provides concrete message classes for cloud communication with G90
|
|
25
|
+
alarm systems, including ping messages, discovery messages, status change
|
|
26
|
+
notifications, and alarm notifications.
|
|
27
|
+
"""
|
|
28
|
+
from typing import List, Type, cast, ClassVar, Any, TypeVar
|
|
29
|
+
import logging
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from datetime import datetime, timezone
|
|
32
|
+
|
|
33
|
+
from pyg90alarm.cloud.protocol import G90CloudMessageContext
|
|
34
|
+
|
|
35
|
+
from .protocol import (
|
|
36
|
+
G90CloudMessage, G90CloudStatusChangeReqMessageBase, G90CloudHeader
|
|
37
|
+
)
|
|
38
|
+
from .const import G90CloudDirection, G90CloudCommand
|
|
39
|
+
from ..const import (
|
|
40
|
+
G90AlertStateChangeTypes, REMOTE_CLOUD_HOST, REMOTE_CLOUD_PORT,
|
|
41
|
+
G90AlertTypes, G90AlertSources, G90AlertStates,
|
|
42
|
+
)
|
|
43
|
+
from ..entities.sensor import G90SensorTypes
|
|
44
|
+
from ..notifications.base import G90DeviceAlert
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_LOGGER = logging.getLogger(__name__)
|
|
48
|
+
CLOUD_MESSAGE_CLASSES: List[Type[Any]] = []
|
|
49
|
+
CloudMessageT = TypeVar('CloudMessageT', bound='G90CloudMessage')
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def cloud_message(obj: Type[CloudMessageT]) -> Type[CloudMessageT]:
|
|
53
|
+
"""
|
|
54
|
+
Register a cloud message class.
|
|
55
|
+
|
|
56
|
+
This decorator registers the cloud message class in the global registry
|
|
57
|
+
and ensures there are no duplicate registrations for the same command/
|
|
58
|
+
source/destination combination.
|
|
59
|
+
|
|
60
|
+
:param obj: The cloud message class to register
|
|
61
|
+
:return: The registered cloud message class
|
|
62
|
+
:raises ValueError: If a class with the same command/source/destination is
|
|
63
|
+
already registered
|
|
64
|
+
"""
|
|
65
|
+
for cls in CLOUD_MESSAGE_CLASSES:
|
|
66
|
+
if cls.matches(obj):
|
|
67
|
+
# pylint:disable=protected-access
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"Duplicate command={obj._command}"
|
|
70
|
+
f"/source={obj._source}/destination={obj._destination}"
|
|
71
|
+
f" in {cls} and {obj}"
|
|
72
|
+
)
|
|
73
|
+
CLOUD_MESSAGE_CLASSES.append(obj)
|
|
74
|
+
return obj
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class G90CloudPingRespMessage(G90CloudMessage):
|
|
79
|
+
"""
|
|
80
|
+
Response message for ping requests.
|
|
81
|
+
|
|
82
|
+
A message sent in response to a ping request from the device.
|
|
83
|
+
"""
|
|
84
|
+
_format = ''
|
|
85
|
+
_command = G90CloudCommand.HELLO
|
|
86
|
+
_source = G90CloudDirection.DEVICE
|
|
87
|
+
_destination = G90CloudDirection.UNSPECIFIED
|
|
88
|
+
_header_kls = G90CloudHeader
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@cloud_message
|
|
92
|
+
@dataclass
|
|
93
|
+
class G90CloudPingReqMessage(G90CloudMessage):
|
|
94
|
+
"""
|
|
95
|
+
Ping request message sent by the device to the cloud server.
|
|
96
|
+
|
|
97
|
+
This message is sent every minute as a keepalive mechanism.
|
|
98
|
+
|
|
99
|
+
:attr _responses: The possible response message classes
|
|
100
|
+
"""
|
|
101
|
+
_format = ''
|
|
102
|
+
_command = G90CloudCommand.HELLO
|
|
103
|
+
_source = G90CloudDirection.DEVICE
|
|
104
|
+
_destination = G90CloudDirection.UNSPECIFIED
|
|
105
|
+
_responses = [G90CloudPingRespMessage]
|
|
106
|
+
_header_kls = G90CloudHeader
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class G90CloudHelloAckMessage(G90CloudMessage):
|
|
111
|
+
"""
|
|
112
|
+
Acknowledgement message sent by the cloud server in response to a hello
|
|
113
|
+
message.
|
|
114
|
+
|
|
115
|
+
This message confirms receipt of the hello request from the device.
|
|
116
|
+
"""
|
|
117
|
+
_format = '<B'
|
|
118
|
+
_command = G90CloudCommand.HELLO_ACK
|
|
119
|
+
_source = G90CloudDirection.CLOUD
|
|
120
|
+
_destination = G90CloudDirection.DEVICE
|
|
121
|
+
|
|
122
|
+
flag: int = 1
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class G90CloudHelloRespMessage(G90CloudMessage):
|
|
127
|
+
"""
|
|
128
|
+
Response message from the cloud server to a hello request from a device.
|
|
129
|
+
"""
|
|
130
|
+
_format = '<B'
|
|
131
|
+
_command = G90CloudCommand.HELLO
|
|
132
|
+
_source = G90CloudDirection.CLOUD
|
|
133
|
+
_destination = G90CloudDirection.DEVICE
|
|
134
|
+
|
|
135
|
+
flag: int = 0x1f
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@dataclass
|
|
139
|
+
class G90CloudHelloInfoRespMessage(G90CloudMessage):
|
|
140
|
+
"""
|
|
141
|
+
Information response message from the cloud server to a device's hello
|
|
142
|
+
request.
|
|
143
|
+
|
|
144
|
+
This message contains information about the port the device should use for
|
|
145
|
+
communication.
|
|
146
|
+
"""
|
|
147
|
+
_format = '<i'
|
|
148
|
+
_command = G90CloudCommand.HELLO_INFO
|
|
149
|
+
_source = G90CloudDirection.CLOUD
|
|
150
|
+
_destination = G90CloudDirection.DEVICE
|
|
151
|
+
|
|
152
|
+
# Actual port is set from context in `__post_init__()` method below
|
|
153
|
+
port: int = 0
|
|
154
|
+
|
|
155
|
+
def __post_init__(self, context: G90CloudMessageContext) -> None:
|
|
156
|
+
super().__post_init__(context)
|
|
157
|
+
self.port = context.local_port
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@cloud_message
|
|
161
|
+
@dataclass
|
|
162
|
+
# pylint:disable=too-many-instance-attributes
|
|
163
|
+
class G90CloudHelloReqMessage(G90CloudMessage):
|
|
164
|
+
"""
|
|
165
|
+
Hello request message sent by the device to the cloud server.
|
|
166
|
+
|
|
167
|
+
This message is sent every minute as part of the device's heartbeat
|
|
168
|
+
mechanism.
|
|
169
|
+
"""
|
|
170
|
+
_format = '<15sx4i3sx6i'
|
|
171
|
+
_command = G90CloudCommand.HELLO
|
|
172
|
+
_source = G90CloudDirection.DEVICE
|
|
173
|
+
_destination = G90CloudDirection.CLOUD
|
|
174
|
+
_responses = [
|
|
175
|
+
G90CloudHelloAckMessage, G90CloudHelloRespMessage,
|
|
176
|
+
G90CloudHelloInfoRespMessage
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
_guid: bytes
|
|
180
|
+
flag1: int # Typically is 1
|
|
181
|
+
flag2: int # Typically is 0
|
|
182
|
+
flag3: int # Typically is 2
|
|
183
|
+
flag4: int # Typically is 28672 (0x7000)
|
|
184
|
+
fw_ver: str
|
|
185
|
+
flag5: int # Typically is 0x2000NNNN
|
|
186
|
+
flag6: int # Typically is 48 (0x30)
|
|
187
|
+
flag7: int # Typically is 0
|
|
188
|
+
flag8: int # Typically is 7
|
|
189
|
+
flag9: int # Typically is 30 (0x1E)
|
|
190
|
+
flag10: int # Typically is 30 (0x1E)
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def guid(self) -> str:
|
|
194
|
+
"""
|
|
195
|
+
Get the device GUID as a string.
|
|
196
|
+
|
|
197
|
+
:return: The device's GUID decoded from bytes to string
|
|
198
|
+
"""
|
|
199
|
+
return self._guid.decode()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass
|
|
203
|
+
class G90CloudHelloDiscoveryRespMessage(G90CloudMessage):
|
|
204
|
+
"""
|
|
205
|
+
Discovery response message from the cloud to the device.
|
|
206
|
+
|
|
207
|
+
This message contains information about the cloud server's IP, port, and
|
|
208
|
+
timestamp.
|
|
209
|
+
"""
|
|
210
|
+
_format = '<16s4i'
|
|
211
|
+
_command = G90CloudCommand.HELLO
|
|
212
|
+
_source = G90CloudDirection.CLOUD_DISCOVERY
|
|
213
|
+
_destination = G90CloudDirection.DEVICE
|
|
214
|
+
|
|
215
|
+
# Simulated cloud response always contains known IP address of the vendor's
|
|
216
|
+
# cloud service - that is, all interactions between alarm panel and
|
|
217
|
+
# simulated cloud service will use same IP address for unification (i.e.
|
|
218
|
+
# traffic redicrection will always be used to divert panel's cloud traffic
|
|
219
|
+
# to the simulated cloud service)
|
|
220
|
+
ip_addr: bytes = REMOTE_CLOUD_HOST.encode()
|
|
221
|
+
flag2: int = 0
|
|
222
|
+
flag3: int = 0
|
|
223
|
+
port: int = REMOTE_CLOUD_PORT
|
|
224
|
+
_timestamp: int = 0 # unix timestamp
|
|
225
|
+
|
|
226
|
+
def __post_init__(self, context: G90CloudMessageContext) -> None:
|
|
227
|
+
super().__post_init__(context)
|
|
228
|
+
|
|
229
|
+
self._timestamp = int(datetime.now(timezone.utc).timestamp())
|
|
230
|
+
_LOGGER.debug(
|
|
231
|
+
"%s: Timestamp added: %s", type(self).__name__, str(self)
|
|
232
|
+
)
|
|
233
|
+
self.ip_addr = context.cloud_host.encode()
|
|
234
|
+
self.port = context.cloud_port
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def timestamp(self) -> datetime:
|
|
238
|
+
"""
|
|
239
|
+
Get the timestamp as a datetime object.
|
|
240
|
+
|
|
241
|
+
:return: The message timestamp converted to a datetime object with UTC
|
|
242
|
+
timezone
|
|
243
|
+
"""
|
|
244
|
+
return datetime.fromtimestamp(
|
|
245
|
+
self._timestamp, tz=timezone.utc
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def __str__(self) -> str:
|
|
249
|
+
return (
|
|
250
|
+
f"{type(self).__name__}"
|
|
251
|
+
f"({super().__str__()}"
|
|
252
|
+
f", timestamp={self.timestamp})"
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@cloud_message
|
|
257
|
+
@dataclass
|
|
258
|
+
# pylint:disable=too-many-instance-attributes
|
|
259
|
+
class G90CloudHelloDiscoveryReqMessage(G90CloudMessage):
|
|
260
|
+
"""
|
|
261
|
+
Hello discovery request message sent by the device.
|
|
262
|
+
|
|
263
|
+
This message is used during the device discovery process to locate the
|
|
264
|
+
cloud server.
|
|
265
|
+
"""
|
|
266
|
+
_format = '<15sx4i3sx3i'
|
|
267
|
+
_command = G90CloudCommand.HELLO
|
|
268
|
+
_source = G90CloudDirection.DEVICE_DISCOVERY
|
|
269
|
+
_destination = G90CloudDirection.CLOUD
|
|
270
|
+
_responses = [G90CloudHelloDiscoveryRespMessage]
|
|
271
|
+
|
|
272
|
+
_guid: bytes
|
|
273
|
+
flag1: int # Typically is 0
|
|
274
|
+
flag2: int # Typically is 0
|
|
275
|
+
flag3: int # Typically is 1
|
|
276
|
+
flag4: int # Typically is 28672 (0x7000)
|
|
277
|
+
fw_ver: str
|
|
278
|
+
flag5: int # Typically is 0x05050505
|
|
279
|
+
flag6: int # Typically is 0x06060030
|
|
280
|
+
flag7: int # Typically is 0x07070707
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def guid(self) -> str:
|
|
284
|
+
"""
|
|
285
|
+
Get the device GUID as a string.
|
|
286
|
+
|
|
287
|
+
:return: The device's GUID decoded from bytes to string
|
|
288
|
+
"""
|
|
289
|
+
return self._guid.decode()
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@cloud_message
|
|
293
|
+
@dataclass
|
|
294
|
+
# pylint:disable=too-many-instance-attributes
|
|
295
|
+
class G90CloudStatusChangeReqMessage(G90CloudStatusChangeReqMessageBase):
|
|
296
|
+
"""
|
|
297
|
+
Status change request message from the device to the cloud.
|
|
298
|
+
|
|
299
|
+
This message is sent when the device's status changes, such as arming or
|
|
300
|
+
disarming.
|
|
301
|
+
"""
|
|
302
|
+
# 68x are typically zeros, while 34x is some garbage from previous
|
|
303
|
+
# notification message (0x22) with its head overwritten with old/new status
|
|
304
|
+
# values
|
|
305
|
+
_format = '<2B34xi68x'
|
|
306
|
+
_command = G90CloudCommand.STATUS_CHANGE
|
|
307
|
+
_source = G90CloudDirection.DEVICE
|
|
308
|
+
_destination = G90CloudDirection.CLOUD
|
|
309
|
+
_type = G90AlertTypes.STATE_CHANGE
|
|
310
|
+
|
|
311
|
+
type: int
|
|
312
|
+
_state: G90AlertStateChangeTypes
|
|
313
|
+
_timestamp: int # Unix timestamp
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def state(self) -> G90AlertStateChangeTypes:
|
|
317
|
+
"""
|
|
318
|
+
Get the state change type.
|
|
319
|
+
|
|
320
|
+
:return: The alert state change type
|
|
321
|
+
"""
|
|
322
|
+
return G90AlertStateChangeTypes(self._state)
|
|
323
|
+
|
|
324
|
+
@property
|
|
325
|
+
def as_device_alert(self) -> G90DeviceAlert:
|
|
326
|
+
"""
|
|
327
|
+
Convert the message to a device alert object.
|
|
328
|
+
|
|
329
|
+
:return: A G90DeviceAlert object constructed from the message
|
|
330
|
+
properties
|
|
331
|
+
"""
|
|
332
|
+
return G90DeviceAlert(
|
|
333
|
+
device_id=self._context.device_id or '',
|
|
334
|
+
state=self.state,
|
|
335
|
+
event_id=self.state,
|
|
336
|
+
zone_name='',
|
|
337
|
+
type=self._type,
|
|
338
|
+
source=G90AlertSources.DEVICE,
|
|
339
|
+
unix_time=self._timestamp,
|
|
340
|
+
resv4=0,
|
|
341
|
+
other='',
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
def __str__(self) -> str:
|
|
345
|
+
return (
|
|
346
|
+
f"{type(self).__name__}"
|
|
347
|
+
f"({super().__str__()}"
|
|
348
|
+
f", type={self.type}"
|
|
349
|
+
f", state={repr(self.state)}"
|
|
350
|
+
f", timestamp={self.timestamp})"
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@cloud_message
|
|
355
|
+
@dataclass
|
|
356
|
+
# pylint:disable=too-many-instance-attributes
|
|
357
|
+
class G90CloudStatusChangeSensorReqMessage(G90CloudStatusChangeReqMessageBase):
|
|
358
|
+
"""
|
|
359
|
+
Status change sensor request message from the device to the cloud.
|
|
360
|
+
|
|
361
|
+
This message is sent when a sensor's status changes, such as when motion is
|
|
362
|
+
detected or a door/window is opened.
|
|
363
|
+
"""
|
|
364
|
+
_format = '<4B32si68x'
|
|
365
|
+
_command = G90CloudCommand.STATUS_CHANGE
|
|
366
|
+
_source = G90CloudDirection.DEVICE
|
|
367
|
+
_destination = G90CloudDirection.CLOUD
|
|
368
|
+
_type: ClassVar[G90AlertTypes] = G90AlertTypes.SENSOR_ACTIVITY
|
|
369
|
+
|
|
370
|
+
type: int
|
|
371
|
+
sensor_id: int
|
|
372
|
+
_sensor_type: G90SensorTypes
|
|
373
|
+
_sensor_state: int
|
|
374
|
+
_sensor: bytes
|
|
375
|
+
_timestamp: int # Unix timestamp
|
|
376
|
+
|
|
377
|
+
@property
|
|
378
|
+
def sensor_type(self) -> G90SensorTypes:
|
|
379
|
+
"""
|
|
380
|
+
Get the sensor type.
|
|
381
|
+
|
|
382
|
+
:return: The type of the sensor that triggered the event
|
|
383
|
+
"""
|
|
384
|
+
return G90SensorTypes(self._sensor_type)
|
|
385
|
+
|
|
386
|
+
@property
|
|
387
|
+
def sensor_state(self) -> G90AlertStates:
|
|
388
|
+
"""
|
|
389
|
+
Get the sensor state.
|
|
390
|
+
|
|
391
|
+
:return: The state of the sensor that triggered the event
|
|
392
|
+
"""
|
|
393
|
+
return G90AlertStates(self._sensor_state)
|
|
394
|
+
|
|
395
|
+
@property
|
|
396
|
+
def sensor(self) -> str:
|
|
397
|
+
"""
|
|
398
|
+
Get the sensor name as a string.
|
|
399
|
+
|
|
400
|
+
:return: The sensor name decoded from bytes with null characters
|
|
401
|
+
removed
|
|
402
|
+
"""
|
|
403
|
+
return self._sensor.decode().rstrip('\x00')
|
|
404
|
+
|
|
405
|
+
@property
|
|
406
|
+
def as_device_alert(self) -> G90DeviceAlert:
|
|
407
|
+
"""
|
|
408
|
+
Convert the message to a device alert object.
|
|
409
|
+
|
|
410
|
+
:return: A G90DeviceAlert object constructed from the message
|
|
411
|
+
properties
|
|
412
|
+
"""
|
|
413
|
+
return G90DeviceAlert(
|
|
414
|
+
device_id=self._context.device_id or '',
|
|
415
|
+
state=self.sensor_state,
|
|
416
|
+
event_id=cast(G90AlertStateChangeTypes, self.sensor_id),
|
|
417
|
+
zone_name=self.sensor,
|
|
418
|
+
type=self._type,
|
|
419
|
+
source=G90AlertSources.SENSOR,
|
|
420
|
+
unix_time=self._timestamp,
|
|
421
|
+
resv4=0,
|
|
422
|
+
other='',
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
def __str__(self) -> str:
|
|
426
|
+
return (
|
|
427
|
+
f"{type(self).__name__}"
|
|
428
|
+
f"({super().__str__()}"
|
|
429
|
+
f", type={self.type}"
|
|
430
|
+
f", sensor={repr(self.sensor)}"
|
|
431
|
+
f", sensor id ={self.sensor_id}"
|
|
432
|
+
f", sensor type={repr(self.sensor_type)}"
|
|
433
|
+
f", sensor state={repr(self.sensor_state)}"
|
|
434
|
+
f", timestamp={self.timestamp})"
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@cloud_message
|
|
439
|
+
@dataclass
|
|
440
|
+
# pylint:disable=too-many-instance-attributes
|
|
441
|
+
class G90CloudStatusChangeAlarmReqMessage(G90CloudStatusChangeReqMessageBase):
|
|
442
|
+
"""
|
|
443
|
+
Status change alarm request message from the device to the cloud.
|
|
444
|
+
|
|
445
|
+
This message is sent when an alarm is triggered on the device, such as when
|
|
446
|
+
an intrusion is detected or when the panic button is pressed.
|
|
447
|
+
"""
|
|
448
|
+
_format = '<4B32si68x'
|
|
449
|
+
_command = G90CloudCommand.STATUS_CHANGE
|
|
450
|
+
_source = G90CloudDirection.DEVICE
|
|
451
|
+
_destination = G90CloudDirection.CLOUD
|
|
452
|
+
_type: ClassVar[G90AlertTypes] = G90AlertTypes.ALARM
|
|
453
|
+
|
|
454
|
+
type: int
|
|
455
|
+
sensor_id: int
|
|
456
|
+
_sensor_type: G90SensorTypes
|
|
457
|
+
_sensor_state: int
|
|
458
|
+
_sensor: bytes
|
|
459
|
+
_timestamp: int # Unix timestamp
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
def sensor_state(self) -> G90AlertStates:
|
|
463
|
+
"""
|
|
464
|
+
Get the sensor state for the alarm event.
|
|
465
|
+
|
|
466
|
+
:return: The state of the sensor that triggered the alarm
|
|
467
|
+
"""
|
|
468
|
+
return G90AlertStates(self._sensor_state)
|
|
469
|
+
|
|
470
|
+
@property
|
|
471
|
+
def sensor(self) -> str:
|
|
472
|
+
"""
|
|
473
|
+
Get the sensor name as a string.
|
|
474
|
+
|
|
475
|
+
:return: The sensor name decoded from bytes with null characters
|
|
476
|
+
removed
|
|
477
|
+
"""
|
|
478
|
+
return self._sensor.decode().rstrip('\x00')
|
|
479
|
+
|
|
480
|
+
@property
|
|
481
|
+
def sensor_type(self) -> G90SensorTypes:
|
|
482
|
+
"""
|
|
483
|
+
Get the sensor type for the alarm event.
|
|
484
|
+
|
|
485
|
+
:return: The type of sensor that triggered the alarm
|
|
486
|
+
"""
|
|
487
|
+
return G90SensorTypes(self._sensor_type)
|
|
488
|
+
|
|
489
|
+
@property
|
|
490
|
+
def as_device_alert(self) -> G90DeviceAlert:
|
|
491
|
+
"""
|
|
492
|
+
Convert the message to a device alert object.
|
|
493
|
+
|
|
494
|
+
:return: A G90DeviceAlert object constructed from the message
|
|
495
|
+
properties
|
|
496
|
+
"""
|
|
497
|
+
return G90DeviceAlert(
|
|
498
|
+
device_id=self._context.device_id or '',
|
|
499
|
+
state=self.sensor_state,
|
|
500
|
+
event_id=cast(G90AlertStateChangeTypes, self.sensor_id),
|
|
501
|
+
zone_name=self.sensor,
|
|
502
|
+
type=self._type,
|
|
503
|
+
source=G90AlertSources.DEVICE,
|
|
504
|
+
unix_time=self._timestamp,
|
|
505
|
+
resv4=0,
|
|
506
|
+
other='',
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
def __str__(self) -> str:
|
|
510
|
+
return (
|
|
511
|
+
f"{type(self).__name__}"
|
|
512
|
+
f"({super().__str__()}"
|
|
513
|
+
f", type={self.type}"
|
|
514
|
+
f", _type={self._type}"
|
|
515
|
+
f", sensor={repr(self.sensor)}"
|
|
516
|
+
f", sensor id ={self.sensor_id}"
|
|
517
|
+
f", sensor type={repr(self.sensor_type)}"
|
|
518
|
+
f", sensor state={repr(self.sensor_state)}"
|
|
519
|
+
f", timestamp={self.timestamp})"
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
@cloud_message
|
|
524
|
+
@dataclass
|
|
525
|
+
class G90CloudNotificationMessage(G90CloudMessage):
|
|
526
|
+
"""
|
|
527
|
+
Notification message from the device to the cloud server.
|
|
528
|
+
|
|
529
|
+
This message carries notification data from the device that may include
|
|
530
|
+
sensor data or other information.
|
|
531
|
+
"""
|
|
532
|
+
_format = ''
|
|
533
|
+
_command = G90CloudCommand.NOTIFICATION
|
|
534
|
+
_source = G90CloudDirection.DEVICE
|
|
535
|
+
_destination = G90CloudDirection.CLOUD
|
|
536
|
+
|
|
537
|
+
@property
|
|
538
|
+
def as_notification_message(self) -> bytes:
|
|
539
|
+
"""
|
|
540
|
+
Extract the notification message payload.
|
|
541
|
+
|
|
542
|
+
:return: The raw notification message bytes extracted from the header
|
|
543
|
+
payload
|
|
544
|
+
"""
|
|
545
|
+
return self.header.payload[self.size():]
|
|
546
|
+
|
|
547
|
+
def __str__(self) -> str:
|
|
548
|
+
return (
|
|
549
|
+
f"{type(self).__name__}"
|
|
550
|
+
f"({super().__str__()}"
|
|
551
|
+
f", notification_message={self.as_notification_message.decode()})"
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
@cloud_message
|
|
556
|
+
@dataclass
|
|
557
|
+
class G90CloudCmdRespMessage(G90CloudMessage):
|
|
558
|
+
"""
|
|
559
|
+
Command response message sent by the device.
|
|
560
|
+
|
|
561
|
+
This message contains command and sequence information.
|
|
562
|
+
"""
|
|
563
|
+
_format = '<HiHi2H'
|
|
564
|
+
_command = G90CloudCommand.HELLO
|
|
565
|
+
_source = G90CloudDirection.UNSPECIFIED
|
|
566
|
+
_destination = G90CloudDirection.CLOUD
|
|
567
|
+
_header_kls = G90CloudHeader
|
|
568
|
+
|
|
569
|
+
flag1: int
|
|
570
|
+
seq_num1: int
|
|
571
|
+
flag3: int
|
|
572
|
+
seq_num2: int
|
|
573
|
+
cmd: int
|
|
574
|
+
subcmd: int
|
|
575
|
+
|
|
576
|
+
@property
|
|
577
|
+
def body(self) -> bytes:
|
|
578
|
+
"""
|
|
579
|
+
Extract the response body payload.
|
|
580
|
+
|
|
581
|
+
:return: The raw response body bytes extracted from the header payload
|
|
582
|
+
"""
|
|
583
|
+
return self.header.payload[self.size():]
|
|
584
|
+
|
|
585
|
+
def __str__(self) -> str:
|
|
586
|
+
return (
|
|
587
|
+
f"{type(self).__name__}"
|
|
588
|
+
f"({super().__str__()}"
|
|
589
|
+
f", cmd={self.cmd}"
|
|
590
|
+
f", subcmd={self.subcmd}"
|
|
591
|
+
f", seq_num={self.seq_num1}"
|
|
592
|
+
f", body={self.body.decode()})"
|
|
593
|
+
)
|