pyg90alarm 1.17.0__tar.gz → 1.17.2__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.17.0 → pyg90alarm-1.17.2}/.github/workflows/main.yml +4 -2
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/PKG-INFO +1 -1
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/alarm.py +43 -28
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/device_notifications.py +7 -3
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm.egg-info/PKG-INFO +1 -1
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/tests/test_alarm.py +51 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/tests/test_history.py +7 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/tox.ini +1 -6
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/.github/CODEOWNERS +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/.gitignore +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/.pylintrc +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/.readthedocs.yaml +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/LICENSE +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/MANIFEST.in +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/README.rst +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/docs/.DS_Store +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/docs/.gitignore +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/docs/api-docs.rst +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/docs/conf.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/docs/index.rst +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/docs/protocol.rst +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/docs/requirements.txt +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/pyproject.toml +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/setup.cfg +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/setup.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/sonar-project.properties +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/__init__.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/base_cmd.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/callback.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/config.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/const.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/definitions/__init__.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/definitions/sensors.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/discovery.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/entities/__init__.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/entities/device.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/entities/sensor.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/exceptions.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/history.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/host_info.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/host_status.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/paginated_cmd.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/paginated_result.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/py.typed +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/targeted_discovery.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/user_data_crc.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm.egg-info/SOURCES.txt +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm.egg-info/dependency_links.txt +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm.egg-info/requires.txt +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm.egg-info/top_level.txt +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/tests/__init__.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/tests/conftest.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/tests/device_mock.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/tests/test_base_commands.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/tests/test_discovery.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/tests/test_notifications.py +0 -0
- {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/tests/test_paginated_commands.py +0 -0
|
@@ -75,6 +75,8 @@ jobs:
|
|
|
75
75
|
name: Publish to PyPi
|
|
76
76
|
runs-on: ubuntu-latest
|
|
77
77
|
needs: [tests]
|
|
78
|
+
permissions:
|
|
79
|
+
id-token: write # Required for trusted publishing
|
|
78
80
|
steps:
|
|
79
81
|
- name: Checkout the code
|
|
80
82
|
uses: actions/checkout@v3
|
|
@@ -94,8 +96,8 @@ jobs:
|
|
|
94
96
|
if: github.event_name != 'release'
|
|
95
97
|
uses: pypa/gh-action-pypi-publish@release/v1
|
|
96
98
|
with:
|
|
97
|
-
password: ${{ secrets.TEST_PYPI_TOKEN }}
|
|
98
99
|
repository_url: https://test.pypi.org/legacy/
|
|
100
|
+
attestations: true
|
|
99
101
|
- name: Publish the release to PyPi
|
|
100
102
|
# Publish to production PyPi only happens when a release published out
|
|
101
103
|
# of the main branch
|
|
@@ -106,4 +108,4 @@ jobs:
|
|
|
106
108
|
|| github.event.release.target_commitish == 'master')
|
|
107
109
|
uses: pypa/gh-action-pypi-publish@release/v1
|
|
108
110
|
with:
|
|
109
|
-
|
|
111
|
+
attestations: true
|
|
@@ -161,7 +161,9 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
161
161
|
self._host: str = host
|
|
162
162
|
self._port: int = port
|
|
163
163
|
self._sensors: List[G90Sensor] = []
|
|
164
|
+
self._sensors_lock = asyncio.Lock()
|
|
164
165
|
self._devices: List[G90Device] = []
|
|
166
|
+
self._devices_lock = asyncio.Lock()
|
|
165
167
|
self._notifications: Optional[G90DeviceNotifications] = None
|
|
166
168
|
self._sensor_cb: Optional[SensorCallback] = None
|
|
167
169
|
self._armdisarm_cb: Optional[ArmDisarmCallback] = None
|
|
@@ -258,20 +260,26 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
258
260
|
|
|
259
261
|
:return: List of sensors
|
|
260
262
|
"""
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
proto_idx=sensor.proto_idx
|
|
263
|
+
# Use lock around the operation, to ensure no duplicated entries in the
|
|
264
|
+
# resulting list or redundant exchanges with panel are made when the
|
|
265
|
+
# method is called concurrently
|
|
266
|
+
async with self._sensors_lock:
|
|
267
|
+
if not self._sensors:
|
|
268
|
+
sensors = self.paginated_result(
|
|
269
|
+
G90Commands.GETSENSORLIST
|
|
269
270
|
)
|
|
270
|
-
|
|
271
|
+
async for sensor in sensors:
|
|
272
|
+
obj = G90Sensor(
|
|
273
|
+
*sensor.data, parent=self, subindex=0,
|
|
274
|
+
proto_idx=sensor.proto_idx
|
|
275
|
+
)
|
|
276
|
+
self._sensors.append(obj)
|
|
271
277
|
|
|
272
|
-
|
|
278
|
+
_LOGGER.debug(
|
|
279
|
+
'Total number of sensors: %s', len(self._sensors)
|
|
280
|
+
)
|
|
273
281
|
|
|
274
|
-
|
|
282
|
+
return self._sensors
|
|
275
283
|
|
|
276
284
|
async def find_sensor(self, idx: int, name: str) -> Optional[G90Sensor]:
|
|
277
285
|
"""
|
|
@@ -316,28 +324,32 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
316
324
|
|
|
317
325
|
:return: List of devices
|
|
318
326
|
"""
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
obj = G90Device(
|
|
325
|
-
*device.data, parent=self, subindex=0,
|
|
326
|
-
proto_idx=device.proto_idx
|
|
327
|
+
# See `get_sensors` method for the rationale behind the lock usage
|
|
328
|
+
async with self._devices_lock:
|
|
329
|
+
if not self._devices:
|
|
330
|
+
devices = self.paginated_result(
|
|
331
|
+
G90Commands.GETDEVICELIST
|
|
327
332
|
)
|
|
328
|
-
|
|
329
|
-
# Multi-node devices (first node has already been added
|
|
330
|
-
# above
|
|
331
|
-
for node in range(1, obj.node_count):
|
|
333
|
+
async for device in devices:
|
|
332
334
|
obj = G90Device(
|
|
333
|
-
*device.data, parent=self,
|
|
334
|
-
|
|
335
|
+
*device.data, parent=self, subindex=0,
|
|
336
|
+
proto_idx=device.proto_idx
|
|
335
337
|
)
|
|
336
338
|
self._devices.append(obj)
|
|
339
|
+
# Multi-node devices (first node has already been added
|
|
340
|
+
# above
|
|
341
|
+
for node in range(1, obj.node_count):
|
|
342
|
+
obj = G90Device(
|
|
343
|
+
*device.data, parent=self,
|
|
344
|
+
subindex=node, proto_idx=device.proto_idx
|
|
345
|
+
)
|
|
346
|
+
self._devices.append(obj)
|
|
337
347
|
|
|
338
|
-
|
|
348
|
+
_LOGGER.debug(
|
|
349
|
+
'Total number of devices: %s', len(self._devices)
|
|
350
|
+
)
|
|
339
351
|
|
|
340
|
-
|
|
352
|
+
return self._devices
|
|
341
353
|
|
|
342
354
|
@property
|
|
343
355
|
async def host_info(self) -> G90HostInfo:
|
|
@@ -915,7 +927,10 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
915
927
|
# notifications port
|
|
916
928
|
self._handle_alert(
|
|
917
929
|
(self._host, self._notifications_local_port),
|
|
918
|
-
item.as_device_alert()
|
|
930
|
+
item.as_device_alert(),
|
|
931
|
+
# Skip verifying device GUID, since history entry
|
|
932
|
+
# don't have it
|
|
933
|
+
verify_device_id=False
|
|
919
934
|
)
|
|
920
935
|
|
|
921
936
|
# Record the entry as most recent one
|
|
@@ -208,13 +208,17 @@ class G90DeviceNotifications(DatagramProtocol):
|
|
|
208
208
|
return False
|
|
209
209
|
|
|
210
210
|
def _handle_alert(
|
|
211
|
-
self, addr: Tuple[str, int], alert: G90DeviceAlert
|
|
211
|
+
self, addr: Tuple[str, int], alert: G90DeviceAlert,
|
|
212
|
+
verify_device_id: bool = True
|
|
212
213
|
) -> None:
|
|
213
214
|
handled = False
|
|
214
215
|
|
|
215
216
|
# Stop processing when alert is received from the device with different
|
|
216
|
-
# GUID
|
|
217
|
-
if
|
|
217
|
+
# GUID (if enabled)
|
|
218
|
+
if (
|
|
219
|
+
verify_device_id and self.device_id
|
|
220
|
+
and alert.device_id != self.device_id
|
|
221
|
+
):
|
|
218
222
|
_LOGGER.error(
|
|
219
223
|
"Received alert from wrong device: expected '%s', got '%s'",
|
|
220
224
|
self.device_id, alert.device_id
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Tests for G90Alarm class
|
|
3
3
|
"""
|
|
4
4
|
import asyncio
|
|
5
|
+
from itertools import cycle
|
|
5
6
|
from unittest.mock import MagicMock
|
|
6
7
|
import pytest
|
|
7
8
|
|
|
@@ -128,6 +129,35 @@ async def test_devices(mock_device: DeviceMock) -> None:
|
|
|
128
129
|
assert isinstance(devices[0]._asdict(), dict)
|
|
129
130
|
|
|
130
131
|
|
|
132
|
+
# Provide an endless sequence of simulated panel responses for the devices
|
|
133
|
+
# list. Each attempt will simulate a single device. This sequence will prevent
|
|
134
|
+
# `G90TimeoutError` if the code under test initiates more exchanges with the
|
|
135
|
+
# panel than the simulated data contains.
|
|
136
|
+
@pytest.mark.g90device(sent_data=cycle([
|
|
137
|
+
b'ISTART[138,'
|
|
138
|
+
b'[[1,1,1],["Switch",10,0,10,1,0,32,0,0,16,1,0,""]]]IEND\0',
|
|
139
|
+
]))
|
|
140
|
+
async def test_get_devices_concurrent(mock_device: DeviceMock) -> None:
|
|
141
|
+
"""
|
|
142
|
+
Tests for concurrently retrieving list of devices produces consistent
|
|
143
|
+
results.
|
|
144
|
+
"""
|
|
145
|
+
g90 = G90Alarm(host=mock_device.host, port=mock_device.port)
|
|
146
|
+
g90.paginated_result = MagicMock( # type: ignore[method-assign]
|
|
147
|
+
spec=g90.paginated_result, wraps=g90.paginated_result
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Issue two concurrent requests to retrieve devices
|
|
151
|
+
res = await asyncio.gather(g90.get_devices(), g90.get_devices())
|
|
152
|
+
# Ensure only single exchange with the panel
|
|
153
|
+
g90.paginated_result.assert_called_once()
|
|
154
|
+
# While `pylint` demands use of generator, the comprehension is used
|
|
155
|
+
# instead for ease of trroubleshooting test failures as it will show the
|
|
156
|
+
# list elements, not just generator instance
|
|
157
|
+
# pylint: disable=use-a-generator
|
|
158
|
+
assert all([len(x) == 1 for x in res])
|
|
159
|
+
|
|
160
|
+
|
|
131
161
|
@pytest.mark.g90device(sent_data=[
|
|
132
162
|
b'ISTART[138,'
|
|
133
163
|
b'[[1,1,1],["Switch",10,0,10,1,0,32,0,0,16,2,0,""]]]IEND\0'
|
|
@@ -207,6 +237,27 @@ async def test_single_sensor(mock_device: DeviceMock) -> None:
|
|
|
207
237
|
assert isinstance(sensors[0]._asdict(), dict)
|
|
208
238
|
|
|
209
239
|
|
|
240
|
+
# See `test_get_devices_concurrent` for the explanation of the test
|
|
241
|
+
@pytest.mark.g90device(sent_data=cycle([
|
|
242
|
+
b'ISTART[102,'
|
|
243
|
+
b'[[1,1,1],["Remote",10,0,10,1,0,32,0,0,16,1,0,""]]]IEND\0',
|
|
244
|
+
]))
|
|
245
|
+
async def test_get_sensors_concurrent(mock_device: DeviceMock) -> None:
|
|
246
|
+
"""
|
|
247
|
+
Tests for concurrently retrieving list of sensors produces consistent
|
|
248
|
+
results.
|
|
249
|
+
"""
|
|
250
|
+
g90 = G90Alarm(host=mock_device.host, port=mock_device.port)
|
|
251
|
+
g90.paginated_result = MagicMock( # type: ignore[method-assign]
|
|
252
|
+
spec=g90.paginated_result, wraps=g90.paginated_result
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
res = await asyncio.gather(g90.get_sensors(), g90.get_sensors())
|
|
256
|
+
g90.paginated_result.assert_called_once()
|
|
257
|
+
# pylint: disable=use-a-generator
|
|
258
|
+
assert all([len(x) == 1 for x in res])
|
|
259
|
+
|
|
260
|
+
|
|
210
261
|
@pytest.mark.g90device(sent_data=[
|
|
211
262
|
b'ISTART[102,'
|
|
212
263
|
b'[[3,1,3],["Remote 1",10,0,10,1,0,32,0,0,16,1,0,""],'
|
|
@@ -135,6 +135,10 @@ async def test_history_parsing_error(mock_device: DeviceMock) -> None:
|
|
|
135
135
|
|
|
136
136
|
|
|
137
137
|
@pytest.mark.g90device(sent_data=[
|
|
138
|
+
# Host info
|
|
139
|
+
b'ISTART[206,'
|
|
140
|
+
b'["DUMMYGUID","DUMMYPRODUCT",'
|
|
141
|
+
b'"1.2","1.1","206","206",3,3,0,2,"4242",50,100]]IEND\0',
|
|
138
142
|
# Simulate empty history initially
|
|
139
143
|
b'ISTART[200,[[0,0,0]]]IEND\0',
|
|
140
144
|
# The history records will be used to remember the timestamp of most recent
|
|
@@ -173,6 +177,9 @@ async def test_simulate_alerts_from_history(mock_device: DeviceMock) -> None:
|
|
|
173
177
|
armdisarm_cb.side_effect = lambda *args: future_armdisarm.set_result(True)
|
|
174
178
|
|
|
175
179
|
g90 = G90Alarm(host=mock_device.host, port=mock_device.port)
|
|
180
|
+
# Call the method to store device GUID, so that its validation in
|
|
181
|
+
# `G90DeviceNotifications._handle_alert()` is involved
|
|
182
|
+
await g90.get_host_info()
|
|
176
183
|
g90.alarm_callback = alarm_cb
|
|
177
184
|
g90.armdisarm_callback = armdisarm_cb
|
|
178
185
|
# Simulate device timeout exception every 2nd call to `G90Alarm.history()`
|
|
@@ -16,19 +16,14 @@ isolated_build = true
|
|
|
16
16
|
deps =
|
|
17
17
|
-r requirements_dev.txt
|
|
18
18
|
|
|
19
|
-
allowlist_externals =
|
|
20
|
-
cat
|
|
21
19
|
commands =
|
|
22
20
|
check-manifest --ignore 'tox.ini,tests/**,docs/**,.pylintrc,.readthedocs.yaml,sonar-project.properties'
|
|
23
21
|
flake8 --tee --output-file=flake8.txt src/pyg90alarm/ tests/
|
|
24
|
-
pylint --output-format=parseable
|
|
22
|
+
pylint --output-format=text,parseable:pylint.txt src/pyg90alarm/ tests/
|
|
25
23
|
mypy --strict --cobertura-xml-report=mypy/ src/pyg90alarm/ tests/
|
|
26
24
|
# Ensure only traces for in-repository module is processed, not for one
|
|
27
25
|
# installed by `tox` (see above for more details)
|
|
28
26
|
pytest --cov=src/pyg90alarm --cov-append --cov-report=term-missing -v tests []
|
|
29
|
-
commands_post =
|
|
30
|
-
# Show the `pylint` report to the standard output, to ease fixing the issues reported
|
|
31
|
-
cat pylint.txt
|
|
32
27
|
|
|
33
28
|
[flake8]
|
|
34
29
|
exclude = .tox,*.egg,build,data,scripts,docs
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|