pyg90alarm 1.17.1__tar.gz → 1.18.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.17.1 → pyg90alarm-1.18.0}/.github/workflows/main.yml +4 -2
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/PKG-INFO +1 -1
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/alarm.py +155 -39
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/const.py +1 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/device_notifications.py +29 -3
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/entities/sensor.py +96 -1
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm.egg-info/PKG-INFO +1 -1
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/tests/test_alarm.py +191 -4
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/tests/test_notifications.py +51 -1
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/tox.ini +1 -6
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/.github/CODEOWNERS +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/.gitignore +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/.pylintrc +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/.readthedocs.yaml +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/LICENSE +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/MANIFEST.in +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/README.rst +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/docs/.DS_Store +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/docs/.gitignore +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/docs/api-docs.rst +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/docs/conf.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/docs/index.rst +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/docs/protocol.rst +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/docs/requirements.txt +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/pyproject.toml +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/setup.cfg +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/setup.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/sonar-project.properties +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/__init__.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/base_cmd.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/callback.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/config.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/definitions/__init__.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/definitions/sensors.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/discovery.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/entities/__init__.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/entities/device.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/exceptions.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/history.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/host_info.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/host_status.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/paginated_cmd.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/paginated_result.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/py.typed +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/targeted_discovery.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm/user_data_crc.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm.egg-info/SOURCES.txt +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm.egg-info/dependency_links.txt +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm.egg-info/requires.txt +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/src/pyg90alarm.egg-info/top_level.txt +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/tests/__init__.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/tests/conftest.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/tests/device_mock.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/tests/test_base_commands.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/tests/test_discovery.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/tests/test_history.py +0 -0
- {pyg90alarm-1.17.1 → pyg90alarm-1.18.0}/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
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
19
|
# SOFTWARE.
|
|
20
|
+
# pylint: disable=too-many-lines
|
|
20
21
|
|
|
21
22
|
"""
|
|
22
23
|
Provides interface to G90 alarm panel.
|
|
@@ -120,6 +121,14 @@ if TYPE_CHECKING:
|
|
|
120
121
|
[int, str, G90RemoteButtonStates], Coroutine[None, None, None]
|
|
121
122
|
]
|
|
122
123
|
]
|
|
124
|
+
DoorOpenWhenArmingCallback = Union[
|
|
125
|
+
Callable[[int, str], None],
|
|
126
|
+
Callable[[int, str], Coroutine[None, None, None]]
|
|
127
|
+
]
|
|
128
|
+
TamperCallback = Union[
|
|
129
|
+
Callable[[int, str], None],
|
|
130
|
+
Callable[[int, str], Coroutine[None, None, None]]
|
|
131
|
+
]
|
|
123
132
|
# Sensor-related callbacks for `G90Sensor` class - despite that class
|
|
124
133
|
# stores them, the invocation is done by the `G90Alarm` class hence these
|
|
125
134
|
# are defined here
|
|
@@ -131,6 +140,14 @@ if TYPE_CHECKING:
|
|
|
131
140
|
Callable[[], None],
|
|
132
141
|
Callable[[], Coroutine[None, None, None]]
|
|
133
142
|
]
|
|
143
|
+
SensorDoorOpenWhenArmingCallback = Union[
|
|
144
|
+
Callable[[], None],
|
|
145
|
+
Callable[[], Coroutine[None, None, None]]
|
|
146
|
+
]
|
|
147
|
+
SensorTamperCallback = Union[
|
|
148
|
+
Callable[[], None],
|
|
149
|
+
Callable[[], Coroutine[None, None, None]]
|
|
150
|
+
]
|
|
134
151
|
|
|
135
152
|
|
|
136
153
|
# pylint: disable=too-many-public-methods
|
|
@@ -161,7 +178,9 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
161
178
|
self._host: str = host
|
|
162
179
|
self._port: int = port
|
|
163
180
|
self._sensors: List[G90Sensor] = []
|
|
181
|
+
self._sensors_lock = asyncio.Lock()
|
|
164
182
|
self._devices: List[G90Device] = []
|
|
183
|
+
self._devices_lock = asyncio.Lock()
|
|
165
184
|
self._notifications: Optional[G90DeviceNotifications] = None
|
|
166
185
|
self._sensor_cb: Optional[SensorCallback] = None
|
|
167
186
|
self._armdisarm_cb: Optional[ArmDisarmCallback] = None
|
|
@@ -172,6 +191,10 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
172
191
|
self._remote_button_press_cb: Optional[
|
|
173
192
|
RemoteButtonPressCallback
|
|
174
193
|
] = None
|
|
194
|
+
self._door_open_when_arming_cb: Optional[
|
|
195
|
+
DoorOpenWhenArmingCallback
|
|
196
|
+
] = None
|
|
197
|
+
self._tamper_cb: Optional[TamperCallback] = None
|
|
175
198
|
self._reset_occupancy_interval = reset_occupancy_interval
|
|
176
199
|
self._alert_config: Optional[G90AlertConfigFlags] = None
|
|
177
200
|
self._sms_alert_when_armed = False
|
|
@@ -258,20 +281,26 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
258
281
|
|
|
259
282
|
:return: List of sensors
|
|
260
283
|
"""
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
proto_idx=sensor.proto_idx
|
|
284
|
+
# Use lock around the operation, to ensure no duplicated entries in the
|
|
285
|
+
# resulting list or redundant exchanges with panel are made when the
|
|
286
|
+
# method is called concurrently
|
|
287
|
+
async with self._sensors_lock:
|
|
288
|
+
if not self._sensors:
|
|
289
|
+
sensors = self.paginated_result(
|
|
290
|
+
G90Commands.GETSENSORLIST
|
|
269
291
|
)
|
|
270
|
-
|
|
292
|
+
async for sensor in sensors:
|
|
293
|
+
obj = G90Sensor(
|
|
294
|
+
*sensor.data, parent=self, subindex=0,
|
|
295
|
+
proto_idx=sensor.proto_idx
|
|
296
|
+
)
|
|
297
|
+
self._sensors.append(obj)
|
|
271
298
|
|
|
272
|
-
|
|
299
|
+
_LOGGER.debug(
|
|
300
|
+
'Total number of sensors: %s', len(self._sensors)
|
|
301
|
+
)
|
|
273
302
|
|
|
274
|
-
|
|
303
|
+
return self._sensors
|
|
275
304
|
|
|
276
305
|
async def find_sensor(self, idx: int, name: str) -> Optional[G90Sensor]:
|
|
277
306
|
"""
|
|
@@ -316,28 +345,32 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
316
345
|
|
|
317
346
|
:return: List of devices
|
|
318
347
|
"""
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
obj = G90Device(
|
|
325
|
-
*device.data, parent=self, subindex=0,
|
|
326
|
-
proto_idx=device.proto_idx
|
|
348
|
+
# See `get_sensors` method for the rationale behind the lock usage
|
|
349
|
+
async with self._devices_lock:
|
|
350
|
+
if not self._devices:
|
|
351
|
+
devices = self.paginated_result(
|
|
352
|
+
G90Commands.GETDEVICELIST
|
|
327
353
|
)
|
|
328
|
-
|
|
329
|
-
# Multi-node devices (first node has already been added
|
|
330
|
-
# above
|
|
331
|
-
for node in range(1, obj.node_count):
|
|
354
|
+
async for device in devices:
|
|
332
355
|
obj = G90Device(
|
|
333
|
-
*device.data, parent=self,
|
|
334
|
-
|
|
356
|
+
*device.data, parent=self, subindex=0,
|
|
357
|
+
proto_idx=device.proto_idx
|
|
335
358
|
)
|
|
336
359
|
self._devices.append(obj)
|
|
360
|
+
# Multi-node devices (first node has already been added
|
|
361
|
+
# above
|
|
362
|
+
for node in range(1, obj.node_count):
|
|
363
|
+
obj = G90Device(
|
|
364
|
+
*device.data, parent=self,
|
|
365
|
+
subindex=node, proto_idx=device.proto_idx
|
|
366
|
+
)
|
|
367
|
+
self._devices.append(obj)
|
|
337
368
|
|
|
338
|
-
|
|
369
|
+
_LOGGER.debug(
|
|
370
|
+
'Total number of devices: %s', len(self._devices)
|
|
371
|
+
)
|
|
339
372
|
|
|
340
|
-
|
|
373
|
+
return self._devices
|
|
341
374
|
|
|
342
375
|
@property
|
|
343
376
|
async def host_info(self) -> G90HostInfo:
|
|
@@ -601,6 +634,17 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
601
634
|
await self.set_alert_config(
|
|
602
635
|
await self.alert_config | G90AlertConfigFlags.SMS_PUSH
|
|
603
636
|
)
|
|
637
|
+
|
|
638
|
+
# Reset the tampered and door open when arming flags on all sensors
|
|
639
|
+
# having those set
|
|
640
|
+
for sensor in await self.get_sensors():
|
|
641
|
+
if sensor.is_tampered:
|
|
642
|
+
# pylint: disable=protected-access
|
|
643
|
+
sensor._set_tampered(False)
|
|
644
|
+
if sensor.is_door_open_when_arming:
|
|
645
|
+
# pylint: disable=protected-access
|
|
646
|
+
sensor._set_door_open_when_arming(False)
|
|
647
|
+
|
|
604
648
|
G90Callback.invoke(self._armdisarm_cb, state)
|
|
605
649
|
|
|
606
650
|
@property
|
|
@@ -615,7 +659,9 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
615
659
|
def armdisarm_callback(self, value: ArmDisarmCallback) -> None:
|
|
616
660
|
self._armdisarm_cb = value
|
|
617
661
|
|
|
618
|
-
async def on_alarm(
|
|
662
|
+
async def on_alarm(
|
|
663
|
+
self, event_id: int, zone_name: str, is_tampered: bool
|
|
664
|
+
) -> None:
|
|
619
665
|
"""
|
|
620
666
|
Invoked when alarm is triggered. Fires corresponding callback if set by
|
|
621
667
|
the user with :attr:`.alarm_callback`.
|
|
@@ -626,16 +672,31 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
626
672
|
:param zone_name: Sensor name
|
|
627
673
|
"""
|
|
628
674
|
sensor = await self.find_sensor(event_id, zone_name)
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
675
|
+
extra_data = None
|
|
676
|
+
if sensor:
|
|
677
|
+
# The callback is still delivered to the caller even if the sensor
|
|
678
|
+
# isn't found, only `extra_data` is skipped. That is to ensure the
|
|
679
|
+
# important callback isn't filtered
|
|
680
|
+
extra_data = sensor.extra_data
|
|
681
|
+
|
|
682
|
+
# Invoke the sensor activity callback to set the sensor occupancy
|
|
683
|
+
# if sensor is known, but only if that isn't already set - it helps
|
|
684
|
+
# when device notifications on triggerring sensor's activity aren't
|
|
685
|
+
# receveid by a reason
|
|
686
|
+
if not sensor.occupancy:
|
|
687
|
+
await self.on_sensor_activity(event_id, zone_name, True)
|
|
688
|
+
|
|
689
|
+
if is_tampered:
|
|
690
|
+
# Set the tampered flag on the sensor
|
|
691
|
+
# pylint: disable=protected-access
|
|
692
|
+
sensor._set_tampered(True)
|
|
693
|
+
|
|
694
|
+
# Invoke per-sensor callback if provided
|
|
695
|
+
G90Callback.invoke(sensor.tamper_callback)
|
|
696
|
+
|
|
697
|
+
# Invoke global tamper callback if provided and the sensor is tampered
|
|
698
|
+
if is_tampered:
|
|
699
|
+
G90Callback.invoke(self._tamper_cb, event_id, zone_name)
|
|
639
700
|
|
|
640
701
|
G90Callback.invoke(
|
|
641
702
|
self._alarm_cb, event_id, zone_name, extra_data
|
|
@@ -706,7 +767,10 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
706
767
|
|
|
707
768
|
# Also report the event as alarm for unification, hard-coding the
|
|
708
769
|
# sensor name in case of host SOS
|
|
709
|
-
await self.on_alarm(
|
|
770
|
+
await self.on_alarm(
|
|
771
|
+
event_id, zone_name='Host SOS' if is_host_sos else zone_name,
|
|
772
|
+
is_tampered=False
|
|
773
|
+
)
|
|
710
774
|
|
|
711
775
|
if not is_host_sos:
|
|
712
776
|
# Also report the remote button press for SOS - the panel will not
|
|
@@ -766,6 +830,58 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
766
830
|
) -> None:
|
|
767
831
|
self._remote_button_press_cb = value
|
|
768
832
|
|
|
833
|
+
async def on_door_open_when_arming(
|
|
834
|
+
self, event_id: int, zone_name: str
|
|
835
|
+
) -> None:
|
|
836
|
+
"""
|
|
837
|
+
Invoked when door is open when arming the device. Fires corresponding
|
|
838
|
+
callback if set by the user with
|
|
839
|
+
:attr:`.door_open_when_arming_callback`.
|
|
840
|
+
|
|
841
|
+
Please note the method is for internal use by the class.
|
|
842
|
+
|
|
843
|
+
:param event_id: The index of the sensor being active when the panel
|
|
844
|
+
is being armed.
|
|
845
|
+
:param zone_name: The name of the sensor
|
|
846
|
+
"""
|
|
847
|
+
_LOGGER.debug('on_door_open_when_arming: %s %s', event_id, zone_name)
|
|
848
|
+
sensor = await self.find_sensor(event_id, zone_name)
|
|
849
|
+
if sensor:
|
|
850
|
+
# Set the low battery flag on the sensor
|
|
851
|
+
# pylint: disable=protected-access
|
|
852
|
+
sensor._set_door_open_when_arming(True)
|
|
853
|
+
# Invoke per-sensor callback if provided
|
|
854
|
+
G90Callback.invoke(sensor.door_open_when_arming_callback)
|
|
855
|
+
|
|
856
|
+
G90Callback.invoke(self._door_open_when_arming_cb, event_id, zone_name)
|
|
857
|
+
|
|
858
|
+
@property
|
|
859
|
+
def door_open_when_arming_callback(
|
|
860
|
+
self
|
|
861
|
+
) -> Optional[DoorOpenWhenArmingCallback]:
|
|
862
|
+
"""
|
|
863
|
+
Door open when arming callback, which is invoked when sensor reports
|
|
864
|
+
the condition.
|
|
865
|
+
"""
|
|
866
|
+
return self._door_open_when_arming_cb
|
|
867
|
+
|
|
868
|
+
@door_open_when_arming_callback.setter
|
|
869
|
+
def door_open_when_arming_callback(
|
|
870
|
+
self, value: DoorOpenWhenArmingCallback
|
|
871
|
+
) -> None:
|
|
872
|
+
self._door_open_when_arming_cb = value
|
|
873
|
+
|
|
874
|
+
@property
|
|
875
|
+
async def tamper_callback(self) -> Optional[TamperCallback]:
|
|
876
|
+
"""
|
|
877
|
+
Tamper callback, which is invoked when sensor reports the condition.
|
|
878
|
+
"""
|
|
879
|
+
return self._tamper_cb
|
|
880
|
+
|
|
881
|
+
@tamper_callback.setter
|
|
882
|
+
def tamper_callback(self, value: TamperCallback) -> None:
|
|
883
|
+
self._tamper_cb = value
|
|
884
|
+
|
|
769
885
|
async def listen_device_notifications(self) -> None:
|
|
770
886
|
"""
|
|
771
887
|
Starts internal listener for device notifications/alerts.
|
|
@@ -159,6 +159,16 @@ class G90DeviceNotifications(DatagramProtocol):
|
|
|
159
159
|
|
|
160
160
|
return
|
|
161
161
|
|
|
162
|
+
# An open door is detected when arming
|
|
163
|
+
if notification.kind == G90NotificationTypes.DOOR_OPEN_WHEN_ARMING:
|
|
164
|
+
g90_zone_info = G90ZoneInfo(*notification.data)
|
|
165
|
+
_LOGGER.debug('Door open detected when arming: %s', g90_zone_info)
|
|
166
|
+
G90Callback.invoke(
|
|
167
|
+
self.on_door_open_when_arming,
|
|
168
|
+
g90_zone_info.idx, g90_zone_info.name
|
|
169
|
+
)
|
|
170
|
+
return
|
|
171
|
+
|
|
162
172
|
_LOGGER.warning('Unknown notification received from %s:%s:'
|
|
163
173
|
' kind %s, data %s',
|
|
164
174
|
addr[0], addr[1], notification.kind, notification.data)
|
|
@@ -259,11 +269,15 @@ class G90DeviceNotifications(DatagramProtocol):
|
|
|
259
269
|
)
|
|
260
270
|
# Regular alarm
|
|
261
271
|
else:
|
|
262
|
-
|
|
272
|
+
is_tampered = alert.state == G90AlertStates.TAMPER
|
|
273
|
+
_LOGGER.debug(
|
|
274
|
+
'Alarm: %s, is tampered: %s', alert.zone_name, is_tampered
|
|
275
|
+
)
|
|
263
276
|
G90Callback.invoke(
|
|
264
277
|
self.on_alarm,
|
|
265
|
-
alert.event_id, alert.zone_name
|
|
278
|
+
alert.event_id, alert.zone_name, is_tampered
|
|
266
279
|
)
|
|
280
|
+
|
|
267
281
|
handled = True
|
|
268
282
|
|
|
269
283
|
# Host SOS
|
|
@@ -372,6 +386,16 @@ class G90DeviceNotifications(DatagramProtocol):
|
|
|
372
386
|
:param name: Name of the sensor.
|
|
373
387
|
"""
|
|
374
388
|
|
|
389
|
+
async def on_door_open_when_arming(
|
|
390
|
+
self, event_id: int, zone_name: str
|
|
391
|
+
) -> None:
|
|
392
|
+
"""
|
|
393
|
+
Invoked when door open is detected when panel is armed.
|
|
394
|
+
|
|
395
|
+
:param event_id: Index of the sensor.
|
|
396
|
+
:param zone_name: Name of the sensor that reports door open.
|
|
397
|
+
"""
|
|
398
|
+
|
|
375
399
|
async def on_door_open_close(
|
|
376
400
|
self, event_id: int, zone_name: str, is_open: bool
|
|
377
401
|
) -> None:
|
|
@@ -391,7 +415,9 @@ class G90DeviceNotifications(DatagramProtocol):
|
|
|
391
415
|
:param zone_name: Name of the sensor that reports low battery.
|
|
392
416
|
"""
|
|
393
417
|
|
|
394
|
-
async def on_alarm(
|
|
418
|
+
async def on_alarm(
|
|
419
|
+
self, event_id: int, zone_name: str, is_tampered: bool
|
|
420
|
+
) -> None:
|
|
395
421
|
"""
|
|
396
422
|
Invoked when device triggers the alarm.
|
|
397
423
|
|
|
@@ -32,7 +32,10 @@ from enum import IntEnum, IntFlag
|
|
|
32
32
|
from ..definitions.sensors import SENSOR_DEFINITIONS, SensorDefinition
|
|
33
33
|
from ..const import G90Commands
|
|
34
34
|
if TYPE_CHECKING:
|
|
35
|
-
from ..alarm import
|
|
35
|
+
from ..alarm import (
|
|
36
|
+
G90Alarm, SensorStateCallback, SensorLowBatteryCallback,
|
|
37
|
+
SensorDoorOpenWhenArmingCallback, SensorTamperCallback,
|
|
38
|
+
)
|
|
36
39
|
|
|
37
40
|
|
|
38
41
|
@dataclass
|
|
@@ -157,6 +160,7 @@ class G90SensorTypes(IntEnum):
|
|
|
157
160
|
_LOGGER = logging.getLogger(__name__)
|
|
158
161
|
|
|
159
162
|
|
|
163
|
+
# pylint: disable=too-many-public-methods
|
|
160
164
|
class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
161
165
|
"""
|
|
162
166
|
Interacts with sensor on G90 alarm panel.
|
|
@@ -186,6 +190,12 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
186
190
|
self._state_callback: Optional[SensorStateCallback] = None
|
|
187
191
|
self._low_battery_callback: Optional[SensorLowBatteryCallback] = None
|
|
188
192
|
self._low_battery = False
|
|
193
|
+
self._tampered = False
|
|
194
|
+
self._door_open_when_arming_callback: Optional[
|
|
195
|
+
SensorDoorOpenWhenArmingCallback
|
|
196
|
+
] = None
|
|
197
|
+
self._tamper_callback: Optional[SensorTamperCallback] = None
|
|
198
|
+
self._door_open_when_arming = False
|
|
189
199
|
self._proto_idx = proto_idx
|
|
190
200
|
self._extra_data: Any = None
|
|
191
201
|
|
|
@@ -238,6 +248,37 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
238
248
|
def low_battery_callback(self, value: SensorLowBatteryCallback) -> None:
|
|
239
249
|
self._low_battery_callback = value
|
|
240
250
|
|
|
251
|
+
@property
|
|
252
|
+
def door_open_when_arming_callback(
|
|
253
|
+
self
|
|
254
|
+
) -> Optional[SensorDoorOpenWhenArmingCallback]:
|
|
255
|
+
"""
|
|
256
|
+
Callback that is invoked when the sensor reports on open door
|
|
257
|
+
condition when arming.
|
|
258
|
+
|
|
259
|
+
:return: Sensor's door open when arming callback
|
|
260
|
+
"""
|
|
261
|
+
return self._door_open_when_arming_callback
|
|
262
|
+
|
|
263
|
+
@door_open_when_arming_callback.setter
|
|
264
|
+
def door_open_when_arming_callback(
|
|
265
|
+
self, value: SensorDoorOpenWhenArmingCallback
|
|
266
|
+
) -> None:
|
|
267
|
+
self._door_open_when_arming_callback = value
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def tamper_callback(self) -> Optional[SensorTamperCallback]:
|
|
271
|
+
"""
|
|
272
|
+
Callback that is invoked when the sensor reports being tampered.
|
|
273
|
+
|
|
274
|
+
:return: Sensor's tamper callback
|
|
275
|
+
"""
|
|
276
|
+
return self._tamper_callback
|
|
277
|
+
|
|
278
|
+
@tamper_callback.setter
|
|
279
|
+
def tamper_callback(self, value: SensorTamperCallback) -> None:
|
|
280
|
+
self._tamper_callback = value
|
|
281
|
+
|
|
241
282
|
@property
|
|
242
283
|
def occupancy(self) -> bool:
|
|
243
284
|
"""
|
|
@@ -365,12 +406,16 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
365
406
|
def is_low_battery(self) -> bool:
|
|
366
407
|
"""
|
|
367
408
|
Indicates if the sensor is reporting low battery.
|
|
409
|
+
|
|
410
|
+
The condition is cleared when the sensor reports activity (i.e. is no
|
|
411
|
+
longer low on battery as it is able to report the activity).
|
|
368
412
|
"""
|
|
369
413
|
return self._low_battery
|
|
370
414
|
|
|
371
415
|
def _set_low_battery(self, value: bool) -> None:
|
|
372
416
|
"""
|
|
373
417
|
Sets low battery state of the sensor.
|
|
418
|
+
|
|
374
419
|
Intentionally private, as low battery state is derived from
|
|
375
420
|
notifications/alerts.
|
|
376
421
|
|
|
@@ -383,6 +428,56 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
383
428
|
)
|
|
384
429
|
self._low_battery = value
|
|
385
430
|
|
|
431
|
+
@property
|
|
432
|
+
def is_tampered(self) -> bool:
|
|
433
|
+
"""
|
|
434
|
+
Indicates if the sensor has been tampered.
|
|
435
|
+
|
|
436
|
+
The condition is cleared when panel is armed/disarmed next time.
|
|
437
|
+
"""
|
|
438
|
+
return self._tampered
|
|
439
|
+
|
|
440
|
+
def _set_tampered(self, value: bool) -> None:
|
|
441
|
+
"""
|
|
442
|
+
Sets tamper state of the sensor.
|
|
443
|
+
|
|
444
|
+
Intentionally private, as tamper state is derived from
|
|
445
|
+
notifications/alerts.
|
|
446
|
+
|
|
447
|
+
:param value: Tamper state
|
|
448
|
+
"""
|
|
449
|
+
_LOGGER.debug(
|
|
450
|
+
"Setting tamper for sensor index=%s '%s': %s"
|
|
451
|
+
" (previous value: %s)",
|
|
452
|
+
self.index, self.name, value, self._tampered
|
|
453
|
+
)
|
|
454
|
+
self._tampered = value
|
|
455
|
+
|
|
456
|
+
@property
|
|
457
|
+
def is_door_open_when_arming(self) -> bool:
|
|
458
|
+
"""
|
|
459
|
+
Indicates if the sensor reports on open door when arming.
|
|
460
|
+
|
|
461
|
+
The condition is cleared when panel is armed/disarmed next time.
|
|
462
|
+
"""
|
|
463
|
+
return self._door_open_when_arming
|
|
464
|
+
|
|
465
|
+
def _set_door_open_when_arming(self, value: bool) -> None:
|
|
466
|
+
"""
|
|
467
|
+
Sets door open state of the sensor when arming.
|
|
468
|
+
|
|
469
|
+
Intentionally private, as door open state is derived from
|
|
470
|
+
notifications/alerts.
|
|
471
|
+
|
|
472
|
+
:param value: Door open state
|
|
473
|
+
"""
|
|
474
|
+
_LOGGER.debug(
|
|
475
|
+
"Setting door open when arming for sensor index=%s '%s': %s"
|
|
476
|
+
" (previous value: %s)",
|
|
477
|
+
self.index, self.name, value, self._door_open_when_arming
|
|
478
|
+
)
|
|
479
|
+
self._door_open_when_arming = value
|
|
480
|
+
|
|
386
481
|
@property
|
|
387
482
|
def enabled(self) -> bool:
|
|
388
483
|
"""
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Tests for G90Alarm class
|
|
3
3
|
"""
|
|
4
|
+
# pylint: disable=too-many-lines
|
|
5
|
+
|
|
4
6
|
import asyncio
|
|
7
|
+
from itertools import cycle
|
|
5
8
|
from unittest.mock import MagicMock
|
|
6
9
|
import pytest
|
|
7
10
|
|
|
@@ -128,6 +131,35 @@ async def test_devices(mock_device: DeviceMock) -> None:
|
|
|
128
131
|
assert isinstance(devices[0]._asdict(), dict)
|
|
129
132
|
|
|
130
133
|
|
|
134
|
+
# Provide an endless sequence of simulated panel responses for the devices
|
|
135
|
+
# list. Each attempt will simulate a single device. This sequence will prevent
|
|
136
|
+
# `G90TimeoutError` if the code under test initiates more exchanges with the
|
|
137
|
+
# panel than the simulated data contains.
|
|
138
|
+
@pytest.mark.g90device(sent_data=cycle([
|
|
139
|
+
b'ISTART[138,'
|
|
140
|
+
b'[[1,1,1],["Switch",10,0,10,1,0,32,0,0,16,1,0,""]]]IEND\0',
|
|
141
|
+
]))
|
|
142
|
+
async def test_get_devices_concurrent(mock_device: DeviceMock) -> None:
|
|
143
|
+
"""
|
|
144
|
+
Tests for concurrently retrieving list of devices produces consistent
|
|
145
|
+
results.
|
|
146
|
+
"""
|
|
147
|
+
g90 = G90Alarm(host=mock_device.host, port=mock_device.port)
|
|
148
|
+
g90.paginated_result = MagicMock( # type: ignore[method-assign]
|
|
149
|
+
spec=g90.paginated_result, wraps=g90.paginated_result
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Issue two concurrent requests to retrieve devices
|
|
153
|
+
res = await asyncio.gather(g90.get_devices(), g90.get_devices())
|
|
154
|
+
# Ensure only single exchange with the panel
|
|
155
|
+
g90.paginated_result.assert_called_once()
|
|
156
|
+
# While `pylint` demands use of generator, the comprehension is used
|
|
157
|
+
# instead for ease of trroubleshooting test failures as it will show the
|
|
158
|
+
# list elements, not just generator instance
|
|
159
|
+
# pylint: disable=use-a-generator
|
|
160
|
+
assert all([len(x) == 1 for x in res])
|
|
161
|
+
|
|
162
|
+
|
|
131
163
|
@pytest.mark.g90device(sent_data=[
|
|
132
164
|
b'ISTART[138,'
|
|
133
165
|
b'[[1,1,1],["Switch",10,0,10,1,0,32,0,0,16,2,0,""]]]IEND\0'
|
|
@@ -207,6 +239,27 @@ async def test_single_sensor(mock_device: DeviceMock) -> None:
|
|
|
207
239
|
assert isinstance(sensors[0]._asdict(), dict)
|
|
208
240
|
|
|
209
241
|
|
|
242
|
+
# See `test_get_devices_concurrent` for the explanation of the test
|
|
243
|
+
@pytest.mark.g90device(sent_data=cycle([
|
|
244
|
+
b'ISTART[102,'
|
|
245
|
+
b'[[1,1,1],["Remote",10,0,10,1,0,32,0,0,16,1,0,""]]]IEND\0',
|
|
246
|
+
]))
|
|
247
|
+
async def test_get_sensors_concurrent(mock_device: DeviceMock) -> None:
|
|
248
|
+
"""
|
|
249
|
+
Tests for concurrently retrieving list of sensors produces consistent
|
|
250
|
+
results.
|
|
251
|
+
"""
|
|
252
|
+
g90 = G90Alarm(host=mock_device.host, port=mock_device.port)
|
|
253
|
+
g90.paginated_result = MagicMock( # type: ignore[method-assign]
|
|
254
|
+
spec=g90.paginated_result, wraps=g90.paginated_result
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
res = await asyncio.gather(g90.get_sensors(), g90.get_sensors())
|
|
258
|
+
g90.paginated_result.assert_called_once()
|
|
259
|
+
# pylint: disable=use-a-generator
|
|
260
|
+
assert all([len(x) == 1 for x in res])
|
|
261
|
+
|
|
262
|
+
|
|
210
263
|
@pytest.mark.g90device(sent_data=[
|
|
211
264
|
b'ISTART[102,'
|
|
212
265
|
b'[[3,1,3],["Remote 1",10,0,10,1,0,32,0,0,16,1,0,""],'
|
|
@@ -387,6 +440,69 @@ async def test_sensor_low_battery_callback(mock_device: DeviceMock) -> None:
|
|
|
387
440
|
|
|
388
441
|
|
|
389
442
|
@pytest.mark.g90device(
|
|
443
|
+
sent_data=[
|
|
444
|
+
b'ISTART[102,'
|
|
445
|
+
b'[[1,1,1],["Hall",21,0,10,1,0,32,0,0,16,1,0,""]]]IEND\0',
|
|
446
|
+
],
|
|
447
|
+
notification_data=[
|
|
448
|
+
b'[170,[6,[21,"Hall"]]]\0',
|
|
449
|
+
b'[170,[1,[3]]]\0',
|
|
450
|
+
]
|
|
451
|
+
)
|
|
452
|
+
async def test_sensor_door_open_when_arming_callback(
|
|
453
|
+
mock_device: DeviceMock
|
|
454
|
+
) -> None:
|
|
455
|
+
"""
|
|
456
|
+
Tests for sensor door open when arming callback.
|
|
457
|
+
"""
|
|
458
|
+
g90 = G90Alarm(host=mock_device.host, port=mock_device.port,
|
|
459
|
+
notifications_local_host=mock_device.notification_host,
|
|
460
|
+
notifications_local_port=mock_device.notification_port)
|
|
461
|
+
|
|
462
|
+
sensors = await g90.get_sensors()
|
|
463
|
+
prop_sensors = await g90.sensors
|
|
464
|
+
|
|
465
|
+
assert sensors == prop_sensors
|
|
466
|
+
future = asyncio.get_running_loop().create_future()
|
|
467
|
+
sensor = [x for x in sensors if x.index == 21 and x.name == 'Hall']
|
|
468
|
+
door_open_when_arming_sensor_cb = MagicMock()
|
|
469
|
+
door_open_when_arming_sensor_cb.side_effect = (
|
|
470
|
+
lambda *args: future.set_result(True)
|
|
471
|
+
)
|
|
472
|
+
sensor[0].door_open_when_arming_callback = door_open_when_arming_sensor_cb
|
|
473
|
+
door_open_when_arming_cb = MagicMock()
|
|
474
|
+
g90.door_open_when_arming_callback = door_open_when_arming_cb
|
|
475
|
+
|
|
476
|
+
await g90.listen_device_notifications()
|
|
477
|
+
await mock_device.send_next_notification()
|
|
478
|
+
await asyncio.wait([future], timeout=0.1)
|
|
479
|
+
|
|
480
|
+
door_open_when_arming_sensor_cb.assert_called_once_with()
|
|
481
|
+
door_open_when_arming_cb.assert_called_once_with(21, 'Hall')
|
|
482
|
+
# Verify the door open when arming state is set upon receiving the
|
|
483
|
+
# notification
|
|
484
|
+
assert sensor[0].is_door_open_when_arming is True
|
|
485
|
+
|
|
486
|
+
# Signal the second notification is ready, the future has to be re-created
|
|
487
|
+
# as the corresponding callback will be fired again
|
|
488
|
+
future = asyncio.get_running_loop().create_future()
|
|
489
|
+
await mock_device.send_next_notification()
|
|
490
|
+
await asyncio.wait([future], timeout=0.1)
|
|
491
|
+
|
|
492
|
+
# Verify the door open when arming state is reset upon disarming
|
|
493
|
+
assert sensor[0].is_door_open_when_arming is False
|
|
494
|
+
|
|
495
|
+
g90.close_device_notifications()
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
@pytest.mark.g90device(
|
|
499
|
+
sent_data=[
|
|
500
|
+
b'ISTART[102,'
|
|
501
|
+
b'[[3,1,3],["Remote 1",10,0,10,1,0,32,0,0,16,1,0,""],'
|
|
502
|
+
b'["Remote 2",11,0,10,1,0,32,0,0,16,1,0,""],'
|
|
503
|
+
b'["Cord 1",12,0,126,1,0,32,0,5,16,1,0,""]'
|
|
504
|
+
b']]IEND\0',
|
|
505
|
+
],
|
|
390
506
|
notification_data=[
|
|
391
507
|
b'[170,[1,[1]]]\0'
|
|
392
508
|
]
|
|
@@ -529,6 +645,65 @@ async def test_alarm_callback(mock_device: DeviceMock) -> None:
|
|
|
529
645
|
g90.close_device_notifications()
|
|
530
646
|
|
|
531
647
|
|
|
648
|
+
@pytest.mark.g90device(
|
|
649
|
+
sent_data=[
|
|
650
|
+
b'ISTART[102,'
|
|
651
|
+
b'[[1,1,1],["Hall",100,0,1,1,0,32,0,0,16,1,0,""]]]IEND\0',
|
|
652
|
+
# Alert configuration, used by sensor activity callback invoked when
|
|
653
|
+
# handling alarm
|
|
654
|
+
b'ISTART[117,[256]]IEND\0',
|
|
655
|
+
],
|
|
656
|
+
notification_data=[
|
|
657
|
+
b'[208,[3,100,1,3,"Hall","DUMMYGUID",1630876128,0,[""]]]\0',
|
|
658
|
+
b'[170,[1,[3]]]\0',
|
|
659
|
+
]
|
|
660
|
+
)
|
|
661
|
+
async def test_sensor_tamper_callback(
|
|
662
|
+
mock_device: DeviceMock
|
|
663
|
+
) -> None:
|
|
664
|
+
"""
|
|
665
|
+
Tests for sensor tamper callback.
|
|
666
|
+
"""
|
|
667
|
+
g90 = G90Alarm(host=mock_device.host, port=mock_device.port,
|
|
668
|
+
notifications_local_host=mock_device.notification_host,
|
|
669
|
+
notifications_local_port=mock_device.notification_port)
|
|
670
|
+
|
|
671
|
+
sensors = await g90.get_sensors()
|
|
672
|
+
prop_sensors = await g90.sensors
|
|
673
|
+
|
|
674
|
+
assert sensors == prop_sensors
|
|
675
|
+
future = asyncio.get_running_loop().create_future()
|
|
676
|
+
sensor = [x for x in sensors if x.index == 100 and x.name == 'Hall']
|
|
677
|
+
tamper_sensor_cb = MagicMock()
|
|
678
|
+
tamper_sensor_cb.side_effect = (
|
|
679
|
+
lambda *args: future.set_result(True)
|
|
680
|
+
)
|
|
681
|
+
sensor[0].tamper_callback = tamper_sensor_cb
|
|
682
|
+
tamper_cb = MagicMock()
|
|
683
|
+
g90.tamper_callback = tamper_cb
|
|
684
|
+
|
|
685
|
+
await g90.listen_device_notifications()
|
|
686
|
+
await mock_device.send_next_notification()
|
|
687
|
+
await asyncio.wait([future], timeout=0.1)
|
|
688
|
+
|
|
689
|
+
tamper_sensor_cb.assert_called_once_with()
|
|
690
|
+
tamper_cb.assert_called_once_with(100, 'Hall')
|
|
691
|
+
# Verify the sensor tampered state is set upon receiving the
|
|
692
|
+
# notification
|
|
693
|
+
assert sensor[0].is_tampered is True
|
|
694
|
+
|
|
695
|
+
# Signal the second notification is ready, the future has to be re-created
|
|
696
|
+
# as the corresponding callback will be fired again
|
|
697
|
+
future = asyncio.get_running_loop().create_future()
|
|
698
|
+
await mock_device.send_next_notification()
|
|
699
|
+
await asyncio.wait([future], timeout=0.1)
|
|
700
|
+
|
|
701
|
+
# Verify the sensor tampered state is reset upon disarming
|
|
702
|
+
assert sensor[0].is_tampered is False
|
|
703
|
+
|
|
704
|
+
g90.close_device_notifications()
|
|
705
|
+
|
|
706
|
+
|
|
532
707
|
@pytest.mark.g90device(
|
|
533
708
|
sent_data=[
|
|
534
709
|
b'ISTART[102,'
|
|
@@ -718,6 +893,13 @@ async def test_set_alert_config(mock_device: DeviceMock) -> None:
|
|
|
718
893
|
# that checks if alert config has been modified externally
|
|
719
894
|
b"ISTART[117,[1]]IEND\0",
|
|
720
895
|
b"ISTARTIEND\0",
|
|
896
|
+
# Simulated list of sensors, which is used to reset door open when
|
|
897
|
+
# arming/tamper flags on those had the flags set when arming
|
|
898
|
+
b'ISTART[102,'
|
|
899
|
+
b'[[3,1,3],["Remote 1",10,0,10,1,0,32,0,0,16,1,0,""],'
|
|
900
|
+
b'["Remote 2",11,0,10,1,0,32,0,0,16,1,0,""],'
|
|
901
|
+
b'["Cord 1",12,0,126,1,0,32,0,5,16,1,0,""]'
|
|
902
|
+
b']]IEND\0',
|
|
721
903
|
],
|
|
722
904
|
notification_data=[
|
|
723
905
|
b'[170,[1,[1]]]\0',
|
|
@@ -739,11 +921,11 @@ async def test_sms_alert_when_armed(mock_device: DeviceMock) -> None:
|
|
|
739
921
|
await mock_device.send_next_notification()
|
|
740
922
|
await asyncio.wait([future], timeout=0.1)
|
|
741
923
|
g90.close_device_notifications()
|
|
742
|
-
assert
|
|
924
|
+
assert set([
|
|
743
925
|
b'ISTART[117,117,""]IEND\0',
|
|
744
926
|
b'ISTART[117,117,""]IEND\0',
|
|
745
927
|
b"ISTART[116,116,[116,[513]]]IEND\0",
|
|
746
|
-
]
|
|
928
|
+
]).issubset(set(mock_device.recv_data))
|
|
747
929
|
|
|
748
930
|
|
|
749
931
|
@pytest.mark.g90device(
|
|
@@ -752,6 +934,11 @@ async def test_sms_alert_when_armed(mock_device: DeviceMock) -> None:
|
|
|
752
934
|
b"ISTART[117,[513]]IEND\0",
|
|
753
935
|
b"ISTART[117,[513]]IEND\0",
|
|
754
936
|
b"ISTARTIEND\0",
|
|
937
|
+
b'ISTART[102,'
|
|
938
|
+
b'[[3,1,3],["Remote 1",10,0,10,1,0,32,0,0,16,1,0,""],'
|
|
939
|
+
b'["Remote 2",11,0,10,1,0,32,0,0,16,1,0,""],'
|
|
940
|
+
b'["Cord 1",12,0,126,1,0,32,0,5,16,1,0,""]'
|
|
941
|
+
b']]IEND\0',
|
|
755
942
|
],
|
|
756
943
|
notification_data=[
|
|
757
944
|
b'[170,[1,[3]]]\0',
|
|
@@ -773,11 +960,11 @@ async def test_sms_alert_when_disarmed(mock_device: DeviceMock) -> None:
|
|
|
773
960
|
await mock_device.send_next_notification()
|
|
774
961
|
await asyncio.wait([future], timeout=0.1)
|
|
775
962
|
g90.close_device_notifications()
|
|
776
|
-
assert
|
|
963
|
+
assert set([
|
|
777
964
|
b'ISTART[117,117,""]IEND\0',
|
|
778
965
|
b'ISTART[117,117,""]IEND\0',
|
|
779
966
|
b"ISTART[116,116,[116,[1]]]IEND\0",
|
|
780
|
-
]
|
|
967
|
+
]).issubset(set(mock_device.recv_data))
|
|
781
968
|
|
|
782
969
|
|
|
783
970
|
@pytest.mark.g90device(sent_data=[
|
|
@@ -481,7 +481,31 @@ async def test_alarm_callback(mock_device: DeviceMock) -> None:
|
|
|
481
481
|
await mock_device.send_next_notification()
|
|
482
482
|
await asyncio.wait([future], timeout=0.1)
|
|
483
483
|
notifications.close()
|
|
484
|
-
notifications.on_alarm.assert_called_once_with(11, 'Hall')
|
|
484
|
+
notifications.on_alarm.assert_called_once_with(11, 'Hall', False)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
@pytest.mark.g90device(notification_data=[
|
|
488
|
+
b'[208,[3,11,1,3,"Hall","DUMMYGUID",1630876128,0,[""]]]\0',
|
|
489
|
+
])
|
|
490
|
+
async def test_tamper_callback(mock_device: DeviceMock) -> None:
|
|
491
|
+
"""
|
|
492
|
+
Verifies that alarm callback is handled correctly when a sensor is
|
|
493
|
+
tampered.
|
|
494
|
+
"""
|
|
495
|
+
future = asyncio.get_running_loop().create_future()
|
|
496
|
+
notifications = G90DeviceNotifications(
|
|
497
|
+
local_host=mock_device.notification_host,
|
|
498
|
+
local_port=mock_device.notification_port
|
|
499
|
+
)
|
|
500
|
+
notifications.on_alarm = MagicMock() # type: ignore[method-assign]
|
|
501
|
+
notifications.on_alarm.side_effect = (
|
|
502
|
+
lambda *args: future.set_result(True)
|
|
503
|
+
)
|
|
504
|
+
await notifications.listen()
|
|
505
|
+
await mock_device.send_next_notification()
|
|
506
|
+
await asyncio.wait([future], timeout=0.1)
|
|
507
|
+
notifications.close()
|
|
508
|
+
notifications.on_alarm.assert_called_once_with(11, 'Hall', True)
|
|
485
509
|
|
|
486
510
|
|
|
487
511
|
@pytest.mark.g90device(notification_data=[
|
|
@@ -541,3 +565,29 @@ async def test_low_battery_callback(mock_device: DeviceMock) -> None:
|
|
|
541
565
|
await asyncio.wait([future], timeout=0.1)
|
|
542
566
|
notifications.close()
|
|
543
567
|
notifications.on_low_battery.assert_called_once_with(26, 'Hall')
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
@pytest.mark.g90device(notification_data=[
|
|
571
|
+
b'[170,[6,[21,"Hall"]]]\0'
|
|
572
|
+
])
|
|
573
|
+
async def test_door_open_when_arming_callback(mock_device: DeviceMock) -> None:
|
|
574
|
+
"""
|
|
575
|
+
Verifies that door open when arming callback is handled correctly.
|
|
576
|
+
"""
|
|
577
|
+
future = asyncio.get_running_loop().create_future()
|
|
578
|
+
notifications = G90DeviceNotifications(
|
|
579
|
+
local_host=mock_device.notification_host,
|
|
580
|
+
local_port=mock_device.notification_port
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
notifications.on_door_open_when_arming = ( # type: ignore[method-assign]
|
|
584
|
+
MagicMock()
|
|
585
|
+
)
|
|
586
|
+
notifications.on_door_open_when_arming.side_effect = (
|
|
587
|
+
lambda *args: future.set_result(True)
|
|
588
|
+
)
|
|
589
|
+
await notifications.listen()
|
|
590
|
+
await mock_device.send_next_notification()
|
|
591
|
+
await asyncio.wait([future], timeout=0.1)
|
|
592
|
+
notifications.close()
|
|
593
|
+
notifications.on_door_open_when_arming.assert_called_once_with(21, 'Hall')
|
|
@@ -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
|