pyg90alarm 1.15.1__tar.gz → 1.16.1__tar.gz
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-1.15.1 → pyg90alarm-1.16.1}/.github/workflows/main.yml +3 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/PKG-INFO +2 -1
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/setup.py +1 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/alarm.py +14 -8
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/base_cmd.py +6 -1
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/const.py +9 -1
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/device_notifications.py +61 -10
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/history.py +66 -34
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/targeted_discovery.py +6 -1
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm.egg-info/PKG-INFO +2 -1
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/tests/test_alarm.py +43 -16
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/tests/test_base_commands.py +20 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/tests/test_discovery.py +25 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/tests/test_notifications.py +127 -16
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/tox.ini +1 -1
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/.github/CODEOWNERS +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/.gitignore +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/.pylintrc +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/.readthedocs.yaml +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/LICENSE +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/MANIFEST.in +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/README.rst +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/docs/.DS_Store +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/docs/.gitignore +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/docs/api-docs.rst +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/docs/conf.py +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/docs/index.rst +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/docs/protocol.rst +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/docs/requirements.txt +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/pyproject.toml +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/setup.cfg +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/sonar-project.properties +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/__init__.py +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/callback.py +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/config.py +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/definitions/__init__.py +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/definitions/sensors.py +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/discovery.py +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/entities/__init__.py +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/entities/device.py +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/entities/sensor.py +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/exceptions.py +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/host_info.py +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/host_status.py +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/paginated_cmd.py +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/paginated_result.py +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/py.typed +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm/user_data_crc.py +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm.egg-info/SOURCES.txt +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm.egg-info/dependency_links.txt +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm.egg-info/requires.txt +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/src/pyg90alarm.egg-info/top_level.txt +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/tests/__init__.py +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/tests/conftest.py +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/tests/device_mock.py +0 -0
- {pyg90alarm-1.15.1 → pyg90alarm-1.16.1}/tests/test_paginated_commands.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyg90alarm
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.16.1
|
|
4
4
|
Summary: G90 Alarm system protocol
|
|
5
5
|
Home-page: https://github.com/hostcc/pyg90alarm
|
|
6
6
|
Author: Ilia Sotnikov
|
|
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.10
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
24
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
24
25
|
Requires-Python: >=3.8, <4
|
|
25
26
|
Description-Content-Type: text/x-rst
|
|
@@ -60,7 +60,8 @@ from .const import (
|
|
|
60
60
|
G90Commands, REMOTE_PORT,
|
|
61
61
|
REMOTE_TARGETED_DISCOVERY_PORT,
|
|
62
62
|
LOCAL_TARGETED_DISCOVERY_PORT,
|
|
63
|
-
|
|
63
|
+
LOCAL_NOTIFICATIONS_HOST,
|
|
64
|
+
LOCAL_NOTIFICATIONS_PORT,
|
|
64
65
|
G90ArmDisarmTypes,
|
|
65
66
|
)
|
|
66
67
|
from .base_cmd import (G90BaseCommand, G90BaseCommandData)
|
|
@@ -140,11 +141,14 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
140
141
|
# pylint: disable=too-many-instance-attributes,too-many-arguments
|
|
141
142
|
def __init__(self, host: str, port: int = REMOTE_PORT,
|
|
142
143
|
reset_occupancy_interval: float = 3.0,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
super().__init__(
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
notifications_local_host: str = LOCAL_NOTIFICATIONS_HOST,
|
|
145
|
+
notifications_local_port: int = LOCAL_NOTIFICATIONS_PORT):
|
|
146
|
+
super().__init__(
|
|
147
|
+
local_host=notifications_local_host,
|
|
148
|
+
local_port=notifications_local_port
|
|
149
|
+
)
|
|
150
|
+
self._host: str = host
|
|
151
|
+
self._port: int = port
|
|
148
152
|
self._sensors: List[G90Sensor] = []
|
|
149
153
|
self._devices: List[G90Device] = []
|
|
150
154
|
self._notifications: Optional[G90DeviceNotifications] = None
|
|
@@ -336,7 +340,9 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
336
340
|
:return: Device information
|
|
337
341
|
"""
|
|
338
342
|
res = await self.command(G90Commands.GETHOSTINFO)
|
|
339
|
-
|
|
343
|
+
info = G90HostInfo(*res)
|
|
344
|
+
self.device_id = info.host_guid
|
|
345
|
+
return info
|
|
340
346
|
|
|
341
347
|
@property
|
|
342
348
|
async def host_status(self) -> G90HostStatus:
|
|
@@ -813,7 +819,7 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
813
819
|
# code as alert, as if it came from the device and its
|
|
814
820
|
# notifications port
|
|
815
821
|
self._handle_alert(
|
|
816
|
-
(self._host, self.
|
|
822
|
+
(self._host, self._notifications_local_port),
|
|
817
823
|
item.as_device_alert()
|
|
818
824
|
)
|
|
819
825
|
|
|
@@ -154,7 +154,12 @@ class G90BaseCommand(DatagramProtocol):
|
|
|
154
154
|
Parses the response from the alarm panel.
|
|
155
155
|
"""
|
|
156
156
|
_LOGGER.debug('To be decoded from wire format %s', data)
|
|
157
|
-
|
|
157
|
+
try:
|
|
158
|
+
self._parse(data.decode('utf-8'))
|
|
159
|
+
except UnicodeDecodeError as exc:
|
|
160
|
+
raise G90Error(
|
|
161
|
+
'Unable to decode response from UTF-8'
|
|
162
|
+
) from exc
|
|
158
163
|
return self._resp.data or []
|
|
159
164
|
|
|
160
165
|
def _parse(self, data: str) -> None:
|
|
@@ -28,7 +28,8 @@ from typing import Optional
|
|
|
28
28
|
REMOTE_PORT = 12368
|
|
29
29
|
REMOTE_TARGETED_DISCOVERY_PORT = 12900
|
|
30
30
|
LOCAL_TARGETED_DISCOVERY_PORT = 12901
|
|
31
|
-
|
|
31
|
+
LOCAL_NOTIFICATIONS_HOST = '0.0.0.0'
|
|
32
|
+
LOCAL_NOTIFICATIONS_PORT = 12901
|
|
32
33
|
|
|
33
34
|
CMD_PAGE_SIZE = 10
|
|
34
35
|
|
|
@@ -165,7 +166,10 @@ class G90NotificationTypes(IntEnum):
|
|
|
165
166
|
Defines types of notifications sent by the alarm panel.
|
|
166
167
|
"""
|
|
167
168
|
ARM_DISARM = 1
|
|
169
|
+
SENSOR_ADDED = 4
|
|
168
170
|
SENSOR_ACTIVITY = 5
|
|
171
|
+
DOOR_OPEN_WHEN_ARMING = 6
|
|
172
|
+
FIRMWARE_UPDATING = 8
|
|
169
173
|
|
|
170
174
|
|
|
171
175
|
class G90ArmDisarmTypes(IntEnum):
|
|
@@ -194,7 +198,10 @@ class G90AlertSources(IntEnum):
|
|
|
194
198
|
"""
|
|
195
199
|
DEVICE = 0
|
|
196
200
|
SENSOR = 1
|
|
201
|
+
REMOTE = 10
|
|
202
|
+
RFID = 11
|
|
197
203
|
DOORBELL = 12
|
|
204
|
+
FINGERPRINT = 15
|
|
198
205
|
|
|
199
206
|
|
|
200
207
|
class G90AlertStates(IntEnum):
|
|
@@ -203,6 +210,7 @@ class G90AlertStates(IntEnum):
|
|
|
203
210
|
"""
|
|
204
211
|
DOOR_CLOSE = 0
|
|
205
212
|
DOOR_OPEN = 1
|
|
213
|
+
SOS = 2
|
|
206
214
|
TAMPER = 3
|
|
207
215
|
LOW_BATTERY = 4
|
|
208
216
|
|
|
@@ -41,7 +41,6 @@ from .const import (
|
|
|
41
41
|
G90AlertStates,
|
|
42
42
|
)
|
|
43
43
|
|
|
44
|
-
|
|
45
44
|
_LOGGER = logging.getLogger(__name__)
|
|
46
45
|
|
|
47
46
|
|
|
@@ -107,12 +106,29 @@ class G90DeviceAlert: # pylint: disable=too-many-instance-attributes
|
|
|
107
106
|
class G90DeviceNotifications(DatagramProtocol):
|
|
108
107
|
"""
|
|
109
108
|
Implements support for notifications/alerts sent by alarm panel.
|
|
109
|
+
|
|
110
|
+
There is a basic check to ensure only notifications/alerts from the correct
|
|
111
|
+
device are processed - the check uses the host and port of the device, and
|
|
112
|
+
the device ID (GUID) that is set by the ancestor class that implements the
|
|
113
|
+
commands (e.g. :class:`G90Alarm`). The latter to work correctly needs a
|
|
114
|
+
command to be performed first, one that fetches device GUID and then stores
|
|
115
|
+
it using :attr:`.device_id` (e.g. :meth:`G90Alarm.get_host_info`).
|
|
110
116
|
"""
|
|
111
|
-
def __init__(self,
|
|
117
|
+
def __init__(self, local_port: int, local_host: str):
|
|
112
118
|
# pylint: disable=too-many-arguments
|
|
113
119
|
self._notification_transport: Optional[BaseTransport] = None
|
|
114
|
-
self.
|
|
115
|
-
self.
|
|
120
|
+
self._notifications_local_host = local_host
|
|
121
|
+
self._notifications_local_port = local_port
|
|
122
|
+
# Host/port of the device is configured to communicating via commands.
|
|
123
|
+
# Inteded to validate if notifications/alert are received from the
|
|
124
|
+
# correct device.
|
|
125
|
+
self._host: Optional[str] = None
|
|
126
|
+
self._port: Optional[int] = None
|
|
127
|
+
# Same but for device ID (GUID) - the notifications logic uses it to
|
|
128
|
+
# perform validation, but doesn't set it from messages received (it
|
|
129
|
+
# will diminish the purpose of the validation, should be done by an
|
|
130
|
+
# ancestor class).
|
|
131
|
+
self._device_id: Optional[str] = None
|
|
116
132
|
|
|
117
133
|
def _handle_notification(
|
|
118
134
|
self, addr: Tuple[str, int], notification: G90Notification
|
|
@@ -145,6 +161,15 @@ class G90DeviceNotifications(DatagramProtocol):
|
|
|
145
161
|
def _handle_alert(
|
|
146
162
|
self, addr: Tuple[str, int], alert: G90DeviceAlert
|
|
147
163
|
) -> None:
|
|
164
|
+
# Stop processing when alert is received from the device with different
|
|
165
|
+
# GUID
|
|
166
|
+
if self.device_id and alert.device_id != self.device_id:
|
|
167
|
+
_LOGGER.error(
|
|
168
|
+
"Received alert from wrong device: expected '%s', got '%s'",
|
|
169
|
+
self.device_id, alert.device_id
|
|
170
|
+
)
|
|
171
|
+
return
|
|
172
|
+
|
|
148
173
|
if alert.type == G90AlertTypes.DOOR_OPEN_CLOSE:
|
|
149
174
|
if alert.state in (
|
|
150
175
|
G90AlertStates.DOOR_OPEN, G90AlertStates.DOOR_CLOSE
|
|
@@ -219,11 +244,23 @@ class G90DeviceNotifications(DatagramProtocol):
|
|
|
219
244
|
def datagram_received( # pylint:disable=R0911
|
|
220
245
|
self, data: bytes, addr: Tuple[str, int]
|
|
221
246
|
) -> None:
|
|
222
|
-
|
|
223
247
|
"""
|
|
224
|
-
Invoked
|
|
248
|
+
Invoked when datagram is received from the device.
|
|
225
249
|
"""
|
|
226
|
-
|
|
250
|
+
if self._host and self._host != addr[0]:
|
|
251
|
+
_LOGGER.error(
|
|
252
|
+
"Received notification/alert from wrong host '%s',"
|
|
253
|
+
" expected from '%s'",
|
|
254
|
+
addr[0], self._host
|
|
255
|
+
)
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
s_data = data.decode('utf-8')
|
|
260
|
+
except UnicodeDecodeError:
|
|
261
|
+
_LOGGER.error('Unable to decode device message from UTF-8')
|
|
262
|
+
return
|
|
263
|
+
|
|
227
264
|
if not s_data.endswith('\0'):
|
|
228
265
|
_LOGGER.error('Missing end marker in data')
|
|
229
266
|
return
|
|
@@ -304,13 +341,13 @@ class G90DeviceNotifications(DatagramProtocol):
|
|
|
304
341
|
loop = asyncio.get_event_loop()
|
|
305
342
|
|
|
306
343
|
_LOGGER.debug('Creating UDP endpoint for %s:%s',
|
|
307
|
-
self.
|
|
308
|
-
self.
|
|
344
|
+
self._notifications_local_host,
|
|
345
|
+
self._notifications_local_port)
|
|
309
346
|
(self._notification_transport,
|
|
310
347
|
_protocol) = await loop.create_datagram_endpoint(
|
|
311
348
|
lambda: self,
|
|
312
349
|
local_addr=(
|
|
313
|
-
self.
|
|
350
|
+
self._notifications_local_host, self._notifications_local_port
|
|
314
351
|
))
|
|
315
352
|
|
|
316
353
|
@property
|
|
@@ -328,3 +365,17 @@ class G90DeviceNotifications(DatagramProtocol):
|
|
|
328
365
|
_LOGGER.debug('No longer listening for device notifications')
|
|
329
366
|
self._notification_transport.close()
|
|
330
367
|
self._notification_transport = None
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def device_id(self) -> Optional[str]:
|
|
371
|
+
"""
|
|
372
|
+
The ID (GUID) of the panel being communicated with thru commands.
|
|
373
|
+
|
|
374
|
+
Available when any panel command receives it from the device
|
|
375
|
+
(:meth:`G90Alarm.get_host_info` currently).
|
|
376
|
+
"""
|
|
377
|
+
return self._device_id
|
|
378
|
+
|
|
379
|
+
@device_id.setter
|
|
380
|
+
def device_id(self, device_id: str) -> None:
|
|
381
|
+
self._device_id = device_id
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"""
|
|
22
22
|
History protocol entity.
|
|
23
23
|
"""
|
|
24
|
+
import logging
|
|
25
|
+
|
|
24
26
|
from typing import Any, Optional, Dict
|
|
25
27
|
from dataclasses import dataclass
|
|
26
28
|
from datetime import datetime, timezone
|
|
@@ -33,12 +35,13 @@ from .const import (
|
|
|
33
35
|
)
|
|
34
36
|
from .device_notifications import G90DeviceAlert
|
|
35
37
|
|
|
38
|
+
_LOGGER = logging.getLogger(__name__)
|
|
39
|
+
|
|
36
40
|
|
|
37
41
|
# The state of the incoming history entries are mixed of `G90AlertStates` and
|
|
38
|
-
# `G90AlertStateChangeTypes`, depending on entry type -
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
states_mapping = {
|
|
42
|
+
# `G90AlertStateChangeTypes`, depending on entry type - hence two separate
|
|
43
|
+
# dictionaries, since enums used for keys have conflicting values
|
|
44
|
+
states_mapping_alerts = {
|
|
42
45
|
G90AlertStates.DOOR_CLOSE:
|
|
43
46
|
G90HistoryStates.DOOR_CLOSE,
|
|
44
47
|
G90AlertStates.DOOR_OPEN:
|
|
@@ -47,6 +50,9 @@ states_mapping = {
|
|
|
47
50
|
G90HistoryStates.TAMPER,
|
|
48
51
|
G90AlertStates.LOW_BATTERY:
|
|
49
52
|
G90HistoryStates.LOW_BATTERY,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
states_mapping_state_changes = {
|
|
50
56
|
G90AlertStateChangeTypes.AC_POWER_FAILURE:
|
|
51
57
|
G90HistoryStates.AC_POWER_FAILURE,
|
|
52
58
|
G90AlertStateChangeTypes.AC_POWER_RECOVER:
|
|
@@ -87,6 +93,7 @@ class G90History:
|
|
|
87
93
|
Represents a history entry from the alarm panel.
|
|
88
94
|
"""
|
|
89
95
|
def __init__(self, *args: Any, **kwargs: Any):
|
|
96
|
+
self._raw_data = args
|
|
90
97
|
self._protocol_data = ProtocolData(*args, **kwargs)
|
|
91
98
|
|
|
92
99
|
@property
|
|
@@ -99,55 +106,79 @@ class G90History:
|
|
|
99
106
|
)
|
|
100
107
|
|
|
101
108
|
@property
|
|
102
|
-
def type(self) -> G90AlertTypes:
|
|
109
|
+
def type(self) -> Optional[G90AlertTypes]:
|
|
103
110
|
"""
|
|
104
111
|
Type of the history entry.
|
|
105
112
|
"""
|
|
106
|
-
|
|
113
|
+
try:
|
|
114
|
+
return G90AlertTypes(self._protocol_data.type)
|
|
115
|
+
except ValueError:
|
|
116
|
+
_LOGGER.warning(
|
|
117
|
+
"Can't interpret '%s' as alert type (decoded protocol"
|
|
118
|
+
" data '%s', raw data '%s')",
|
|
119
|
+
self._protocol_data.type, self._protocol_data, self._raw_data
|
|
120
|
+
)
|
|
121
|
+
return None
|
|
107
122
|
|
|
108
123
|
@property
|
|
109
|
-
def state(self) -> G90HistoryStates:
|
|
124
|
+
def state(self) -> Optional[G90HistoryStates]:
|
|
110
125
|
"""
|
|
111
126
|
State for the history entry.
|
|
112
127
|
"""
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
128
|
+
try:
|
|
129
|
+
# Door open/close or alert types, mapped against `G90AlertStates`
|
|
130
|
+
# using `state` incoming field
|
|
131
|
+
if self.type in [
|
|
132
|
+
G90AlertTypes.DOOR_OPEN_CLOSE, G90AlertTypes.ALARM
|
|
133
|
+
]:
|
|
134
|
+
return G90HistoryStates(
|
|
135
|
+
states_mapping_alerts[
|
|
136
|
+
G90AlertStates(self._protocol_data.state)
|
|
137
|
+
]
|
|
138
|
+
)
|
|
139
|
+
except ValueError:
|
|
140
|
+
_LOGGER.warning(
|
|
141
|
+
"Can't interpret '%s' as alert state (decoded protocol"
|
|
142
|
+
" data '%s', raw data '%s')",
|
|
143
|
+
self._protocol_data.state, self._protocol_data, self._raw_data
|
|
118
144
|
)
|
|
145
|
+
return None
|
|
119
146
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if self.type == G90AlertTypes.STATE_CHANGE:
|
|
147
|
+
try:
|
|
148
|
+
# Other types are mapped against `G90AlertStateChangeTypes`
|
|
123
149
|
return G90HistoryStates(
|
|
124
|
-
|
|
150
|
+
states_mapping_state_changes[
|
|
125
151
|
G90AlertStateChangeTypes(self._protocol_data.event_id)
|
|
126
152
|
]
|
|
127
153
|
)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
G90AlertStateChangeTypes(self._protocol_data.event_id)
|
|
137
|
-
]
|
|
138
|
-
)
|
|
154
|
+
except ValueError:
|
|
155
|
+
_LOGGER.warning(
|
|
156
|
+
"Can't interpret '%s' as state change (decoded protocol"
|
|
157
|
+
" data '%s', raw data '%s')",
|
|
158
|
+
self._protocol_data.event_id, self._protocol_data,
|
|
159
|
+
self._raw_data
|
|
160
|
+
)
|
|
161
|
+
return None
|
|
139
162
|
|
|
140
163
|
@property
|
|
141
|
-
def source(self) -> G90AlertSources:
|
|
164
|
+
def source(self) -> Optional[G90AlertSources]:
|
|
142
165
|
"""
|
|
143
166
|
Source of the history entry.
|
|
144
167
|
"""
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
168
|
+
try:
|
|
169
|
+
# Device state changes or open/close events are mapped against
|
|
170
|
+
# `G90AlertSources` using `source` incoming field
|
|
171
|
+
if self.type in [
|
|
172
|
+
G90AlertTypes.STATE_CHANGE, G90AlertTypes.DOOR_OPEN_CLOSE
|
|
173
|
+
]:
|
|
174
|
+
return G90AlertSources(self._protocol_data.source)
|
|
175
|
+
except ValueError:
|
|
176
|
+
_LOGGER.warning(
|
|
177
|
+
"Can't interpret '%s' as alert source (decoded protocol"
|
|
178
|
+
" data '%s', raw data '%s')",
|
|
179
|
+
self._protocol_data.source, self._protocol_data, self._raw_data
|
|
180
|
+
)
|
|
181
|
+
return None
|
|
151
182
|
|
|
152
183
|
# Alarm will have `SENSOR` as the source, since that is likely what
|
|
153
184
|
# triggered it
|
|
@@ -182,6 +213,7 @@ class G90History:
|
|
|
182
213
|
Returns the history entry represented as device alert structure,
|
|
183
214
|
suitable for :meth:`G90DeviceNotifications._handle_alert`.
|
|
184
215
|
"""
|
|
216
|
+
|
|
185
217
|
return G90DeviceAlert(
|
|
186
218
|
type=self._protocol_data.type,
|
|
187
219
|
event_id=self._protocol_data.event_id,
|
|
@@ -103,7 +103,12 @@ class G90TargetedDiscovery(G90BaseCommand):
|
|
|
103
103
|
"""
|
|
104
104
|
try:
|
|
105
105
|
_LOGGER.debug('Received from %s:%s: %s', addr[0], addr[1], data)
|
|
106
|
-
|
|
106
|
+
try:
|
|
107
|
+
decoded = data.decode('utf-8')
|
|
108
|
+
except UnicodeDecodeError as exc:
|
|
109
|
+
raise G90Error(
|
|
110
|
+
'Unable to decode discovery response from UTF-8'
|
|
111
|
+
) from exc
|
|
107
112
|
if not decoded.endswith('\0'):
|
|
108
113
|
raise G90Error('Invalid discovery response')
|
|
109
114
|
host_info = G90TargetedDiscoveryInfo(*decoded[:-1].split(','))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyg90alarm
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.16.1
|
|
4
4
|
Summary: G90 Alarm system protocol
|
|
5
5
|
Home-page: https://github.com/hostcc/pyg90alarm
|
|
6
6
|
Author: Ilia Sotnikov
|
|
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.10
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
24
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
24
25
|
Requires-Python: >=3.8, <4
|
|
25
26
|
Description-Content-Type: text/x-rst
|
|
@@ -308,8 +308,8 @@ async def test_sensor_event(mock_device: DeviceMock) -> None:
|
|
|
308
308
|
reset_interval = 0.5
|
|
309
309
|
g90 = G90Alarm(host=mock_device.host, port=mock_device.port,
|
|
310
310
|
reset_occupancy_interval=reset_interval,
|
|
311
|
-
|
|
312
|
-
|
|
311
|
+
notifications_local_host=mock_device.notification_host,
|
|
312
|
+
notifications_local_port=mock_device.notification_port)
|
|
313
313
|
|
|
314
314
|
sensors = await g90.get_sensors()
|
|
315
315
|
prop_sensors = await g90.sensors
|
|
@@ -352,8 +352,8 @@ async def test_sensor_low_battery_event(mock_device: DeviceMock) -> None:
|
|
|
352
352
|
Tests for sensor low battery callback.
|
|
353
353
|
"""
|
|
354
354
|
g90 = G90Alarm(host=mock_device.host, port=mock_device.port,
|
|
355
|
-
|
|
356
|
-
|
|
355
|
+
notifications_local_host=mock_device.notification_host,
|
|
356
|
+
notifications_local_port=mock_device.notification_port)
|
|
357
357
|
|
|
358
358
|
sensors = await g90.get_sensors()
|
|
359
359
|
prop_sensors = await g90.sensors
|
|
@@ -401,8 +401,8 @@ async def test_armdisarm_callback(mock_device: DeviceMock) -> None:
|
|
|
401
401
|
armdisarm_cb = MagicMock()
|
|
402
402
|
armdisarm_cb.side_effect = lambda *args: future.set_result(True)
|
|
403
403
|
g90 = G90Alarm(host=mock_device.host, port=mock_device.port,
|
|
404
|
-
|
|
405
|
-
|
|
404
|
+
notifications_local_host=mock_device.notification_host,
|
|
405
|
+
notifications_local_port=mock_device.notification_port)
|
|
406
406
|
g90.armdisarm_callback = armdisarm_cb
|
|
407
407
|
await g90.listen_device_notifications()
|
|
408
408
|
await mock_device.send_next_notification()
|
|
@@ -434,8 +434,8 @@ async def test_door_open_close_callback(mock_device: DeviceMock) -> None:
|
|
|
434
434
|
door_open_close_cb.side_effect = lambda *args: future.set_result(True)
|
|
435
435
|
|
|
436
436
|
g90 = G90Alarm(host=mock_device.host, port=mock_device.port,
|
|
437
|
-
|
|
438
|
-
|
|
437
|
+
notifications_local_host=mock_device.notification_host,
|
|
438
|
+
notifications_local_port=mock_device.notification_port)
|
|
439
439
|
g90.door_open_close_callback = door_open_close_cb
|
|
440
440
|
|
|
441
441
|
# Simulate two device alerts - for opening (this one) and then closing the
|
|
@@ -492,8 +492,8 @@ async def test_alarm_callback(mock_device: DeviceMock) -> None:
|
|
|
492
492
|
alarm_cb.side_effect = lambda *args: future.set_result(True)
|
|
493
493
|
|
|
494
494
|
g90 = G90Alarm(host=mock_device.host, port=mock_device.port,
|
|
495
|
-
|
|
496
|
-
|
|
495
|
+
notifications_local_host=mock_device.notification_host,
|
|
496
|
+
notifications_local_port=mock_device.notification_port)
|
|
497
497
|
sensors = await g90.get_sensors()
|
|
498
498
|
# Set extra data for the 1st sensor
|
|
499
499
|
sensors[0].extra_data = 'Dummy extra data'
|
|
@@ -575,7 +575,7 @@ async def test_disarm(mock_device: DeviceMock) -> None:
|
|
|
575
575
|
|
|
576
576
|
@pytest.mark.g90device(sent_data=[
|
|
577
577
|
b'ISTART[200,[[50,1,5],'
|
|
578
|
-
b'[3,33,7,
|
|
578
|
+
b'[3,33,7,1,"Sensor 1",1630147285,""],'
|
|
579
579
|
b'[2,3,0,0,"",1630142877,""],'
|
|
580
580
|
b'[2,5,0,0,"",1630142871,""],'
|
|
581
581
|
b'[2,4,0,0,"",1630142757,""],'
|
|
@@ -595,6 +595,33 @@ async def test_history(mock_device: DeviceMock) -> None:
|
|
|
595
595
|
assert isinstance(history[0]._asdict(), dict)
|
|
596
596
|
|
|
597
597
|
|
|
598
|
+
@pytest.mark.g90device(sent_data=[
|
|
599
|
+
b'ISTART[200,[[3,1,3],'
|
|
600
|
+
# Wrong state
|
|
601
|
+
b'[3,33,7,254,"Sensor 1",1630147285,""],'
|
|
602
|
+
# Wrong source
|
|
603
|
+
b'[2,33,254,1,"Sensor 1",1630147285,""],'
|
|
604
|
+
# Wrong type
|
|
605
|
+
b'[254,33,1,1,"Sensor 1",1630147285,""]'
|
|
606
|
+
b']]IEND\0',
|
|
607
|
+
])
|
|
608
|
+
async def test_history_parsing_error(mock_device: DeviceMock) -> None:
|
|
609
|
+
"""
|
|
610
|
+
Tests for processing history from the device, when the parsing error
|
|
611
|
+
occurs.
|
|
612
|
+
"""
|
|
613
|
+
g90 = G90Alarm(host=mock_device.host, port=mock_device.port)
|
|
614
|
+
history = await g90.history(count=5)
|
|
615
|
+
assert len(history) == 3
|
|
616
|
+
assert isinstance(history[0], G90History)
|
|
617
|
+
assert isinstance(history[0]._asdict(), dict)
|
|
618
|
+
# Wrong entry element should result in corresponding key having 'None'
|
|
619
|
+
# value
|
|
620
|
+
assert history[0]._asdict()['state'] is None
|
|
621
|
+
assert history[1]._asdict()['source'] is None
|
|
622
|
+
assert history[2]._asdict()['type'] is None
|
|
623
|
+
|
|
624
|
+
|
|
598
625
|
@pytest.mark.g90device(sent_data=[
|
|
599
626
|
# Simulate empty history initially
|
|
600
627
|
b'ISTART[200,[[0,0,0]]]IEND\0',
|
|
@@ -606,7 +633,7 @@ async def test_history(mock_device: DeviceMock) -> None:
|
|
|
606
633
|
# The records will be used to simulate the device alerts, but only for
|
|
607
634
|
# those newer that one above
|
|
608
635
|
b'ISTART[200,[[3,1,3],'
|
|
609
|
-
b'[3,33,7,
|
|
636
|
+
b'[3,33,7,1,"Sensor 1",1630147285,""],'
|
|
610
637
|
b'[2,3,0,0,"",1630142877,""],'
|
|
611
638
|
b'[2,5,0,0,"",1630142871,""]'
|
|
612
639
|
b']]IEND\0',
|
|
@@ -753,8 +780,8 @@ async def test_sms_alert_when_armed(mock_device: DeviceMock) -> None:
|
|
|
753
780
|
armdisarm_cb = MagicMock()
|
|
754
781
|
armdisarm_cb.side_effect = lambda *args: future.set_result(True)
|
|
755
782
|
g90 = G90Alarm(host=mock_device.host, port=mock_device.port,
|
|
756
|
-
|
|
757
|
-
|
|
783
|
+
notifications_local_host=mock_device.notification_host,
|
|
784
|
+
notifications_local_port=mock_device.notification_port)
|
|
758
785
|
g90.armdisarm_callback = armdisarm_cb
|
|
759
786
|
g90.sms_alert_when_armed = True
|
|
760
787
|
await g90.listen_device_notifications()
|
|
@@ -787,8 +814,8 @@ async def test_sms_alert_when_disarmed(mock_device: DeviceMock) -> None:
|
|
|
787
814
|
armdisarm_cb = MagicMock()
|
|
788
815
|
armdisarm_cb.side_effect = lambda *args: future.set_result(True)
|
|
789
816
|
g90 = G90Alarm(host=mock_device.host, port=mock_device.port,
|
|
790
|
-
|
|
791
|
-
|
|
817
|
+
notifications_local_host=mock_device.notification_host,
|
|
818
|
+
notifications_local_port=mock_device.notification_port)
|
|
792
819
|
g90.armdisarm_callback = armdisarm_cb
|
|
793
820
|
g90.sms_alert_when_armed = True
|
|
794
821
|
await g90.listen_device_notifications()
|
|
@@ -117,6 +117,26 @@ async def test_timeout(mock_device: DeviceMock) -> None:
|
|
|
117
117
|
]
|
|
118
118
|
|
|
119
119
|
|
|
120
|
+
@pytest.mark.g90device(sent_data=[
|
|
121
|
+
b'\xdeadbeef\0',
|
|
122
|
+
])
|
|
123
|
+
async def test_invalid_utf8_encoding(mock_device: DeviceMock) -> None:
|
|
124
|
+
"""
|
|
125
|
+
Verifies that invalid UTF-8 encoding of response is handled properly.
|
|
126
|
+
"""
|
|
127
|
+
g90 = G90BaseCommand(
|
|
128
|
+
host=mock_device.host, port=mock_device.port,
|
|
129
|
+
code=G90Commands.GETHOSTINFO
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
with pytest.raises(
|
|
133
|
+
G90Error,
|
|
134
|
+
match=re.escape("Unable to decode response from UTF-8")
|
|
135
|
+
):
|
|
136
|
+
await g90.process()
|
|
137
|
+
assert mock_device.recv_data == [b'ISTART[206,206,""]IEND\0']
|
|
138
|
+
|
|
139
|
+
|
|
120
140
|
@pytest.mark.g90device(sent_data=[
|
|
121
141
|
b'ISTART[IEND\0',
|
|
122
142
|
])
|
|
@@ -57,6 +57,31 @@ async def test_targeted_discovery(mock_device: DeviceMock) -> None:
|
|
|
57
57
|
assert mock_device.recv_data == [b'IWTAC_PROBE_DEVICE,DUMMYGUID\0']
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
@pytest.mark.g90device(sent_data=[
|
|
61
|
+
b'\xdeadbeef'
|
|
62
|
+
])
|
|
63
|
+
async def test_targeted_discovery_invalid_utf_response(
|
|
64
|
+
mock_device: DeviceMock, caplog: LogCaptureFixture
|
|
65
|
+
) -> None:
|
|
66
|
+
"""
|
|
67
|
+
Verifies that invalid UTF-8 data in response to targeted discovery is
|
|
68
|
+
logged but ignored.
|
|
69
|
+
"""
|
|
70
|
+
g90 = G90TargetedDiscovery(
|
|
71
|
+
device_id='DUMMYGUID',
|
|
72
|
+
host=mock_device.host,
|
|
73
|
+
port=mock_device.port,
|
|
74
|
+
local_port=LOCAL_TARGETED_DISCOVERY_PORT,
|
|
75
|
+
timeout=0.1)
|
|
76
|
+
|
|
77
|
+
caplog.set_level('WARNING')
|
|
78
|
+
await g90.process()
|
|
79
|
+
assert ''.join(caplog.messages) == (
|
|
80
|
+
'Got exception, ignoring:'
|
|
81
|
+
' Unable to decode discovery response from UTF-8'
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
60
85
|
@pytest.mark.g90device(sent_data=[
|
|
61
86
|
b'IWTAC_PROBE_DEVICE_ACK_BAD,TSV018-3SIA'
|
|
62
87
|
b',1.2,1.1,206,1.8,3,3,1,0,2,50,100\0',
|
|
@@ -10,10 +10,34 @@ from pytest import LogCaptureFixture
|
|
|
10
10
|
from pyg90alarm.device_notifications import (
|
|
11
11
|
G90DeviceNotifications,
|
|
12
12
|
)
|
|
13
|
+
from pyg90alarm.alarm import G90Alarm
|
|
13
14
|
|
|
14
15
|
from .device_mock import DeviceMock
|
|
15
16
|
|
|
16
17
|
|
|
18
|
+
@pytest.mark.g90device(notification_data=[
|
|
19
|
+
b'\xdeadbeef\0',
|
|
20
|
+
])
|
|
21
|
+
async def test_device_notification_invalid_utf_data(
|
|
22
|
+
mock_device: DeviceMock, caplog: LogCaptureFixture
|
|
23
|
+
) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Verifies that wrong UTF encoded data in device notification is handled
|
|
26
|
+
correctly.
|
|
27
|
+
"""
|
|
28
|
+
notifications = G90DeviceNotifications(
|
|
29
|
+
local_host=mock_device.notification_host,
|
|
30
|
+
local_port=mock_device.notification_port
|
|
31
|
+
)
|
|
32
|
+
caplog.set_level('ERROR')
|
|
33
|
+
await notifications.listen()
|
|
34
|
+
await mock_device.send_next_notification()
|
|
35
|
+
assert ''.join(caplog.messages) == (
|
|
36
|
+
"Unable to decode device message from UTF-8"
|
|
37
|
+
)
|
|
38
|
+
notifications.close()
|
|
39
|
+
|
|
40
|
+
|
|
17
41
|
@pytest.mark.g90device(notification_data=[
|
|
18
42
|
b'[170]\0',
|
|
19
43
|
])
|
|
@@ -24,7 +48,8 @@ async def test_device_notification_missing_header(
|
|
|
24
48
|
Verifies that missing header in device notification is handled correctly.
|
|
25
49
|
"""
|
|
26
50
|
notifications = G90DeviceNotifications(
|
|
27
|
-
|
|
51
|
+
local_host=mock_device.notification_host,
|
|
52
|
+
local_port=mock_device.notification_port
|
|
28
53
|
)
|
|
29
54
|
caplog.set_level('ERROR')
|
|
30
55
|
await notifications.listen()
|
|
@@ -49,7 +74,8 @@ async def test_device_notification_malformed_message(
|
|
|
49
74
|
correctly.
|
|
50
75
|
"""
|
|
51
76
|
notifications = G90DeviceNotifications(
|
|
52
|
-
|
|
77
|
+
local_host=mock_device.notification_host,
|
|
78
|
+
local_port=mock_device.notification_port
|
|
53
79
|
)
|
|
54
80
|
caplog.set_level('ERROR')
|
|
55
81
|
await notifications.listen()
|
|
@@ -72,7 +98,8 @@ async def test_device_notification_missing_end_marker(
|
|
|
72
98
|
correctly.
|
|
73
99
|
"""
|
|
74
100
|
notifications = G90DeviceNotifications(
|
|
75
|
-
|
|
101
|
+
local_host=mock_device.notification_host,
|
|
102
|
+
local_port=mock_device.notification_port
|
|
76
103
|
)
|
|
77
104
|
caplog.set_level('ERROR')
|
|
78
105
|
await notifications.listen()
|
|
@@ -91,7 +118,8 @@ async def test_wrong_device_notification_format(
|
|
|
91
118
|
Verifies that wrong device notification format is handled correctly.
|
|
92
119
|
"""
|
|
93
120
|
notifications = G90DeviceNotifications(
|
|
94
|
-
|
|
121
|
+
local_host=mock_device.notification_host,
|
|
122
|
+
local_port=mock_device.notification_port
|
|
95
123
|
)
|
|
96
124
|
caplog.set_level('ERROR')
|
|
97
125
|
await notifications.listen()
|
|
@@ -114,7 +142,8 @@ async def test_wrong_device_alert_format(
|
|
|
114
142
|
Verifies that wrong device alert format is handled correctly.
|
|
115
143
|
"""
|
|
116
144
|
notifications = G90DeviceNotifications(
|
|
117
|
-
|
|
145
|
+
local_host=mock_device.notification_host,
|
|
146
|
+
local_port=mock_device.notification_port
|
|
118
147
|
)
|
|
119
148
|
|
|
120
149
|
caplog.set_level('ERROR')
|
|
@@ -140,7 +169,8 @@ async def test_unknown_device_notification(
|
|
|
140
169
|
Verifies that unknown device notification is handled correctly.
|
|
141
170
|
"""
|
|
142
171
|
notifications = G90DeviceNotifications(
|
|
143
|
-
|
|
172
|
+
local_host=mock_device.notification_host,
|
|
173
|
+
local_port=mock_device.notification_port
|
|
144
174
|
)
|
|
145
175
|
caplog.set_level('WARNING')
|
|
146
176
|
await notifications.listen()
|
|
@@ -164,7 +194,8 @@ async def test_unknown_device_alert(
|
|
|
164
194
|
Verifies that unknown device alert is handled correctly.
|
|
165
195
|
"""
|
|
166
196
|
notifications = G90DeviceNotifications(
|
|
167
|
-
|
|
197
|
+
local_host=mock_device.notification_host,
|
|
198
|
+
local_port=mock_device.notification_port
|
|
168
199
|
)
|
|
169
200
|
caplog.set_level('WARNING')
|
|
170
201
|
await notifications.listen()
|
|
@@ -179,6 +210,79 @@ async def test_unknown_device_alert(
|
|
|
179
210
|
notifications.close()
|
|
180
211
|
|
|
181
212
|
|
|
213
|
+
@pytest.mark.g90device(notification_data=[
|
|
214
|
+
b'[208,[999,100,1,1,"Hall","DUMMYGUID",'
|
|
215
|
+
b'1631545189,0,[""]]]\0',
|
|
216
|
+
])
|
|
217
|
+
async def test_wrong_host(
|
|
218
|
+
mock_device: DeviceMock, caplog: LogCaptureFixture
|
|
219
|
+
) -> None:
|
|
220
|
+
"""
|
|
221
|
+
Verifies that unknown device alert is handled correctly.
|
|
222
|
+
"""
|
|
223
|
+
g90 = G90Alarm(
|
|
224
|
+
host='1.2.3.4',
|
|
225
|
+
notifications_local_host=mock_device.notification_host,
|
|
226
|
+
notifications_local_port=mock_device.notification_port
|
|
227
|
+
)
|
|
228
|
+
# pylint: disable=protected-access
|
|
229
|
+
g90._handle_alert = ( # type: ignore[method-assign]
|
|
230
|
+
MagicMock()
|
|
231
|
+
)
|
|
232
|
+
# pylint: disable=protected-access
|
|
233
|
+
g90._handle_notification = ( # type: ignore[method-assign]
|
|
234
|
+
MagicMock()
|
|
235
|
+
)
|
|
236
|
+
caplog.set_level('WARNING')
|
|
237
|
+
await g90.listen()
|
|
238
|
+
await mock_device.send_next_notification()
|
|
239
|
+
assert ''.join(caplog.messages) == (
|
|
240
|
+
"Received notification/alert from wrong host '127.0.0.1'"
|
|
241
|
+
", expected from '1.2.3.4'"
|
|
242
|
+
)
|
|
243
|
+
g90.close()
|
|
244
|
+
# pylint: disable=protected-access
|
|
245
|
+
g90._handle_alert.assert_not_called()
|
|
246
|
+
# pylint: disable=protected-access
|
|
247
|
+
g90._handle_notification.assert_not_called()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@pytest.mark.g90device(
|
|
251
|
+
sent_data=[
|
|
252
|
+
b'ISTART[206,'
|
|
253
|
+
b'["DUMMYGUID","DUMMYPRODUCT",'
|
|
254
|
+
b'"1.2","1.1","206","206",3,3,0,2,"4242",50,100]]IEND\0',
|
|
255
|
+
],
|
|
256
|
+
notification_data=[
|
|
257
|
+
b'[208,[2,4,0,0,"","DIFFERENTGUID",1630876128,0,[""]]]\0'
|
|
258
|
+
],
|
|
259
|
+
)
|
|
260
|
+
async def test_wrong_device_guid(
|
|
261
|
+
mock_device: DeviceMock, caplog: LogCaptureFixture
|
|
262
|
+
) -> None:
|
|
263
|
+
"""
|
|
264
|
+
Verifies that alert from device with different GUID is ignored.
|
|
265
|
+
"""
|
|
266
|
+
g90 = G90Alarm(
|
|
267
|
+
host=mock_device.host, port=mock_device.port,
|
|
268
|
+
notifications_local_host=mock_device.notification_host,
|
|
269
|
+
notifications_local_port=mock_device.notification_port
|
|
270
|
+
)
|
|
271
|
+
caplog.set_level('WARNING')
|
|
272
|
+
# The command will fetch the host info and store the GIUD
|
|
273
|
+
await g90.get_host_info()
|
|
274
|
+
g90.on_armdisarm = MagicMock() # type: ignore[method-assign]
|
|
275
|
+
await g90.listen()
|
|
276
|
+
await mock_device.send_next_notification()
|
|
277
|
+
assert ''.join(caplog.messages) == (
|
|
278
|
+
"Received alert from wrong device: expected 'DUMMYGUID'"
|
|
279
|
+
", got 'DIFFERENTGUID'"
|
|
280
|
+
)
|
|
281
|
+
g90.close()
|
|
282
|
+
# Verify the associated callback was not called
|
|
283
|
+
g90.on_armdisarm.assert_not_called()
|
|
284
|
+
|
|
285
|
+
|
|
182
286
|
@pytest.mark.g90device(notification_data=[
|
|
183
287
|
b'[170,[5,[100,"Hall"]]]\0',
|
|
184
288
|
])
|
|
@@ -188,8 +292,8 @@ async def test_sensor_callback(mock_device: DeviceMock) -> None:
|
|
|
188
292
|
"""
|
|
189
293
|
future = asyncio.get_running_loop().create_future()
|
|
190
294
|
notifications = G90DeviceNotifications(
|
|
191
|
-
|
|
192
|
-
|
|
295
|
+
local_host=mock_device.notification_host,
|
|
296
|
+
local_port=mock_device.notification_port,
|
|
193
297
|
)
|
|
194
298
|
|
|
195
299
|
notifications.on_sensor_activity = ( # type: ignore[method-assign]
|
|
@@ -216,7 +320,8 @@ async def test_armdisarm_notification_callback(
|
|
|
216
320
|
"""
|
|
217
321
|
future = asyncio.get_running_loop().create_future()
|
|
218
322
|
notifications = G90DeviceNotifications(
|
|
219
|
-
|
|
323
|
+
local_host=mock_device.notification_host,
|
|
324
|
+
local_port=mock_device.notification_port
|
|
220
325
|
)
|
|
221
326
|
notifications.on_armdisarm = MagicMock() # type: ignore[method-assign]
|
|
222
327
|
notifications.on_armdisarm.side_effect = (
|
|
@@ -238,7 +343,8 @@ async def test_armdisarm_alert_callback(mock_device: DeviceMock) -> None:
|
|
|
238
343
|
"""
|
|
239
344
|
future = asyncio.get_running_loop().create_future()
|
|
240
345
|
notifications = G90DeviceNotifications(
|
|
241
|
-
|
|
346
|
+
local_host=mock_device.notification_host,
|
|
347
|
+
local_port=mock_device.notification_port
|
|
242
348
|
)
|
|
243
349
|
notifications.on_armdisarm = MagicMock() # type: ignore[method-assign]
|
|
244
350
|
notifications.on_armdisarm.side_effect = (
|
|
@@ -260,7 +366,8 @@ async def test_door_open_callback(mock_device: DeviceMock) -> None:
|
|
|
260
366
|
"""
|
|
261
367
|
future = asyncio.get_running_loop().create_future()
|
|
262
368
|
notifications = G90DeviceNotifications(
|
|
263
|
-
|
|
369
|
+
local_host=mock_device.notification_host,
|
|
370
|
+
local_port=mock_device.notification_port
|
|
264
371
|
)
|
|
265
372
|
|
|
266
373
|
notifications.on_door_open_close = ( # type: ignore[method-assign]
|
|
@@ -285,7 +392,8 @@ async def test_door_close_callback(mock_device: DeviceMock) -> None:
|
|
|
285
392
|
"""
|
|
286
393
|
future = asyncio.get_running_loop().create_future()
|
|
287
394
|
notifications = G90DeviceNotifications(
|
|
288
|
-
|
|
395
|
+
local_host=mock_device.notification_host,
|
|
396
|
+
local_port=mock_device.notification_port
|
|
289
397
|
)
|
|
290
398
|
|
|
291
399
|
notifications.on_door_open_close = ( # type: ignore[method-assign]
|
|
@@ -312,7 +420,8 @@ async def test_doorbell_callback(mock_device: DeviceMock) -> None:
|
|
|
312
420
|
"""
|
|
313
421
|
future = asyncio.get_running_loop().create_future()
|
|
314
422
|
notifications = G90DeviceNotifications(
|
|
315
|
-
|
|
423
|
+
local_host=mock_device.notification_host,
|
|
424
|
+
local_port=mock_device.notification_port
|
|
316
425
|
)
|
|
317
426
|
|
|
318
427
|
notifications.on_door_open_close = ( # type: ignore[method-assign]
|
|
@@ -339,7 +448,8 @@ async def test_alarm_callback(mock_device: DeviceMock) -> None:
|
|
|
339
448
|
"""
|
|
340
449
|
future = asyncio.get_running_loop().create_future()
|
|
341
450
|
notifications = G90DeviceNotifications(
|
|
342
|
-
|
|
451
|
+
local_host=mock_device.notification_host,
|
|
452
|
+
local_port=mock_device.notification_port
|
|
343
453
|
)
|
|
344
454
|
notifications.on_alarm = MagicMock() # type: ignore[method-assign]
|
|
345
455
|
notifications.on_alarm.side_effect = (
|
|
@@ -361,7 +471,8 @@ async def test_low_battery_callback(mock_device: DeviceMock) -> None:
|
|
|
361
471
|
"""
|
|
362
472
|
future = asyncio.get_running_loop().create_future()
|
|
363
473
|
notifications = G90DeviceNotifications(
|
|
364
|
-
|
|
474
|
+
local_host=mock_device.notification_host,
|
|
475
|
+
local_port=mock_device.notification_port
|
|
365
476
|
)
|
|
366
477
|
notifications.on_low_battery = MagicMock() # type: ignore[method-assign]
|
|
367
478
|
notifications.on_low_battery.side_effect = (
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|