pyg90alarm 1.17.2__tar.gz → 1.19.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.2 → pyg90alarm-1.19.0}/PKG-INFO +1 -1
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/alarm.py +116 -12
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/const.py +1 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/device_notifications.py +29 -3
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/entities/sensor.py +147 -18
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm.egg-info/PKG-INFO +1 -1
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/tests/test_alarm.py +174 -5
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/tests/test_notifications.py +51 -1
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/.github/CODEOWNERS +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/.github/workflows/main.yml +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/.gitignore +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/.pylintrc +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/.readthedocs.yaml +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/LICENSE +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/MANIFEST.in +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/README.rst +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/docs/.DS_Store +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/docs/.gitignore +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/docs/api-docs.rst +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/docs/conf.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/docs/index.rst +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/docs/protocol.rst +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/docs/requirements.txt +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/pyproject.toml +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/setup.cfg +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/setup.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/sonar-project.properties +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/__init__.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/base_cmd.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/callback.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/config.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/definitions/__init__.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/definitions/sensors.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/discovery.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/entities/__init__.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/entities/device.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/exceptions.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/history.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/host_info.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/host_status.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/paginated_cmd.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/paginated_result.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/py.typed +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/targeted_discovery.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm/user_data_crc.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm.egg-info/SOURCES.txt +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm.egg-info/dependency_links.txt +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm.egg-info/requires.txt +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/src/pyg90alarm.egg-info/top_level.txt +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/tests/__init__.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/tests/conftest.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/tests/device_mock.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/tests/test_base_commands.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/tests/test_discovery.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/tests/test_history.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/tests/test_paginated_commands.py +0 -0
- {pyg90alarm-1.17.2 → pyg90alarm-1.19.0}/tox.ini +0 -0
|
@@ -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
|
|
@@ -174,6 +191,10 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
174
191
|
self._remote_button_press_cb: Optional[
|
|
175
192
|
RemoteButtonPressCallback
|
|
176
193
|
] = None
|
|
194
|
+
self._door_open_when_arming_cb: Optional[
|
|
195
|
+
DoorOpenWhenArmingCallback
|
|
196
|
+
] = None
|
|
197
|
+
self._tamper_cb: Optional[TamperCallback] = None
|
|
177
198
|
self._reset_occupancy_interval = reset_occupancy_interval
|
|
178
199
|
self._alert_config: Optional[G90AlertConfigFlags] = None
|
|
179
200
|
self._sms_alert_when_armed = False
|
|
@@ -613,6 +634,17 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
613
634
|
await self.set_alert_config(
|
|
614
635
|
await self.alert_config | G90AlertConfigFlags.SMS_PUSH
|
|
615
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
|
+
|
|
616
648
|
G90Callback.invoke(self._armdisarm_cb, state)
|
|
617
649
|
|
|
618
650
|
@property
|
|
@@ -627,7 +659,9 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
627
659
|
def armdisarm_callback(self, value: ArmDisarmCallback) -> None:
|
|
628
660
|
self._armdisarm_cb = value
|
|
629
661
|
|
|
630
|
-
async def on_alarm(
|
|
662
|
+
async def on_alarm(
|
|
663
|
+
self, event_id: int, zone_name: str, is_tampered: bool
|
|
664
|
+
) -> None:
|
|
631
665
|
"""
|
|
632
666
|
Invoked when alarm is triggered. Fires corresponding callback if set by
|
|
633
667
|
the user with :attr:`.alarm_callback`.
|
|
@@ -638,16 +672,31 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
638
672
|
:param zone_name: Sensor name
|
|
639
673
|
"""
|
|
640
674
|
sensor = await self.find_sensor(event_id, zone_name)
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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)
|
|
651
700
|
|
|
652
701
|
G90Callback.invoke(
|
|
653
702
|
self._alarm_cb, event_id, zone_name, extra_data
|
|
@@ -718,7 +767,10 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
718
767
|
|
|
719
768
|
# Also report the event as alarm for unification, hard-coding the
|
|
720
769
|
# sensor name in case of host SOS
|
|
721
|
-
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
|
+
)
|
|
722
774
|
|
|
723
775
|
if not is_host_sos:
|
|
724
776
|
# Also report the remote button press for SOS - the panel will not
|
|
@@ -778,6 +830,58 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
778
830
|
) -> None:
|
|
779
831
|
self._remote_button_press_cb = value
|
|
780
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
|
+
|
|
781
885
|
async def listen_device_notifications(self) -> None:
|
|
782
886
|
"""
|
|
783
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
|
|
@@ -101,6 +104,16 @@ class G90SensorUserFlags(IntFlag):
|
|
|
101
104
|
ALERT_WHEN_AWAY_AND_HOME = 32
|
|
102
105
|
ALERT_WHEN_AWAY = 64
|
|
103
106
|
SUPPORTS_UPDATING_SUBTYPE = 512 # Only relevant for cord sensors
|
|
107
|
+
# Flags that can be set by the user
|
|
108
|
+
USER_SETTABLE = (
|
|
109
|
+
ENABLED
|
|
110
|
+
| ARM_DELAY
|
|
111
|
+
| DETECT_DOOR
|
|
112
|
+
| DOOR_CHIME
|
|
113
|
+
| INDEPENDENT_ZONE
|
|
114
|
+
| ALERT_WHEN_AWAY_AND_HOME
|
|
115
|
+
| ALERT_WHEN_AWAY
|
|
116
|
+
)
|
|
104
117
|
|
|
105
118
|
|
|
106
119
|
class G90SensorProtocols(IntEnum):
|
|
@@ -157,6 +170,7 @@ class G90SensorTypes(IntEnum):
|
|
|
157
170
|
_LOGGER = logging.getLogger(__name__)
|
|
158
171
|
|
|
159
172
|
|
|
173
|
+
# pylint: disable=too-many-public-methods
|
|
160
174
|
class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
161
175
|
"""
|
|
162
176
|
Interacts with sensor on G90 alarm panel.
|
|
@@ -186,6 +200,12 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
186
200
|
self._state_callback: Optional[SensorStateCallback] = None
|
|
187
201
|
self._low_battery_callback: Optional[SensorLowBatteryCallback] = None
|
|
188
202
|
self._low_battery = False
|
|
203
|
+
self._tampered = False
|
|
204
|
+
self._door_open_when_arming_callback: Optional[
|
|
205
|
+
SensorDoorOpenWhenArmingCallback
|
|
206
|
+
] = None
|
|
207
|
+
self._tamper_callback: Optional[SensorTamperCallback] = None
|
|
208
|
+
self._door_open_when_arming = False
|
|
189
209
|
self._proto_idx = proto_idx
|
|
190
210
|
self._extra_data: Any = None
|
|
191
211
|
|
|
@@ -238,6 +258,37 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
238
258
|
def low_battery_callback(self, value: SensorLowBatteryCallback) -> None:
|
|
239
259
|
self._low_battery_callback = value
|
|
240
260
|
|
|
261
|
+
@property
|
|
262
|
+
def door_open_when_arming_callback(
|
|
263
|
+
self
|
|
264
|
+
) -> Optional[SensorDoorOpenWhenArmingCallback]:
|
|
265
|
+
"""
|
|
266
|
+
Callback that is invoked when the sensor reports on open door
|
|
267
|
+
condition when arming.
|
|
268
|
+
|
|
269
|
+
:return: Sensor's door open when arming callback
|
|
270
|
+
"""
|
|
271
|
+
return self._door_open_when_arming_callback
|
|
272
|
+
|
|
273
|
+
@door_open_when_arming_callback.setter
|
|
274
|
+
def door_open_when_arming_callback(
|
|
275
|
+
self, value: SensorDoorOpenWhenArmingCallback
|
|
276
|
+
) -> None:
|
|
277
|
+
self._door_open_when_arming_callback = value
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def tamper_callback(self) -> Optional[SensorTamperCallback]:
|
|
281
|
+
"""
|
|
282
|
+
Callback that is invoked when the sensor reports being tampered.
|
|
283
|
+
|
|
284
|
+
:return: Sensor's tamper callback
|
|
285
|
+
"""
|
|
286
|
+
return self._tamper_callback
|
|
287
|
+
|
|
288
|
+
@tamper_callback.setter
|
|
289
|
+
def tamper_callback(self, value: SensorTamperCallback) -> None:
|
|
290
|
+
self._tamper_callback = value
|
|
291
|
+
|
|
241
292
|
@property
|
|
242
293
|
def occupancy(self) -> bool:
|
|
243
294
|
"""
|
|
@@ -365,12 +416,16 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
365
416
|
def is_low_battery(self) -> bool:
|
|
366
417
|
"""
|
|
367
418
|
Indicates if the sensor is reporting low battery.
|
|
419
|
+
|
|
420
|
+
The condition is cleared when the sensor reports activity (i.e. is no
|
|
421
|
+
longer low on battery as it is able to report the activity).
|
|
368
422
|
"""
|
|
369
423
|
return self._low_battery
|
|
370
424
|
|
|
371
425
|
def _set_low_battery(self, value: bool) -> None:
|
|
372
426
|
"""
|
|
373
427
|
Sets low battery state of the sensor.
|
|
428
|
+
|
|
374
429
|
Intentionally private, as low battery state is derived from
|
|
375
430
|
notifications/alerts.
|
|
376
431
|
|
|
@@ -383,6 +438,56 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
383
438
|
)
|
|
384
439
|
self._low_battery = value
|
|
385
440
|
|
|
441
|
+
@property
|
|
442
|
+
def is_tampered(self) -> bool:
|
|
443
|
+
"""
|
|
444
|
+
Indicates if the sensor has been tampered.
|
|
445
|
+
|
|
446
|
+
The condition is cleared when panel is armed/disarmed next time.
|
|
447
|
+
"""
|
|
448
|
+
return self._tampered
|
|
449
|
+
|
|
450
|
+
def _set_tampered(self, value: bool) -> None:
|
|
451
|
+
"""
|
|
452
|
+
Sets tamper state of the sensor.
|
|
453
|
+
|
|
454
|
+
Intentionally private, as tamper state is derived from
|
|
455
|
+
notifications/alerts.
|
|
456
|
+
|
|
457
|
+
:param value: Tamper state
|
|
458
|
+
"""
|
|
459
|
+
_LOGGER.debug(
|
|
460
|
+
"Setting tamper for sensor index=%s '%s': %s"
|
|
461
|
+
" (previous value: %s)",
|
|
462
|
+
self.index, self.name, value, self._tampered
|
|
463
|
+
)
|
|
464
|
+
self._tampered = value
|
|
465
|
+
|
|
466
|
+
@property
|
|
467
|
+
def is_door_open_when_arming(self) -> bool:
|
|
468
|
+
"""
|
|
469
|
+
Indicates if the sensor reports on open door when arming.
|
|
470
|
+
|
|
471
|
+
The condition is cleared when panel is armed/disarmed next time.
|
|
472
|
+
"""
|
|
473
|
+
return self._door_open_when_arming
|
|
474
|
+
|
|
475
|
+
def _set_door_open_when_arming(self, value: bool) -> None:
|
|
476
|
+
"""
|
|
477
|
+
Sets door open state of the sensor when arming.
|
|
478
|
+
|
|
479
|
+
Intentionally private, as door open state is derived from
|
|
480
|
+
notifications/alerts.
|
|
481
|
+
|
|
482
|
+
:param value: Door open state
|
|
483
|
+
"""
|
|
484
|
+
_LOGGER.debug(
|
|
485
|
+
"Setting door open when arming for sensor index=%s '%s': %s"
|
|
486
|
+
" (previous value: %s)",
|
|
487
|
+
self.index, self.name, value, self._door_open_when_arming
|
|
488
|
+
)
|
|
489
|
+
self._door_open_when_arming = value
|
|
490
|
+
|
|
386
491
|
@property
|
|
387
492
|
def enabled(self) -> bool:
|
|
388
493
|
"""
|
|
@@ -392,18 +497,26 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
392
497
|
"""
|
|
393
498
|
return self.user_flag & G90SensorUserFlags.ENABLED != 0
|
|
394
499
|
|
|
395
|
-
async def
|
|
500
|
+
async def set_user_flag(self, value: G90SensorUserFlags) -> None:
|
|
396
501
|
"""
|
|
397
|
-
Sets
|
|
502
|
+
Sets user flags of the sensor.
|
|
398
503
|
|
|
399
|
-
:param value:
|
|
504
|
+
:param value: User flags to set, values other than
|
|
505
|
+
:attr:`.G90SensorUserFlags.USER_SETTABLE` will be ignored and
|
|
506
|
+
preserved from existing sensor flags.
|
|
400
507
|
"""
|
|
508
|
+
if value & ~G90SensorUserFlags.USER_SETTABLE:
|
|
509
|
+
_LOGGER.warning(
|
|
510
|
+
'User flags for sensor index=%s contain non-user settable'
|
|
511
|
+
' flags, those will be ignored: %s',
|
|
512
|
+
self.index, repr(value & ~G90SensorUserFlags.USER_SETTABLE)
|
|
513
|
+
)
|
|
401
514
|
# Checking private attribute directly, since `mypy` doesn't recognize
|
|
402
515
|
# the check for sensor definition to be defined if done over
|
|
403
516
|
# `self.supports_enable_disable` property
|
|
404
517
|
if not self._definition:
|
|
405
518
|
_LOGGER.warning(
|
|
406
|
-
'Manipulating with
|
|
519
|
+
'Manipulating with user flags for sensor index=%s'
|
|
407
520
|
' is unsupported - no sensor definition for'
|
|
408
521
|
' type=%s, subtype=%s',
|
|
409
522
|
self.index,
|
|
@@ -431,7 +544,7 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
431
544
|
if not sensors:
|
|
432
545
|
_LOGGER.error(
|
|
433
546
|
'Sensor index=%s not found when attempting to set its'
|
|
434
|
-
'
|
|
547
|
+
' user flag',
|
|
435
548
|
self.index,
|
|
436
549
|
)
|
|
437
550
|
return
|
|
@@ -444,30 +557,30 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
444
557
|
) != self._protocol_data:
|
|
445
558
|
_LOGGER.error(
|
|
446
559
|
"Sensor index=%s '%s' has been changed externally,"
|
|
447
|
-
" refusing to alter its
|
|
560
|
+
" refusing to alter its user flag",
|
|
448
561
|
self.index,
|
|
449
562
|
self.name
|
|
450
563
|
)
|
|
451
564
|
return
|
|
452
565
|
|
|
453
|
-
|
|
454
|
-
# appropriately.
|
|
455
|
-
user_flag = self.user_flag
|
|
456
|
-
if value:
|
|
457
|
-
user_flag |= G90SensorUserFlags.ENABLED
|
|
458
|
-
else:
|
|
459
|
-
user_flag &= ~G90SensorUserFlags.ENABLED
|
|
566
|
+
prev_user_flag = self.user_flag
|
|
460
567
|
|
|
461
568
|
# Re-instantiate the protocol data with modified user flags
|
|
462
569
|
_data = asdict(self._protocol_data)
|
|
463
|
-
_data['user_flag_data'] =
|
|
570
|
+
_data['user_flag_data'] = (
|
|
571
|
+
# Preserve flags that are not user-settable
|
|
572
|
+
self.user_flag & ~G90SensorUserFlags.USER_SETTABLE
|
|
573
|
+
) | (
|
|
574
|
+
# Combine them with the new user-settable flags
|
|
575
|
+
value & G90SensorUserFlags.USER_SETTABLE
|
|
576
|
+
)
|
|
464
577
|
self._protocol_data = self._protocol_incoming_data_kls(**_data)
|
|
465
578
|
|
|
466
579
|
_LOGGER.debug(
|
|
467
|
-
'Sensor index=%s: %s
|
|
580
|
+
'Sensor index=%s: previous user_flag %s, resulting user_flag %s',
|
|
468
581
|
self._protocol_data.index,
|
|
469
|
-
|
|
470
|
-
self.user_flag
|
|
582
|
+
repr(prev_user_flag),
|
|
583
|
+
repr(self.user_flag)
|
|
471
584
|
)
|
|
472
585
|
|
|
473
586
|
# Generate protocol data from write operation, deriving values either
|
|
@@ -494,6 +607,20 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
494
607
|
G90Commands.SETSINGLESENSOR, list(astuple(outgoing_data))
|
|
495
608
|
)
|
|
496
609
|
|
|
610
|
+
async def set_enabled(self, value: bool) -> None:
|
|
611
|
+
"""
|
|
612
|
+
Sets the sensor enabled/disabled.
|
|
613
|
+
"""
|
|
614
|
+
|
|
615
|
+
# Modify the value of the user flag setting enabled/disabled one
|
|
616
|
+
# appropriately.
|
|
617
|
+
user_flag = self.user_flag
|
|
618
|
+
if value:
|
|
619
|
+
user_flag |= G90SensorUserFlags.ENABLED
|
|
620
|
+
else:
|
|
621
|
+
user_flag &= ~G90SensorUserFlags.ENABLED
|
|
622
|
+
await self.set_user_flag(user_flag)
|
|
623
|
+
|
|
497
624
|
@property
|
|
498
625
|
def extra_data(self) -> Any:
|
|
499
626
|
"""
|
|
@@ -528,6 +655,8 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
528
655
|
'supports_enable_disable': self.supports_enable_disable,
|
|
529
656
|
'is_wireless': self.is_wireless,
|
|
530
657
|
'is_low_battery': self.is_low_battery,
|
|
658
|
+
'is_tampered': self.is_tampered,
|
|
659
|
+
'is_door_open_when_arming': self.is_door_open_when_arming,
|
|
531
660
|
}
|
|
532
661
|
|
|
533
662
|
def __repr__(self) -> str:
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Tests for G90Alarm class
|
|
3
3
|
"""
|
|
4
|
+
# pylint: disable=too-many-lines
|
|
5
|
+
|
|
4
6
|
import asyncio
|
|
5
7
|
from itertools import cycle
|
|
6
8
|
from unittest.mock import MagicMock
|
|
@@ -13,7 +15,7 @@ from pyg90alarm.host_info import (
|
|
|
13
15
|
G90HostInfo, G90HostInfoGsmStatus, G90HostInfoWifiStatus,
|
|
14
16
|
)
|
|
15
17
|
from pyg90alarm.entities.sensor import (
|
|
16
|
-
G90Sensor,
|
|
18
|
+
G90Sensor, G90SensorUserFlags,
|
|
17
19
|
)
|
|
18
20
|
from pyg90alarm.entities.device import (
|
|
19
21
|
G90Device,
|
|
@@ -438,6 +440,69 @@ async def test_sensor_low_battery_callback(mock_device: DeviceMock) -> None:
|
|
|
438
440
|
|
|
439
441
|
|
|
440
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
|
+
],
|
|
441
506
|
notification_data=[
|
|
442
507
|
b'[170,[1,[1]]]\0'
|
|
443
508
|
]
|
|
@@ -580,6 +645,65 @@ async def test_alarm_callback(mock_device: DeviceMock) -> None:
|
|
|
580
645
|
g90.close_device_notifications()
|
|
581
646
|
|
|
582
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
|
+
|
|
583
707
|
@pytest.mark.g90device(
|
|
584
708
|
sent_data=[
|
|
585
709
|
b'ISTART[102,'
|
|
@@ -769,6 +893,13 @@ async def test_set_alert_config(mock_device: DeviceMock) -> None:
|
|
|
769
893
|
# that checks if alert config has been modified externally
|
|
770
894
|
b"ISTART[117,[1]]IEND\0",
|
|
771
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',
|
|
772
903
|
],
|
|
773
904
|
notification_data=[
|
|
774
905
|
b'[170,[1,[1]]]\0',
|
|
@@ -790,11 +921,11 @@ async def test_sms_alert_when_armed(mock_device: DeviceMock) -> None:
|
|
|
790
921
|
await mock_device.send_next_notification()
|
|
791
922
|
await asyncio.wait([future], timeout=0.1)
|
|
792
923
|
g90.close_device_notifications()
|
|
793
|
-
assert
|
|
924
|
+
assert set([
|
|
794
925
|
b'ISTART[117,117,""]IEND\0',
|
|
795
926
|
b'ISTART[117,117,""]IEND\0',
|
|
796
927
|
b"ISTART[116,116,[116,[513]]]IEND\0",
|
|
797
|
-
]
|
|
928
|
+
]).issubset(set(mock_device.recv_data))
|
|
798
929
|
|
|
799
930
|
|
|
800
931
|
@pytest.mark.g90device(
|
|
@@ -803,6 +934,11 @@ async def test_sms_alert_when_armed(mock_device: DeviceMock) -> None:
|
|
|
803
934
|
b"ISTART[117,[513]]IEND\0",
|
|
804
935
|
b"ISTART[117,[513]]IEND\0",
|
|
805
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',
|
|
806
942
|
],
|
|
807
943
|
notification_data=[
|
|
808
944
|
b'[170,[1,[3]]]\0',
|
|
@@ -824,11 +960,11 @@ async def test_sms_alert_when_disarmed(mock_device: DeviceMock) -> None:
|
|
|
824
960
|
await mock_device.send_next_notification()
|
|
825
961
|
await asyncio.wait([future], timeout=0.1)
|
|
826
962
|
g90.close_device_notifications()
|
|
827
|
-
assert
|
|
963
|
+
assert set([
|
|
828
964
|
b'ISTART[117,117,""]IEND\0',
|
|
829
965
|
b'ISTART[117,117,""]IEND\0',
|
|
830
966
|
b"ISTART[116,116,[116,[1]]]IEND\0",
|
|
831
|
-
]
|
|
967
|
+
]).issubset(set(mock_device.recv_data))
|
|
832
968
|
|
|
833
969
|
|
|
834
970
|
@pytest.mark.g90device(sent_data=[
|
|
@@ -963,3 +1099,36 @@ async def test_device_unsupported_disable(mock_device: DeviceMock) -> None:
|
|
|
963
1099
|
assert mock_device.recv_data == [
|
|
964
1100
|
b'ISTART[138,138,[138,[1,10]]]IEND\0',
|
|
965
1101
|
]
|
|
1102
|
+
|
|
1103
|
+
|
|
1104
|
+
@pytest.mark.g90device(sent_data=[
|
|
1105
|
+
b'ISTART[102,'
|
|
1106
|
+
b'[[1,1,1],'
|
|
1107
|
+
b'["Night Light2",10,0,138,0,0,33,0,0,17,1,0,""]'
|
|
1108
|
+
b']]IEND\0',
|
|
1109
|
+
b'ISTART[102,'
|
|
1110
|
+
b'[[1,1,1],'
|
|
1111
|
+
b'["Night Light2",10,0,138,0,0,33,0,0,17,1,0,""]'
|
|
1112
|
+
b']]IEND\0',
|
|
1113
|
+
b"ISTARTIEND\0",
|
|
1114
|
+
])
|
|
1115
|
+
async def test_sensor_set_user_flags(mock_device: DeviceMock) -> None:
|
|
1116
|
+
"""
|
|
1117
|
+
Tests for setting user flags on a sensor.
|
|
1118
|
+
"""
|
|
1119
|
+
g90 = G90Alarm(host=mock_device.host, port=mock_device.port)
|
|
1120
|
+
sensors = await g90.get_sensors()
|
|
1121
|
+
await sensors[0].set_user_flag(
|
|
1122
|
+
# Intentionally contains non-user settable flag, which should be
|
|
1123
|
+
# ignored and not configured for the sensor that initial doesn't have
|
|
1124
|
+
# it set
|
|
1125
|
+
G90SensorUserFlags.INDEPENDENT_ZONE | G90SensorUserFlags.ARM_DELAY
|
|
1126
|
+
| G90SensorUserFlags.SUPPORTS_UPDATING_SUBTYPE
|
|
1127
|
+
)
|
|
1128
|
+
assert mock_device.recv_data == [
|
|
1129
|
+
b'ISTART[102,102,[102,[1,10]]]IEND\0',
|
|
1130
|
+
b'ISTART[102,102,[102,[1,1]]]IEND\0',
|
|
1131
|
+
b'ISTART[103,103,[103,'
|
|
1132
|
+
b'["Night Light2",10,0,138,0,0,18,0,0,17,1,0,2,"060A0600"]'
|
|
1133
|
+
b']]IEND\0',
|
|
1134
|
+
]
|
|
@@ -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')
|
|
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
|