pyg90alarm 1.14.0__tar.gz → 1.15.0__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.14.0 → pyg90alarm-1.15.0}/MANIFEST.in +2 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/PKG-INFO +1 -1
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/pyproject.toml +1 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/alarm.py +13 -9
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/entities/sensor.py +46 -10
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/history.py +1 -6
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm.egg-info/PKG-INFO +1 -1
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/tests/test_alarm.py +28 -5
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/tox.ini +1 -15
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/.github/CODEOWNERS +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/.github/workflows/main.yml +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/.gitignore +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/.pylintrc +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/.readthedocs.yaml +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/LICENSE +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/README.rst +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/docs/.DS_Store +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/docs/.gitignore +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/docs/api-docs.rst +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/docs/conf.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/docs/index.rst +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/docs/protocol.rst +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/docs/requirements.txt +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/setup.cfg +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/setup.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/sonar-project.properties +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/__init__.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/base_cmd.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/callback.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/config.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/const.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/definitions/__init__.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/definitions/sensors.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/device_notifications.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/discovery.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/entities/__init__.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/entities/device.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/exceptions.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/host_info.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/host_status.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/paginated_cmd.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/paginated_result.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/py.typed +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/targeted_discovery.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm/user_data_crc.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm.egg-info/SOURCES.txt +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm.egg-info/dependency_links.txt +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm.egg-info/requires.txt +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/src/pyg90alarm.egg-info/top_level.txt +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/tests/__init__.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/tests/conftest.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/tests/device_mock.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/tests/test_base_commands.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/tests/test_discovery.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/tests/test_notifications.py +0 -0
- {pyg90alarm-1.14.0 → pyg90alarm-1.15.0}/tests/test_paginated_commands.py +0 -0
|
@@ -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
|
|
91
|
+
AlarmCallback = Union[
|
|
95
92
|
Callable[[int, str, Any], None],
|
|
96
93
|
Callable[[int, str, Any], Coroutine[None, None, None]]
|
|
97
94
|
]
|
|
@@ -470,16 +467,19 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
470
467
|
_LOGGER.debug('on_sensor_activity: %s %s %s', idx, name, occupancy)
|
|
471
468
|
sensor = await self.find_sensor(idx, name)
|
|
472
469
|
if sensor:
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
470
|
+
# Reset the low battery flag since the sensor reports activity,
|
|
471
|
+
# implying it has sufficient battery power
|
|
472
|
+
# pylint: disable=protected-access
|
|
473
|
+
sensor._set_low_battery(False)
|
|
474
|
+
# Set the sensor occupancy
|
|
475
|
+
# pylint: disable=protected-access
|
|
476
|
+
sensor._set_occupancy(occupancy)
|
|
476
477
|
|
|
477
478
|
# Emulate turning off the occupancy - most of sensors will not
|
|
478
479
|
# notify the device of that, nor the device would emit such
|
|
479
480
|
# notification itself
|
|
480
481
|
def reset_sensor_occupancy(sensor: G90Sensor) -> None:
|
|
481
|
-
|
|
482
|
-
sensor.occupancy = False
|
|
482
|
+
sensor._set_occupancy(False)
|
|
483
483
|
G90Callback.invoke(sensor.state_callback, sensor.occupancy)
|
|
484
484
|
|
|
485
485
|
# Determine if door close notifications are available for the given
|
|
@@ -639,8 +639,12 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
639
639
|
:param event_id: Index of the sensor triggered alarm
|
|
640
640
|
:param zone_name: Sensor name
|
|
641
641
|
"""
|
|
642
|
+
_LOGGER.debug('on_low_battery: %s %s', event_id, zone_name)
|
|
642
643
|
sensor = await self.find_sensor(event_id, zone_name)
|
|
643
644
|
if sensor:
|
|
645
|
+
# Set the low battery flag on the sensor
|
|
646
|
+
# pylint: disable=protected-access
|
|
647
|
+
sensor._set_low_battery(True)
|
|
644
648
|
# Invoke per-sensor callback if provided
|
|
645
649
|
G90Callback.invoke(sensor.low_battery_callback)
|
|
646
650
|
|
|
@@ -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
|
-
|
|
251
|
-
|
|
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
|
|
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'
|
|
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())})'
|
|
@@ -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'[[
|
|
215
|
-
b'["Remote 2",11,0,10,1,0,32,0,0,16,1,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) ==
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|