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.
Files changed (57) hide show
  1. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/.github/workflows/main.yml +4 -2
  2. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/PKG-INFO +1 -1
  3. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/alarm.py +43 -28
  4. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/device_notifications.py +7 -3
  5. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm.egg-info/PKG-INFO +1 -1
  6. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/tests/test_alarm.py +51 -0
  7. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/tests/test_history.py +7 -0
  8. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/tox.ini +1 -6
  9. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/.github/CODEOWNERS +0 -0
  10. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/.gitignore +0 -0
  11. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/.pylintrc +0 -0
  12. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/.readthedocs.yaml +0 -0
  13. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/LICENSE +0 -0
  14. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/MANIFEST.in +0 -0
  15. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/README.rst +0 -0
  16. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/docs/.DS_Store +0 -0
  17. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/docs/.gitignore +0 -0
  18. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/docs/api-docs.rst +0 -0
  19. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/docs/conf.py +0 -0
  20. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/docs/index.rst +0 -0
  21. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/docs/protocol.rst +0 -0
  22. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/docs/requirements.txt +0 -0
  23. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/pyproject.toml +0 -0
  24. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/setup.cfg +0 -0
  25. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/setup.py +0 -0
  26. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/sonar-project.properties +0 -0
  27. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/__init__.py +0 -0
  28. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/base_cmd.py +0 -0
  29. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/callback.py +0 -0
  30. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/config.py +0 -0
  31. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/const.py +0 -0
  32. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/definitions/__init__.py +0 -0
  33. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/definitions/sensors.py +0 -0
  34. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/discovery.py +0 -0
  35. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/entities/__init__.py +0 -0
  36. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/entities/device.py +0 -0
  37. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/entities/sensor.py +0 -0
  38. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/exceptions.py +0 -0
  39. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/history.py +0 -0
  40. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/host_info.py +0 -0
  41. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/host_status.py +0 -0
  42. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/paginated_cmd.py +0 -0
  43. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/paginated_result.py +0 -0
  44. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/py.typed +0 -0
  45. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/targeted_discovery.py +0 -0
  46. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm/user_data_crc.py +0 -0
  47. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm.egg-info/SOURCES.txt +0 -0
  48. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm.egg-info/dependency_links.txt +0 -0
  49. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm.egg-info/requires.txt +0 -0
  50. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/src/pyg90alarm.egg-info/top_level.txt +0 -0
  51. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/tests/__init__.py +0 -0
  52. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/tests/conftest.py +0 -0
  53. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/tests/device_mock.py +0 -0
  54. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/tests/test_base_commands.py +0 -0
  55. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/tests/test_discovery.py +0 -0
  56. {pyg90alarm-1.17.0 → pyg90alarm-1.17.2}/tests/test_notifications.py +0 -0
  57. {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
- password: ${{ secrets.PYPI_TOKEN }}
111
+ attestations: true
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyg90alarm
3
- Version: 1.17.0
3
+ Version: 1.17.2
4
4
  Summary: G90 Alarm system protocol
5
5
  Home-page: https://github.com/hostcc/pyg90alarm
6
6
  Author: Ilia Sotnikov
@@ -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
- if not self._sensors:
262
- sensors = self.paginated_result(
263
- G90Commands.GETSENSORLIST
264
- )
265
- async for sensor in sensors:
266
- obj = G90Sensor(
267
- *sensor.data, parent=self, subindex=0,
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
- self._sensors.append(obj)
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
- _LOGGER.debug('Total number of sensors: %s', len(self._sensors))
278
+ _LOGGER.debug(
279
+ 'Total number of sensors: %s', len(self._sensors)
280
+ )
273
281
 
274
- return self._sensors
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
- if not self._devices:
320
- devices = self.paginated_result(
321
- G90Commands.GETDEVICELIST
322
- )
323
- async for device in devices:
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
- self._devices.append(obj)
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
- subindex=node, proto_idx=device.proto_idx
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
- _LOGGER.debug('Total number of devices: %s', len(self._devices))
348
+ _LOGGER.debug(
349
+ 'Total number of devices: %s', len(self._devices)
350
+ )
339
351
 
340
- return self._devices
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 self.device_id and alert.device_id != self.device_id:
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyg90alarm
3
- Version: 1.17.0
3
+ Version: 1.17.2
4
4
  Summary: G90 Alarm system protocol
5
5
  Home-page: https://github.com/hostcc/pyg90alarm
6
6
  Author: Ilia Sotnikov
@@ -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 --output=pylint.txt src/pyg90alarm/ tests/
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