pyg90alarm 1.14.0__tar.gz → 1.15.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.
Files changed (56) hide show
  1. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/MANIFEST.in +2 -0
  2. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/PKG-INFO +1 -1
  3. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/pyproject.toml +1 -0
  4. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/alarm.py +24 -17
  5. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/entities/sensor.py +46 -10
  6. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/history.py +1 -6
  7. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm.egg-info/PKG-INFO +1 -1
  8. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/tests/test_alarm.py +28 -5
  9. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/tox.ini +1 -15
  10. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/.github/CODEOWNERS +0 -0
  11. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/.github/workflows/main.yml +0 -0
  12. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/.gitignore +0 -0
  13. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/.pylintrc +0 -0
  14. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/.readthedocs.yaml +0 -0
  15. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/LICENSE +0 -0
  16. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/README.rst +0 -0
  17. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/docs/.DS_Store +0 -0
  18. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/docs/.gitignore +0 -0
  19. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/docs/api-docs.rst +0 -0
  20. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/docs/conf.py +0 -0
  21. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/docs/index.rst +0 -0
  22. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/docs/protocol.rst +0 -0
  23. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/docs/requirements.txt +0 -0
  24. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/setup.cfg +0 -0
  25. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/setup.py +0 -0
  26. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/sonar-project.properties +0 -0
  27. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/__init__.py +0 -0
  28. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/base_cmd.py +0 -0
  29. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/callback.py +0 -0
  30. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/config.py +0 -0
  31. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/const.py +0 -0
  32. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/definitions/__init__.py +0 -0
  33. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/definitions/sensors.py +0 -0
  34. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/device_notifications.py +0 -0
  35. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/discovery.py +0 -0
  36. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/entities/__init__.py +0 -0
  37. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/entities/device.py +0 -0
  38. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/exceptions.py +0 -0
  39. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/host_info.py +0 -0
  40. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/host_status.py +0 -0
  41. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/paginated_cmd.py +0 -0
  42. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/paginated_result.py +0 -0
  43. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/py.typed +0 -0
  44. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/targeted_discovery.py +0 -0
  45. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm/user_data_crc.py +0 -0
  46. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm.egg-info/SOURCES.txt +0 -0
  47. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm.egg-info/dependency_links.txt +0 -0
  48. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm.egg-info/requires.txt +0 -0
  49. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/src/pyg90alarm.egg-info/top_level.txt +0 -0
  50. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/tests/__init__.py +0 -0
  51. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/tests/conftest.py +0 -0
  52. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/tests/device_mock.py +0 -0
  53. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/tests/test_base_commands.py +0 -0
  54. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/tests/test_discovery.py +0 -0
  55. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/tests/test_notifications.py +0 -0
  56. {pyg90alarm-1.14.0 → pyg90alarm-1.15.1}/tests/test_paginated_commands.py +0 -0
@@ -10,3 +10,5 @@ include LICENSE
10
10
  include setup.py
11
11
 
12
12
  recursive-include src py.typed
13
+
14
+ exclude requirements_dev.txt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyg90alarm
3
- Version: 1.14.0
3
+ Version: 1.15.1
4
4
  Summary: G90 Alarm system protocol
5
5
  Home-page: https://github.com/hostcc/pyg90alarm
6
6
  Author: Ilia Sotnikov
@@ -11,6 +11,7 @@ local_scheme = "no-local-version"
11
11
  log_cli = 1
12
12
  log_cli_level = "error"
13
13
  asyncio_mode = "auto"
14
+ pythonpath = "src/"
14
15
 
15
16
  [tool.coverage.run]
16
17
  relative_files = true
@@ -56,9 +56,6 @@ from typing import (
56
56
  TYPE_CHECKING, Any, List, Optional, AsyncGenerator,
57
57
  Callable, Coroutine, Union
58
58
  )
59
- from typing_extensions import (
60
- TypeAlias
61
- )
62
59
  from .const import (
63
60
  G90Commands, REMOTE_PORT,
64
61
  REMOTE_TARGETED_DISCOVERY_PORT,
@@ -91,7 +88,7 @@ if TYPE_CHECKING:
91
88
  # Type alias for the callback functions available to the user, should be
92
89
  # compatible with `G90Callback.Callback` type, since `G90Callback.invoke`
93
90
  # is used to invoke them
94
- AlarmCallback: TypeAlias = Union[
91
+ AlarmCallback = Union[
95
92
  Callable[[int, str, Any], None],
96
93
  Callable[[int, str, Any], Coroutine[None, None, None]]
97
94
  ]
@@ -103,22 +100,25 @@ if TYPE_CHECKING:
103
100
  Callable[[int, str, bool], None],
104
101
  Callable[[int, str, bool], Coroutine[None, None, None]]
105
102
  ]
106
- SensorStateCallback = Union[
107
- Callable[[bool], None],
108
- Callable[[bool], Coroutine[None, None, None]]
109
- ]
110
103
  LowBatteryCallback = Union[
111
104
  Callable[[int, str], None],
112
105
  Callable[[int, str], Coroutine[None, None, None]]
113
106
  ]
114
- SensorLowBatteryCallback = Union[
115
- Callable[[bool], None],
116
- Callable[[bool], Coroutine[None, None, None]]
117
- ]
118
107
  ArmDisarmCallback = Union[
119
108
  Callable[[G90ArmDisarmTypes], None],
120
109
  Callable[[G90ArmDisarmTypes], Coroutine[None, None, None]]
121
110
  ]
111
+ # Sensor-related callbacks for `G90Sensor` class - despite that class
112
+ # stores them, the invication is done by the `G90Alarm` class hence these
113
+ # are defined here
114
+ SensorStateCallback = Union[
115
+ Callable[[bool], None],
116
+ Callable[[bool], Coroutine[None, None, None]]
117
+ ]
118
+ SensorLowBatteryCallback = Union[
119
+ Callable[[], None],
120
+ Callable[[], Coroutine[None, None, None]]
121
+ ]
122
122
 
123
123
 
124
124
  # pylint: disable=too-many-public-methods
@@ -470,16 +470,19 @@ class G90Alarm(G90DeviceNotifications):
470
470
  _LOGGER.debug('on_sensor_activity: %s %s %s', idx, name, occupancy)
471
471
  sensor = await self.find_sensor(idx, name)
472
472
  if sensor:
473
- _LOGGER.debug('Setting occupancy to %s (previously %s)',
474
- occupancy, sensor.occupancy)
475
- sensor.occupancy = occupancy
473
+ # Reset the low battery flag since the sensor reports activity,
474
+ # implying it has sufficient battery power
475
+ # pylint: disable=protected-access
476
+ sensor._set_low_battery(False)
477
+ # Set the sensor occupancy
478
+ # pylint: disable=protected-access
479
+ sensor._set_occupancy(occupancy)
476
480
 
477
481
  # Emulate turning off the occupancy - most of sensors will not
478
482
  # notify the device of that, nor the device would emit such
479
483
  # notification itself
480
484
  def reset_sensor_occupancy(sensor: G90Sensor) -> None:
481
- _LOGGER.debug('Resetting occupancy for sensor %s', sensor)
482
- sensor.occupancy = False
485
+ sensor._set_occupancy(False)
483
486
  G90Callback.invoke(sensor.state_callback, sensor.occupancy)
484
487
 
485
488
  # Determine if door close notifications are available for the given
@@ -639,8 +642,12 @@ class G90Alarm(G90DeviceNotifications):
639
642
  :param event_id: Index of the sensor triggered alarm
640
643
  :param zone_name: Sensor name
641
644
  """
645
+ _LOGGER.debug('on_low_battery: %s %s', event_id, zone_name)
642
646
  sensor = await self.find_sensor(event_id, zone_name)
643
647
  if sensor:
648
+ # Set the low battery flag on the sensor
649
+ # pylint: disable=protected-access
650
+ sensor._set_low_battery(True)
644
651
  # Invoke per-sensor callback if provided
645
652
  G90Callback.invoke(sensor.low_battery_callback)
646
653
 
@@ -185,6 +185,7 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
185
185
  self._occupancy = False
186
186
  self._state_callback: Optional[SensorStateCallback] = None
187
187
  self._low_battery_callback: Optional[SensorLowBatteryCallback] = None
188
+ self._low_battery = False
188
189
  self._proto_idx = proto_idx
189
190
  self._extra_data: Any = None
190
191
 
@@ -247,8 +248,19 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
247
248
  """
248
249
  return self._occupancy
249
250
 
250
- @occupancy.setter
251
- def occupancy(self, value: bool) -> None:
251
+ def _set_occupancy(self, value: bool) -> None:
252
+ """
253
+ Sets occupancy state of the sensor.
254
+ Intentionally private, as occupancy state is derived from
255
+ notifications/alerts.
256
+
257
+ :param value: Occupancy state
258
+ """
259
+ _LOGGER.debug(
260
+ "Setting occupancy for sensor index=%s: '%s' %s"
261
+ " (previous value: %s)",
262
+ self.index, self.name, value, self._occupancy
263
+ )
252
264
  self._occupancy = value
253
265
 
254
266
  @property
@@ -342,6 +354,35 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
342
354
  """
343
355
  return self._definition is not None
344
356
 
357
+ @property
358
+ def is_wireless(self) -> bool:
359
+ """
360
+ Indicates if the sensor is wireless.
361
+ """
362
+ return self.protocol not in (G90SensorProtocols.CORD,)
363
+
364
+ @property
365
+ def is_low_battery(self) -> bool:
366
+ """
367
+ Indicates if the sensor is reporting low battery.
368
+ """
369
+ return self._low_battery
370
+
371
+ def _set_low_battery(self, value: bool) -> None:
372
+ """
373
+ Sets low battery state of the sensor.
374
+ Intentionally private, as low battery state is derived from
375
+ notifications/alerts.
376
+
377
+ :param value: Low battery state
378
+ """
379
+ _LOGGER.debug(
380
+ "Setting low battery for sensor index=%s '%s': %s"
381
+ " (previous value: %s)",
382
+ self.index, self.name, value, self._low_battery
383
+ )
384
+ self._low_battery = value
385
+
345
386
  @property
346
387
  def enabled(self) -> bool:
347
388
  """
@@ -485,6 +526,8 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
485
526
  'extra_data': self.extra_data,
486
527
  'enabled': self.enabled,
487
528
  'supports_enable_disable': self.supports_enable_disable,
529
+ 'is_wireless': self.is_wireless,
530
+ 'is_low_battery': self.is_low_battery,
488
531
  }
489
532
 
490
533
  def __repr__(self) -> str:
@@ -493,11 +536,4 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
493
536
 
494
537
  :return: String representation
495
538
  """
496
- return super().__repr__() + f"(name='{str(self.name)}'" \
497
- f' type={str(self.type)}' \
498
- f' subtype={str(self.subtype)}' \
499
- f' protocol={str(self.protocol)}' \
500
- f' occupancy={self.occupancy}' \
501
- f' user flag={str(self.user_flag)}' \
502
- f' reserved={str(self.reserved)}' \
503
- f" extra_data={str(self.extra_data)})"
539
+ return super().__repr__() + f'({repr(self._asdict())})'
@@ -211,9 +211,4 @@ class G90History:
211
211
  """
212
212
  Textural representation of the history entry.
213
213
  """
214
- return f'type={repr(self.type)}' \
215
- + f' source={repr(self.source)}' \
216
- + f' state={repr(self.state)}' \
217
- + f' sensor_name={self.sensor_name}' \
218
- + f' sensor_idx={self.sensor_idx}' \
219
- + f' datetime={repr(self.datetime)}'
214
+ return super().__repr__() + f'({repr(self._asdict())})'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyg90alarm
3
- Version: 1.14.0
3
+ Version: 1.15.1
4
4
  Summary: G90 Alarm system protocol
5
5
  Home-page: https://github.com/hostcc/pyg90alarm
6
6
  Author: Ilia Sotnikov
@@ -211,8 +211,10 @@ async def test_single_sensor(mock_device: DeviceMock) -> None:
211
211
 
212
212
  @pytest.mark.g90device(sent_data=[
213
213
  b'ISTART[102,'
214
- b'[[2,1,2],["Remote 1",10,0,10,1,0,32,0,0,16,1,0,""],'
215
- b'["Remote 2",11,0,10,1,0,32,0,0,16,1,0,""]]]IEND\0',
214
+ b'[[3,1,3],["Remote 1",10,0,10,1,0,32,0,0,16,1,0,""],'
215
+ b'["Remote 2",11,0,10,1,0,32,0,0,16,1,0,""],'
216
+ b'["Cord 1",12,0,126,1,0,32,0,5,16,1,0,""]'
217
+ b']]IEND\0',
216
218
  ])
217
219
  async def test_multiple_sensors_shorter_than_page(
218
220
  mock_device: DeviceMock
@@ -230,14 +232,20 @@ async def test_multiple_sensors_shorter_than_page(
230
232
  assert mock_device.recv_data == [
231
233
  b'ISTART[102,102,[102,[1,10]]]IEND\0',
232
234
  ]
233
- assert len(sensors) == 2
235
+ assert len(sensors) == 3
234
236
  assert isinstance(sensors, list)
235
237
  assert isinstance(sensors[0], G90Sensor)
236
238
  assert sensors[0].name == 'Remote 1'
237
239
  assert sensors[0].index == 10
240
+ assert sensors[0].is_wireless is True
238
241
  assert isinstance(sensors[1], G90Sensor)
239
242
  assert sensors[1].name == 'Remote 2'
240
243
  assert sensors[1].index == 11
244
+ assert sensors[1].is_wireless is True
245
+ assert isinstance(sensors[2], G90Sensor)
246
+ assert sensors[2].name == 'Cord 1'
247
+ assert sensors[2].index == 12
248
+ assert sensors[2].is_wireless is False
241
249
 
242
250
 
243
251
  @pytest.mark.g90device(sent_data=[
@@ -334,7 +342,9 @@ async def test_sensor_event(mock_device: DeviceMock) -> None:
334
342
  b'ISTART[117,[256]]IEND\0',
335
343
  ],
336
344
  notification_data=[
337
- b'[208,[4,26,1,4,"Remote","DUMMYGUID",1719223959,0,[""]]]\0'
345
+ b'[208,[4,26,1,4,"Remote","DUMMYGUID",1719223959,0,[""]]]\0',
346
+ # Simulate sensor activity, which should reset low battery state for it
347
+ b'[170,[5,[26,"Remote"]]]\0',
338
348
  ]
339
349
  )
340
350
  async def test_sensor_low_battery_event(mock_device: DeviceMock) -> None:
@@ -360,9 +370,22 @@ async def test_sensor_low_battery_event(mock_device: DeviceMock) -> None:
360
370
  await g90.listen_device_notifications()
361
371
  await mock_device.send_next_notification()
362
372
  await asyncio.wait([future], timeout=0.1)
363
- g90.close_device_notifications()
373
+
364
374
  low_battery_sensor_cb.assert_called_once_with()
365
375
  low_battery_cb.assert_called_once_with(26, 'Remote')
376
+ # Verify the low battery state is set upon receiving the notification
377
+ assert sensor[0].is_low_battery is True
378
+
379
+ # Signal the second notification is ready, the future has to be re-created
380
+ # as the corresponding callback will be fired again
381
+ future = asyncio.get_running_loop().create_future()
382
+ await mock_device.send_next_notification()
383
+ await asyncio.wait([future], timeout=0.1)
384
+
385
+ # Verify the low battery state is reset upon sensor activity
386
+ assert sensor[0].is_low_battery is False
387
+
388
+ g90.close_device_notifications()
366
389
 
367
390
 
368
391
  @pytest.mark.g90device(
@@ -14,22 +14,8 @@ isolated_build = true
14
14
 
15
15
  [testenv]
16
16
  deps =
17
- check-manifest == 0.49
18
- flake8 == 7.1.1
19
- pytest == 8.3.2
20
- pytest-asyncio == 0.23.8
21
- pytest-cov == 5.0.0
22
- pylint == 3.2.6
23
- mypy[reports] == 1.11.2
17
+ -r requirements_dev.txt
24
18
 
25
- setenv =
26
- # Ensure the module under test will be found under `src/` directory, in
27
- # case of any test command below will attempt importing it. In particular,
28
- # it helps `coverage` to recognize test traces from the module under `src/`
29
- # directory and report correct (aligned with repository layout) paths, not
30
- # from the module installed by `tox` in the virtual environment (the traces
31
- # will be referencing `tox` specific paths, not aligned with repository)
32
- PYTHONPATH = src
33
19
  allowlist_externals =
34
20
  cat
35
21
  commands =
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