pyg90alarm 1.19.0__tar.gz → 1.20.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.19.0 → pyg90alarm-1.20.0}/PKG-INFO +13 -2
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/alarm.py +29 -80
- pyg90alarm-1.20.0/src/pyg90alarm/entities/base_entity.py +83 -0
- pyg90alarm-1.20.0/src/pyg90alarm/entities/base_list.py +165 -0
- pyg90alarm-1.20.0/src/pyg90alarm/entities/device_list.py +58 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/entities/sensor.py +63 -3
- pyg90alarm-1.20.0/src/pyg90alarm/entities/sensor_list.py +50 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm.egg-info/PKG-INFO +13 -2
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm.egg-info/SOURCES.txt +4 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/tests/test_alarm.py +85 -42
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/tests/test_history.py +1 -1
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/.github/CODEOWNERS +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/.github/workflows/main.yml +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/.gitignore +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/.pylintrc +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/.readthedocs.yaml +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/LICENSE +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/MANIFEST.in +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/README.rst +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/docs/.DS_Store +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/docs/.gitignore +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/docs/api-docs.rst +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/docs/conf.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/docs/index.rst +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/docs/protocol.rst +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/docs/requirements.txt +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/pyproject.toml +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/setup.cfg +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/setup.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/sonar-project.properties +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/__init__.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/base_cmd.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/callback.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/config.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/const.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/definitions/__init__.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/definitions/sensors.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/device_notifications.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/discovery.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/entities/__init__.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/entities/device.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/exceptions.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/history.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/host_info.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/host_status.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/paginated_cmd.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/paginated_result.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/py.typed +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/targeted_discovery.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm/user_data_crc.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm.egg-info/dependency_links.txt +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm.egg-info/requires.txt +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/src/pyg90alarm.egg-info/top_level.txt +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/tests/__init__.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/tests/conftest.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/tests/device_mock.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/tests/test_base_commands.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/tests/test_discovery.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/tests/test_notifications.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/tests/test_paginated_commands.py +0 -0
- {pyg90alarm-1.19.0 → pyg90alarm-1.20.0}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: pyg90alarm
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.20.0
|
|
4
4
|
Summary: G90 Alarm system protocol
|
|
5
5
|
Home-page: https://github.com/hostcc/pyg90alarm
|
|
6
6
|
Author: Ilia Sotnikov
|
|
@@ -33,6 +33,17 @@ Requires-Dist: asynctest; extra == "test"
|
|
|
33
33
|
Provides-Extra: docs
|
|
34
34
|
Requires-Dist: sphinx; extra == "docs"
|
|
35
35
|
Requires-Dist: sphinx-rtd-theme; extra == "docs"
|
|
36
|
+
Dynamic: author
|
|
37
|
+
Dynamic: author-email
|
|
38
|
+
Dynamic: classifier
|
|
39
|
+
Dynamic: description
|
|
40
|
+
Dynamic: description-content-type
|
|
41
|
+
Dynamic: home-page
|
|
42
|
+
Dynamic: keywords
|
|
43
|
+
Dynamic: project-url
|
|
44
|
+
Dynamic: provides-extra
|
|
45
|
+
Dynamic: requires-python
|
|
46
|
+
Dynamic: summary
|
|
36
47
|
|
|
37
48
|
.. image:: https://github.com/hostcc/pyg90alarm/actions/workflows/main.yml/badge.svg?branch=master
|
|
38
49
|
:target: https://github.com/hostcc/pyg90alarm/tree/master
|
|
@@ -69,7 +69,9 @@ from .const import (
|
|
|
69
69
|
from .base_cmd import (G90BaseCommand, G90BaseCommandData)
|
|
70
70
|
from .paginated_result import G90PaginatedResult, G90PaginatedResponse
|
|
71
71
|
from .entities.sensor import (G90Sensor, G90SensorTypes)
|
|
72
|
+
from .entities.sensor_list import G90SensorList
|
|
72
73
|
from .entities.device import G90Device
|
|
74
|
+
from .entities.device_list import G90DeviceList
|
|
73
75
|
from .device_notifications import (
|
|
74
76
|
G90DeviceNotifications,
|
|
75
77
|
)
|
|
@@ -177,10 +179,8 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
177
179
|
)
|
|
178
180
|
self._host: str = host
|
|
179
181
|
self._port: int = port
|
|
180
|
-
self._sensors
|
|
181
|
-
self.
|
|
182
|
-
self._devices: List[G90Device] = []
|
|
183
|
-
self._devices_lock = asyncio.Lock()
|
|
182
|
+
self._sensors = G90SensorList(self)
|
|
183
|
+
self._devices = G90DeviceList(self)
|
|
184
184
|
self._notifications: Optional[G90DeviceNotifications] = None
|
|
185
185
|
self._sensor_cb: Optional[SensorCallback] = None
|
|
186
186
|
self._armdisarm_cb: Optional[ArmDisarmCallback] = None
|
|
@@ -268,109 +268,58 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
268
268
|
@property
|
|
269
269
|
async def sensors(self) -> List[G90Sensor]:
|
|
270
270
|
"""
|
|
271
|
-
|
|
272
|
-
|
|
271
|
+
Returns the list of sensors configured in the device. Please note
|
|
272
|
+
it doesn't update those from the panel except initially when the list
|
|
273
|
+
if empty.
|
|
274
|
+
|
|
275
|
+
:return: List of sensors
|
|
273
276
|
"""
|
|
274
|
-
return await self.
|
|
277
|
+
return await self._sensors.entities
|
|
275
278
|
|
|
276
279
|
async def get_sensors(self) -> List[G90Sensor]:
|
|
277
280
|
"""
|
|
278
|
-
Provides list of sensors configured in the device
|
|
279
|
-
|
|
280
|
-
reflect any updates there.
|
|
281
|
+
Provides list of sensors configured in the device, updating them from
|
|
282
|
+
panel on each call.
|
|
281
283
|
|
|
282
284
|
:return: List of sensors
|
|
283
285
|
"""
|
|
284
|
-
|
|
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
|
|
291
|
-
)
|
|
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)
|
|
286
|
+
return await self._sensors.update()
|
|
298
287
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
return self._sensors
|
|
304
|
-
|
|
305
|
-
async def find_sensor(self, idx: int, name: str) -> Optional[G90Sensor]:
|
|
288
|
+
async def find_sensor(
|
|
289
|
+
self, idx: int, name: str, exclude_unavailable: bool = True
|
|
290
|
+
) -> Optional[G90Sensor]:
|
|
306
291
|
"""
|
|
307
292
|
Finds sensor by index and name.
|
|
308
293
|
|
|
309
294
|
:param idx: Sensor index
|
|
310
295
|
:param name: Sensor name
|
|
296
|
+
:param exclude_unavailable: Flag indicating if unavailable sensors
|
|
297
|
+
should be excluded from the search
|
|
311
298
|
:return: Sensor instance
|
|
312
299
|
"""
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
# Fast lookup by direct index
|
|
316
|
-
if idx < len(sensors) and sensors[idx].name == name:
|
|
317
|
-
sensor = sensors[idx]
|
|
318
|
-
_LOGGER.debug('Found sensor via fast lookup: %s', sensor)
|
|
319
|
-
return sensor
|
|
320
|
-
|
|
321
|
-
# Fast lookup failed, perform slow one over the whole sensors list
|
|
322
|
-
for sensor in sensors:
|
|
323
|
-
if sensor.index == idx and sensor.name == name:
|
|
324
|
-
_LOGGER.debug('Found sensor: %s', sensor)
|
|
325
|
-
return sensor
|
|
326
|
-
|
|
327
|
-
_LOGGER.error('Sensor not found: idx=%s, name=%s', idx, name)
|
|
328
|
-
return None
|
|
300
|
+
return await self._sensors.find(idx, name, exclude_unavailable)
|
|
329
301
|
|
|
330
302
|
@property
|
|
331
303
|
async def devices(self) -> List[G90Device]:
|
|
332
304
|
"""
|
|
333
|
-
|
|
334
|
-
|
|
305
|
+
Returns the list of devices (switches) configured in the device. Please
|
|
306
|
+
note it doesn't update those from the panel except initially when
|
|
307
|
+
the list if empty.
|
|
308
|
+
|
|
309
|
+
:return: List of devices
|
|
335
310
|
"""
|
|
336
|
-
return await self.
|
|
311
|
+
return await self._devices.entities
|
|
337
312
|
|
|
338
313
|
async def get_devices(self) -> List[G90Device]:
|
|
339
314
|
"""
|
|
340
|
-
Provides list of devices (switches) configured in the device
|
|
341
|
-
|
|
342
|
-
the class to reflect any updates there. Multi-node devices, those
|
|
315
|
+
Provides list of devices (switches) configured in the device, updating
|
|
316
|
+
them from panel on each call. Multi-node devices, those
|
|
343
317
|
having multiple ports, are expanded into corresponding number of
|
|
344
318
|
resulting entries.
|
|
345
319
|
|
|
346
320
|
:return: List of devices
|
|
347
321
|
"""
|
|
348
|
-
|
|
349
|
-
async with self._devices_lock:
|
|
350
|
-
if not self._devices:
|
|
351
|
-
devices = self.paginated_result(
|
|
352
|
-
G90Commands.GETDEVICELIST
|
|
353
|
-
)
|
|
354
|
-
async for device in devices:
|
|
355
|
-
obj = G90Device(
|
|
356
|
-
*device.data, parent=self, subindex=0,
|
|
357
|
-
proto_idx=device.proto_idx
|
|
358
|
-
)
|
|
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)
|
|
368
|
-
|
|
369
|
-
_LOGGER.debug(
|
|
370
|
-
'Total number of devices: %s', len(self._devices)
|
|
371
|
-
)
|
|
372
|
-
|
|
373
|
-
return self._devices
|
|
322
|
+
return await self._devices.update()
|
|
374
323
|
|
|
375
324
|
@property
|
|
376
325
|
async def host_info(self) -> G90HostInfo:
|
|
@@ -637,7 +586,7 @@ class G90Alarm(G90DeviceNotifications):
|
|
|
637
586
|
|
|
638
587
|
# Reset the tampered and door open when arming flags on all sensors
|
|
639
588
|
# having those set
|
|
640
|
-
for sensor in await self.
|
|
589
|
+
for sensor in await self.sensors:
|
|
641
590
|
if sensor.is_tampered:
|
|
642
591
|
# pylint: disable=protected-access
|
|
643
592
|
sensor._set_tampered(False)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Copyright (c) 2025 Ilia Sotnikov
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
|
11
|
+
# all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
# SOFTWARE.
|
|
20
|
+
"""
|
|
21
|
+
Base entity.
|
|
22
|
+
"""
|
|
23
|
+
from abc import ABC, abstractmethod
|
|
24
|
+
# `Self` has been introduced in Python 3.11, need to use `typing_extensions`
|
|
25
|
+
# for earlier versions
|
|
26
|
+
try:
|
|
27
|
+
from typing import Self # type: ignore[attr-defined,unused-ignore]
|
|
28
|
+
except ImportError:
|
|
29
|
+
from typing_extensions import Self
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class G90BaseEntity(ABC):
|
|
33
|
+
"""
|
|
34
|
+
Base entity class.
|
|
35
|
+
|
|
36
|
+
Contains minimal set of method for :class:`.G90BaseList` class
|
|
37
|
+
"""
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def update(
|
|
40
|
+
self,
|
|
41
|
+
obj: Self # pylint: disable=used-before-assignment
|
|
42
|
+
) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Update the entity from another one.
|
|
45
|
+
|
|
46
|
+
:param obj: Object to update from.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def is_unavailable(self) -> bool:
|
|
52
|
+
"""
|
|
53
|
+
Check if the entity is unavailable.
|
|
54
|
+
|
|
55
|
+
:return: True if the entity is unavailable.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
@is_unavailable.setter
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def is_unavailable(self, value: bool) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Set the entity as unavailable.
|
|
63
|
+
|
|
64
|
+
:param value: Value to set.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def name(self) -> str:
|
|
70
|
+
"""
|
|
71
|
+
Get the name of the entity.
|
|
72
|
+
|
|
73
|
+
:return: Name of the entity.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def index(self) -> int:
|
|
79
|
+
"""
|
|
80
|
+
Get the index of the entity.
|
|
81
|
+
|
|
82
|
+
:return: Index of the entity.
|
|
83
|
+
"""
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Copyright (c) 2025 Ilia Sotnikov
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
|
11
|
+
# all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
# SOFTWARE.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
Base entity list.
|
|
23
|
+
"""
|
|
24
|
+
from abc import ABC, abstractmethod
|
|
25
|
+
from typing import (
|
|
26
|
+
List, AsyncGenerator, Optional, TypeVar, Generic, cast, TYPE_CHECKING,
|
|
27
|
+
)
|
|
28
|
+
import asyncio
|
|
29
|
+
import logging
|
|
30
|
+
|
|
31
|
+
from .base_entity import G90BaseEntity
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from ..alarm import G90Alarm
|
|
34
|
+
else:
|
|
35
|
+
# Alias G90Alarm to object avoid circular imports
|
|
36
|
+
# (`G90Alarm` -> `G90SensorList` -> `G90BaseList` -> `G90Alarm`)
|
|
37
|
+
G90Alarm = object
|
|
38
|
+
|
|
39
|
+
T = TypeVar('T', bound=G90BaseEntity)
|
|
40
|
+
_LOGGER = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class G90BaseList(Generic[T], ABC):
|
|
44
|
+
"""
|
|
45
|
+
Base entity list class.
|
|
46
|
+
|
|
47
|
+
:param parent: Parent alarm panel instance.
|
|
48
|
+
"""
|
|
49
|
+
def __init__(self, parent: G90Alarm) -> None:
|
|
50
|
+
self._entities: List[T] = []
|
|
51
|
+
self._lock = asyncio.Lock()
|
|
52
|
+
self._parent = parent
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
async def _fetch(self) -> AsyncGenerator[T, None]:
|
|
56
|
+
"""
|
|
57
|
+
Fetch the list of entities from the panel.
|
|
58
|
+
|
|
59
|
+
:return: Async generator of entities
|
|
60
|
+
"""
|
|
61
|
+
yield cast(T, None)
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
async def entities(self) -> List[T]:
|
|
65
|
+
"""
|
|
66
|
+
Return the list of entities.
|
|
67
|
+
|
|
68
|
+
:meth:`update` is called if the list is empty.
|
|
69
|
+
|
|
70
|
+
:return: List of entities
|
|
71
|
+
"""
|
|
72
|
+
# Please see below for the explanation of the lock usage
|
|
73
|
+
async with self._lock:
|
|
74
|
+
entities = self._entities
|
|
75
|
+
|
|
76
|
+
if not entities:
|
|
77
|
+
return await self.update()
|
|
78
|
+
|
|
79
|
+
return entities
|
|
80
|
+
|
|
81
|
+
async def update(self) -> List[T]:
|
|
82
|
+
"""
|
|
83
|
+
Update the list of entities from the panel.
|
|
84
|
+
|
|
85
|
+
:return: List of entities
|
|
86
|
+
"""
|
|
87
|
+
# Use lock around the operation, to ensure no duplicated entries in the
|
|
88
|
+
# resulting list or redundant exchanges with panel are made when the
|
|
89
|
+
# method is called concurrently
|
|
90
|
+
async with self._lock:
|
|
91
|
+
entities = self._fetch()
|
|
92
|
+
|
|
93
|
+
non_existing_entities = self._entities.copy()
|
|
94
|
+
async for entity in entities:
|
|
95
|
+
try:
|
|
96
|
+
existing_entity_idx = self._entities.index(entity)
|
|
97
|
+
except ValueError:
|
|
98
|
+
existing_entity_idx = None
|
|
99
|
+
|
|
100
|
+
if existing_entity_idx is not None:
|
|
101
|
+
existing_entity = self._entities[existing_entity_idx]
|
|
102
|
+
# Update the existing entity with the new data
|
|
103
|
+
_LOGGER.debug(
|
|
104
|
+
"Updating existing entity '%s' from protocol"
|
|
105
|
+
" data '%s'", existing_entity, entity
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
self._entities[existing_entity_idx].update(entity)
|
|
109
|
+
non_existing_entities.remove(entity)
|
|
110
|
+
else:
|
|
111
|
+
# Add the new entity to the list
|
|
112
|
+
_LOGGER.debug('Adding new entity: %s', entity)
|
|
113
|
+
self._entities.append(entity)
|
|
114
|
+
|
|
115
|
+
# Mark the entities that are no longer in the list
|
|
116
|
+
for unavailable_entity in non_existing_entities:
|
|
117
|
+
_LOGGER.debug(
|
|
118
|
+
'Marking entity as unavailable: %s', unavailable_entity
|
|
119
|
+
)
|
|
120
|
+
unavailable_entity.is_unavailable = True
|
|
121
|
+
|
|
122
|
+
_LOGGER.debug(
|
|
123
|
+
'Total number of entities: %s, unavailable: %s',
|
|
124
|
+
len(self._entities), len(non_existing_entities)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
return self._entities
|
|
128
|
+
|
|
129
|
+
async def find(
|
|
130
|
+
self, idx: int, name: str, exclude_unavailable: bool
|
|
131
|
+
) -> Optional[T]:
|
|
132
|
+
"""
|
|
133
|
+
Finds entity by index and name.
|
|
134
|
+
|
|
135
|
+
:param idx: Entity index
|
|
136
|
+
:param name: Entity name
|
|
137
|
+
:param exclude_unavailable: Exclude unavailable entities
|
|
138
|
+
:return: Entity instance
|
|
139
|
+
"""
|
|
140
|
+
entities = await self.entities
|
|
141
|
+
|
|
142
|
+
found = None
|
|
143
|
+
# Fast lookup by direct index
|
|
144
|
+
if idx < len(entities) and entities[idx].name == name:
|
|
145
|
+
entity = entities[idx]
|
|
146
|
+
_LOGGER.debug('Found entity via fast lookup: %s', entity)
|
|
147
|
+
found = entity
|
|
148
|
+
|
|
149
|
+
# Fast lookup failed, perform slow one over the whole entities list
|
|
150
|
+
if not found:
|
|
151
|
+
for entity in entities:
|
|
152
|
+
if entity.index == idx and entity.name == name:
|
|
153
|
+
_LOGGER.debug('Found entity: %s', entity)
|
|
154
|
+
found = entity
|
|
155
|
+
|
|
156
|
+
if found:
|
|
157
|
+
if not exclude_unavailable or not found.is_unavailable:
|
|
158
|
+
return found
|
|
159
|
+
|
|
160
|
+
_LOGGER.debug(
|
|
161
|
+
'Entity is found but unavailable, will result in none returned'
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
_LOGGER.error('Entity not found: idx=%s, name=%s', idx, name)
|
|
165
|
+
return None
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Copyright (c) 2025 Ilia Sotnikov
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
|
11
|
+
# all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
# SOFTWARE.
|
|
20
|
+
"""
|
|
21
|
+
Device list.
|
|
22
|
+
"""
|
|
23
|
+
from typing import AsyncGenerator
|
|
24
|
+
from .device import G90Device
|
|
25
|
+
from .base_list import G90BaseList
|
|
26
|
+
from ..const import G90Commands
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class G90DeviceList(G90BaseList[G90Device]):
|
|
30
|
+
"""
|
|
31
|
+
Device list class.
|
|
32
|
+
"""
|
|
33
|
+
async def _fetch(self) -> AsyncGenerator[G90Device, None]:
|
|
34
|
+
"""
|
|
35
|
+
Fetch the list of devices from the panel.
|
|
36
|
+
|
|
37
|
+
:yields: G90Device: Device entity.
|
|
38
|
+
"""
|
|
39
|
+
devices = self._parent.paginated_result(
|
|
40
|
+
G90Commands.GETDEVICELIST
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
async for device in devices:
|
|
44
|
+
obj = G90Device(
|
|
45
|
+
*device.data, parent=self._parent, subindex=0,
|
|
46
|
+
proto_idx=device.proto_idx
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
yield obj
|
|
50
|
+
|
|
51
|
+
# Multi-node devices (first node has already been handled
|
|
52
|
+
# above)
|
|
53
|
+
for node in range(1, obj.node_count):
|
|
54
|
+
obj = G90Device(
|
|
55
|
+
*device.data, parent=self._parent,
|
|
56
|
+
subindex=node, proto_idx=device.proto_idx
|
|
57
|
+
)
|
|
58
|
+
yield obj
|
|
@@ -31,6 +31,7 @@ from typing import (
|
|
|
31
31
|
from enum import IntEnum, IntFlag
|
|
32
32
|
from ..definitions.sensors import SENSOR_DEFINITIONS, SensorDefinition
|
|
33
33
|
from ..const import G90Commands
|
|
34
|
+
from .base_entity import G90BaseEntity
|
|
34
35
|
if TYPE_CHECKING:
|
|
35
36
|
from ..alarm import (
|
|
36
37
|
G90Alarm, SensorStateCallback, SensorLowBatteryCallback,
|
|
@@ -171,7 +172,7 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
171
172
|
|
|
172
173
|
|
|
173
174
|
# pylint: disable=too-many-public-methods
|
|
174
|
-
class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
175
|
+
class G90Sensor(G90BaseEntity): # pylint:disable=too-many-instance-attributes
|
|
175
176
|
"""
|
|
176
177
|
Interacts with sensor on G90 alarm panel.
|
|
177
178
|
|
|
@@ -208,6 +209,7 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
208
209
|
self._door_open_when_arming = False
|
|
209
210
|
self._proto_idx = proto_idx
|
|
210
211
|
self._extra_data: Any = None
|
|
212
|
+
self._unavailable = False
|
|
211
213
|
|
|
212
214
|
self._definition: Optional[SensorDefinition] = None
|
|
213
215
|
# Get sensor definition corresponds to the sensor type/subtype if any
|
|
@@ -219,6 +221,15 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
219
221
|
self._definition = s_def
|
|
220
222
|
break
|
|
221
223
|
|
|
224
|
+
def update(self, obj: G90Sensor) -> None:
|
|
225
|
+
"""
|
|
226
|
+
Updates sensor from another instance.
|
|
227
|
+
|
|
228
|
+
:param obj: Sensor instance to update from
|
|
229
|
+
"""
|
|
230
|
+
self._protocol_data = obj.protocol_data
|
|
231
|
+
self._proto_idx = obj.proto_idx
|
|
232
|
+
|
|
222
233
|
@property
|
|
223
234
|
def name(self) -> str:
|
|
224
235
|
"""
|
|
@@ -396,6 +407,16 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
396
407
|
"""
|
|
397
408
|
return self._subindex
|
|
398
409
|
|
|
410
|
+
@property
|
|
411
|
+
def proto_idx(self) -> int:
|
|
412
|
+
"""
|
|
413
|
+
Index of the sensor within list of sensors as retrieved from the alarm
|
|
414
|
+
panel.
|
|
415
|
+
|
|
416
|
+
:return: Index of sensor in list of sensors.
|
|
417
|
+
"""
|
|
418
|
+
return self._proto_idx
|
|
419
|
+
|
|
399
420
|
@property
|
|
400
421
|
def supports_enable_disable(self) -> bool:
|
|
401
422
|
"""
|
|
@@ -405,6 +426,15 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
405
426
|
"""
|
|
406
427
|
return self._definition is not None
|
|
407
428
|
|
|
429
|
+
@property
|
|
430
|
+
def protocol_data(self) -> G90SensorIncomingData:
|
|
431
|
+
"""
|
|
432
|
+
Protocol data of the sensor.
|
|
433
|
+
|
|
434
|
+
:return: Protocol data
|
|
435
|
+
"""
|
|
436
|
+
return self._protocol_data
|
|
437
|
+
|
|
408
438
|
@property
|
|
409
439
|
def is_wireless(self) -> bool:
|
|
410
440
|
"""
|
|
@@ -532,11 +562,11 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
532
562
|
# when instantiated.
|
|
533
563
|
_LOGGER.debug(
|
|
534
564
|
'Refreshing sensor at index=%s, position in protocol list=%s',
|
|
535
|
-
self.index, self.
|
|
565
|
+
self.index, self.proto_idx
|
|
536
566
|
)
|
|
537
567
|
sensors_result = self.parent.paginated_result(
|
|
538
568
|
G90Commands.GETSENSORLIST,
|
|
539
|
-
start=self.
|
|
569
|
+
start=self.proto_idx, end=self.proto_idx
|
|
540
570
|
)
|
|
541
571
|
sensors = [x.data async for x in sensors_result]
|
|
542
572
|
|
|
@@ -633,6 +663,17 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
633
663
|
def extra_data(self, val: Any) -> None:
|
|
634
664
|
self._extra_data = val
|
|
635
665
|
|
|
666
|
+
@property
|
|
667
|
+
def is_unavailable(self) -> bool:
|
|
668
|
+
"""
|
|
669
|
+
Indicates if the sensor is unavailable (e.g. has been removed).
|
|
670
|
+
"""
|
|
671
|
+
return self._unavailable
|
|
672
|
+
|
|
673
|
+
@is_unavailable.setter
|
|
674
|
+
def is_unavailable(self, value: bool) -> None:
|
|
675
|
+
self._unavailable = value
|
|
676
|
+
|
|
636
677
|
def _asdict(self) -> Dict[str, Any]:
|
|
637
678
|
"""
|
|
638
679
|
Returns dictionary representation of the sensor.
|
|
@@ -644,6 +685,7 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
644
685
|
'type': self.type,
|
|
645
686
|
'subtype': self.subtype,
|
|
646
687
|
'index': self.index,
|
|
688
|
+
'protocol_index': self.proto_idx,
|
|
647
689
|
'subindex': self.subindex,
|
|
648
690
|
'node_count': self.node_count,
|
|
649
691
|
'protocol': self.protocol,
|
|
@@ -657,6 +699,7 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
657
699
|
'is_low_battery': self.is_low_battery,
|
|
658
700
|
'is_tampered': self.is_tampered,
|
|
659
701
|
'is_door_open_when_arming': self.is_door_open_when_arming,
|
|
702
|
+
'is_unavailable': self.is_unavailable,
|
|
660
703
|
}
|
|
661
704
|
|
|
662
705
|
def __repr__(self) -> str:
|
|
@@ -666,3 +709,20 @@ class G90Sensor: # pylint:disable=too-many-instance-attributes
|
|
|
666
709
|
:return: String representation
|
|
667
710
|
"""
|
|
668
711
|
return super().__repr__() + f'({repr(self._asdict())})'
|
|
712
|
+
|
|
713
|
+
def __eq__(self, value: object) -> bool:
|
|
714
|
+
"""
|
|
715
|
+
Compares the sensor with another object.
|
|
716
|
+
|
|
717
|
+
:param value: Object to compare with
|
|
718
|
+
:return: If the sensor is equal to the object
|
|
719
|
+
"""
|
|
720
|
+
if not isinstance(value, G90Sensor):
|
|
721
|
+
return False
|
|
722
|
+
|
|
723
|
+
return (
|
|
724
|
+
self.type == value.type
|
|
725
|
+
and self.subtype == value.subtype
|
|
726
|
+
and self.index == value.index
|
|
727
|
+
and self.name == value.name
|
|
728
|
+
)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Copyright (c) 2025 Ilia Sotnikov
|
|
2
|
+
#
|
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
# furnished to do so, subject to the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
|
11
|
+
# all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
# SOFTWARE.
|
|
20
|
+
"""
|
|
21
|
+
Sensor list.
|
|
22
|
+
"""
|
|
23
|
+
from typing import (
|
|
24
|
+
AsyncGenerator
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from .sensor import G90Sensor
|
|
28
|
+
from .base_list import G90BaseList
|
|
29
|
+
from ..const import G90Commands
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class G90SensorList(G90BaseList[G90Sensor]):
|
|
33
|
+
"""
|
|
34
|
+
Sensor list class.
|
|
35
|
+
"""
|
|
36
|
+
async def _fetch(self) -> AsyncGenerator[G90Sensor, None]:
|
|
37
|
+
"""
|
|
38
|
+
Fetch the list of sensors from the panel.
|
|
39
|
+
|
|
40
|
+
:yields: G90Sensor: Sensor entity.
|
|
41
|
+
"""
|
|
42
|
+
entities = self._parent.paginated_result(
|
|
43
|
+
G90Commands.GETSENSORLIST
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
async for entity in entities:
|
|
47
|
+
yield G90Sensor(
|
|
48
|
+
*entity.data, parent=self._parent, subindex=0,
|
|
49
|
+
proto_idx=entity.proto_idx
|
|
50
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: pyg90alarm
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.20.0
|
|
4
4
|
Summary: G90 Alarm system protocol
|
|
5
5
|
Home-page: https://github.com/hostcc/pyg90alarm
|
|
6
6
|
Author: Ilia Sotnikov
|
|
@@ -33,6 +33,17 @@ Requires-Dist: asynctest; extra == "test"
|
|
|
33
33
|
Provides-Extra: docs
|
|
34
34
|
Requires-Dist: sphinx; extra == "docs"
|
|
35
35
|
Requires-Dist: sphinx-rtd-theme; extra == "docs"
|
|
36
|
+
Dynamic: author
|
|
37
|
+
Dynamic: author-email
|
|
38
|
+
Dynamic: classifier
|
|
39
|
+
Dynamic: description
|
|
40
|
+
Dynamic: description-content-type
|
|
41
|
+
Dynamic: home-page
|
|
42
|
+
Dynamic: keywords
|
|
43
|
+
Dynamic: project-url
|
|
44
|
+
Dynamic: provides-extra
|
|
45
|
+
Dynamic: requires-python
|
|
46
|
+
Dynamic: summary
|
|
36
47
|
|
|
37
48
|
.. image:: https://github.com/hostcc/pyg90alarm/actions/workflows/main.yml/badge.svg?branch=master
|
|
38
49
|
:target: https://github.com/hostcc/pyg90alarm/tree/master
|
|
@@ -43,8 +43,12 @@ src/pyg90alarm.egg-info/top_level.txt
|
|
|
43
43
|
src/pyg90alarm/definitions/__init__.py
|
|
44
44
|
src/pyg90alarm/definitions/sensors.py
|
|
45
45
|
src/pyg90alarm/entities/__init__.py
|
|
46
|
+
src/pyg90alarm/entities/base_entity.py
|
|
47
|
+
src/pyg90alarm/entities/base_list.py
|
|
46
48
|
src/pyg90alarm/entities/device.py
|
|
49
|
+
src/pyg90alarm/entities/device_list.py
|
|
47
50
|
src/pyg90alarm/entities/sensor.py
|
|
51
|
+
src/pyg90alarm/entities/sensor_list.py
|
|
48
52
|
tests/__init__.py
|
|
49
53
|
tests/conftest.py
|
|
50
54
|
tests/device_mock.py
|
|
@@ -152,9 +152,9 @@ async def test_get_devices_concurrent(mock_device: DeviceMock) -> None:
|
|
|
152
152
|
# Issue two concurrent requests to retrieve devices
|
|
153
153
|
res = await asyncio.gather(g90.get_devices(), g90.get_devices())
|
|
154
154
|
# Ensure only single exchange with the panel
|
|
155
|
-
g90.paginated_result.
|
|
155
|
+
assert g90.paginated_result.call_count == 2
|
|
156
156
|
# While `pylint` demands use of generator, the comprehension is used
|
|
157
|
-
# instead for ease of
|
|
157
|
+
# instead for ease of troubleshooting test failures as it will show the
|
|
158
158
|
# list elements, not just generator instance
|
|
159
159
|
# pylint: disable=use-a-generator
|
|
160
160
|
assert all([len(x) == 1 for x in res])
|
|
@@ -224,10 +224,8 @@ async def test_single_sensor(mock_device: DeviceMock) -> None:
|
|
|
224
224
|
"""
|
|
225
225
|
g90 = G90Alarm(host=mock_device.host, port=mock_device.port)
|
|
226
226
|
|
|
227
|
-
sensors = await g90.
|
|
228
|
-
prop_sensors = await g90.sensors
|
|
227
|
+
sensors = await g90.sensors
|
|
229
228
|
|
|
230
|
-
assert sensors == prop_sensors
|
|
231
229
|
assert mock_device.recv_data == [
|
|
232
230
|
b'ISTART[102,102,[102,[1,10]]]IEND\0',
|
|
233
231
|
]
|
|
@@ -255,11 +253,78 @@ async def test_get_sensors_concurrent(mock_device: DeviceMock) -> None:
|
|
|
255
253
|
)
|
|
256
254
|
|
|
257
255
|
res = await asyncio.gather(g90.get_sensors(), g90.get_sensors())
|
|
258
|
-
g90.paginated_result.
|
|
256
|
+
assert g90.paginated_result.call_count == 2
|
|
259
257
|
# pylint: disable=use-a-generator
|
|
260
258
|
assert all([len(x) == 1 for x in res])
|
|
261
259
|
|
|
262
260
|
|
|
261
|
+
@pytest.mark.g90device(sent_data=[
|
|
262
|
+
b'ISTART[102,'
|
|
263
|
+
b'[[1,1,1],["Remote",10,0,10,1,0,32,0,0,16,1,0,""]]]IEND\0',
|
|
264
|
+
b'ISTART[102,'
|
|
265
|
+
b'[[1,1,1],["Remote",10,0,10,1,0,33,0,0,16,1,0,""]]]IEND\0',
|
|
266
|
+
b'ISTART[102,'
|
|
267
|
+
b'[[1,1,1],["Remote 2",11,0,10,1,0,33,0,0,16,1,0,""]]]IEND\0',
|
|
268
|
+
])
|
|
269
|
+
async def test_get_sensors_update(mock_device: DeviceMock) -> None:
|
|
270
|
+
"""
|
|
271
|
+
Verifies updating the sensor list from the panel properly updates entitries
|
|
272
|
+
exists in the list already, and marks those are not.
|
|
273
|
+
"""
|
|
274
|
+
g90 = G90Alarm(host=mock_device.host, port=mock_device.port)
|
|
275
|
+
|
|
276
|
+
# Ensure initial update results in single list entry
|
|
277
|
+
sensors = await g90.get_sensors()
|
|
278
|
+
assert len(sensors) == 1
|
|
279
|
+
assert sensors[0].name == 'Remote'
|
|
280
|
+
assert sensors[0].enabled is False
|
|
281
|
+
# Check if existing enty is properly updated
|
|
282
|
+
sensors = await g90.get_sensors()
|
|
283
|
+
assert len(sensors) == 1
|
|
284
|
+
assert sensors[0].name == 'Remote'
|
|
285
|
+
assert sensors[0].enabled is True
|
|
286
|
+
|
|
287
|
+
sensor_1 = sensors[0]
|
|
288
|
+
|
|
289
|
+
# Ensure subsequent update results in two list entries, one being added and
|
|
290
|
+
# another one is marked as unavailable (since it isn't present in the list
|
|
291
|
+
# fetched from the device)
|
|
292
|
+
sensors = await g90.get_sensors()
|
|
293
|
+
assert len(sensors) == 2
|
|
294
|
+
assert sensors[0].is_unavailable is True
|
|
295
|
+
assert sensors[0].name == 'Remote'
|
|
296
|
+
|
|
297
|
+
assert sensors[1].is_unavailable is False
|
|
298
|
+
assert sensors[1].name == 'Remote 2'
|
|
299
|
+
|
|
300
|
+
# Ensure the first entry is still in the list
|
|
301
|
+
assert sensors[0] == sensor_1
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@pytest.mark.g90device(sent_data=[
|
|
305
|
+
b'ISTART[102,'
|
|
306
|
+
b'[[1,1,1],["Remote",10,0,10,1,0,32,0,0,16,1,0,""]]]IEND\0',
|
|
307
|
+
b'ISTART[102,'
|
|
308
|
+
b'[[1,1,1],["Remote 2",11,0,10,1,0,33,0,0,16,1,0,""]]]IEND\0',
|
|
309
|
+
])
|
|
310
|
+
async def test_find_sensor(mock_device: DeviceMock) -> None:
|
|
311
|
+
"""
|
|
312
|
+
Verifies updating the sensor list from the panel properly updates entitries
|
|
313
|
+
exists in the list already, and marks those are not.
|
|
314
|
+
"""
|
|
315
|
+
g90 = G90Alarm(host=mock_device.host, port=mock_device.port)
|
|
316
|
+
|
|
317
|
+
sensor = await g90.find_sensor(10, 'Remote')
|
|
318
|
+
assert sensor is not None
|
|
319
|
+
assert sensor.name == 'Remote'
|
|
320
|
+
|
|
321
|
+
await g90.get_sensors()
|
|
322
|
+
sensor = await g90.find_sensor(10, 'Remote')
|
|
323
|
+
assert sensor is None
|
|
324
|
+
sensor = await g90.find_sensor(10, 'Remote', exclude_unavailable=False)
|
|
325
|
+
assert sensor is not None
|
|
326
|
+
|
|
327
|
+
|
|
263
328
|
@pytest.mark.g90device(sent_data=[
|
|
264
329
|
b'ISTART[102,'
|
|
265
330
|
b'[[3,1,3],["Remote 1",10,0,10,1,0,32,0,0,16,1,0,""],'
|
|
@@ -276,10 +341,8 @@ async def test_multiple_sensors_shorter_than_page(
|
|
|
276
341
|
"""
|
|
277
342
|
g90 = G90Alarm(host=mock_device.host, port=mock_device.port)
|
|
278
343
|
|
|
279
|
-
sensors = await g90.
|
|
280
|
-
prop_sensors = await g90.sensors
|
|
344
|
+
sensors = await g90.sensors
|
|
281
345
|
|
|
282
|
-
assert sensors == prop_sensors
|
|
283
346
|
assert mock_device.recv_data == [
|
|
284
347
|
b'ISTART[102,102,[102,[1,10]]]IEND\0',
|
|
285
348
|
]
|
|
@@ -327,10 +390,8 @@ async def test_multiple_sensors_longer_than_page(
|
|
|
327
390
|
"""
|
|
328
391
|
g90 = G90Alarm(host=mock_device.host, port=mock_device.port)
|
|
329
392
|
|
|
330
|
-
sensors = await g90.
|
|
331
|
-
prop_sensors = await g90.sensors
|
|
393
|
+
sensors = await g90.sensors
|
|
332
394
|
|
|
333
|
-
assert sensors == prop_sensors
|
|
334
395
|
assert mock_device.recv_data == [
|
|
335
396
|
b'ISTART[102,102,[102,[1,10]]]IEND\0',
|
|
336
397
|
b'ISTART[102,102,[102,[11,11]]]IEND\0',
|
|
@@ -362,10 +423,8 @@ async def test_sensor_callback(mock_device: DeviceMock) -> None:
|
|
|
362
423
|
notifications_local_host=mock_device.notification_host,
|
|
363
424
|
notifications_local_port=mock_device.notification_port)
|
|
364
425
|
|
|
365
|
-
sensors = await g90.
|
|
366
|
-
prop_sensors = await g90.sensors
|
|
426
|
+
sensors = await g90.sensors
|
|
367
427
|
|
|
368
|
-
assert sensors == prop_sensors
|
|
369
428
|
future = asyncio.get_running_loop().create_future()
|
|
370
429
|
sensor = [x for x in sensors if x.index == 10 and x.name == 'Remote']
|
|
371
430
|
sensor[0].state_callback = lambda *args: future.set_result(True)
|
|
@@ -406,10 +465,8 @@ async def test_sensor_low_battery_callback(mock_device: DeviceMock) -> None:
|
|
|
406
465
|
notifications_local_host=mock_device.notification_host,
|
|
407
466
|
notifications_local_port=mock_device.notification_port)
|
|
408
467
|
|
|
409
|
-
sensors = await g90.
|
|
410
|
-
prop_sensors = await g90.sensors
|
|
468
|
+
sensors = await g90.sensors
|
|
411
469
|
|
|
412
|
-
assert sensors == prop_sensors
|
|
413
470
|
future = asyncio.get_running_loop().create_future()
|
|
414
471
|
sensor = [x for x in sensors if x.index == 26 and x.name == 'Remote']
|
|
415
472
|
low_battery_sensor_cb = MagicMock()
|
|
@@ -459,10 +516,8 @@ async def test_sensor_door_open_when_arming_callback(
|
|
|
459
516
|
notifications_local_host=mock_device.notification_host,
|
|
460
517
|
notifications_local_port=mock_device.notification_port)
|
|
461
518
|
|
|
462
|
-
sensors = await g90.
|
|
463
|
-
prop_sensors = await g90.sensors
|
|
519
|
+
sensors = await g90.sensors
|
|
464
520
|
|
|
465
|
-
assert sensors == prop_sensors
|
|
466
521
|
future = asyncio.get_running_loop().create_future()
|
|
467
522
|
sensor = [x for x in sensors if x.index == 21 and x.name == 'Hall']
|
|
468
523
|
door_open_when_arming_sensor_cb = MagicMock()
|
|
@@ -558,9 +613,7 @@ async def test_door_open_close_callback(mock_device: DeviceMock) -> None:
|
|
|
558
613
|
await mock_device.send_next_notification()
|
|
559
614
|
await asyncio.wait([future], timeout=0.1)
|
|
560
615
|
# Corresponding sensor should turn to occupied (=door opened)
|
|
561
|
-
sensors = await g90.
|
|
562
|
-
prop_sensors = await g90.sensors
|
|
563
|
-
assert sensors == prop_sensors
|
|
616
|
+
sensors = await g90.sensors
|
|
564
617
|
assert sensors[0].occupancy
|
|
565
618
|
|
|
566
619
|
# Simulate second device alert for door close
|
|
@@ -571,9 +624,7 @@ async def test_door_open_close_callback(mock_device: DeviceMock) -> None:
|
|
|
571
624
|
|
|
572
625
|
await asyncio.wait([future], timeout=0.1)
|
|
573
626
|
# The sensor should become inactive (=door closed)
|
|
574
|
-
sensors = await g90.
|
|
575
|
-
prop_sensors = await g90.sensors
|
|
576
|
-
assert sensors == prop_sensors
|
|
627
|
+
sensors = await g90.sensors
|
|
577
628
|
assert not sensors[0].occupancy
|
|
578
629
|
|
|
579
630
|
g90.close_device_notifications()
|
|
@@ -608,7 +659,7 @@ async def test_alarm_callback(mock_device: DeviceMock) -> None:
|
|
|
608
659
|
g90 = G90Alarm(host=mock_device.host, port=mock_device.port,
|
|
609
660
|
notifications_local_host=mock_device.notification_host,
|
|
610
661
|
notifications_local_port=mock_device.notification_port)
|
|
611
|
-
sensors = await g90.
|
|
662
|
+
sensors = await g90.sensors
|
|
612
663
|
# Set extra data for the 1st sensor
|
|
613
664
|
sensors[0].extra_data = 'Dummy extra data'
|
|
614
665
|
|
|
@@ -668,10 +719,8 @@ async def test_sensor_tamper_callback(
|
|
|
668
719
|
notifications_local_host=mock_device.notification_host,
|
|
669
720
|
notifications_local_port=mock_device.notification_port)
|
|
670
721
|
|
|
671
|
-
sensors = await g90.
|
|
672
|
-
prop_sensors = await g90.sensors
|
|
722
|
+
sensors = await g90.sensors
|
|
673
723
|
|
|
674
|
-
assert sensors == prop_sensors
|
|
675
724
|
future = asyncio.get_running_loop().create_future()
|
|
676
725
|
sensor = [x for x in sensors if x.index == 100 and x.name == 'Hall']
|
|
677
726
|
tamper_sensor_cb = MagicMock()
|
|
@@ -984,9 +1033,7 @@ async def test_sensor_disable(mock_device: DeviceMock) -> None:
|
|
|
984
1033
|
Tests for disabling a sensor.
|
|
985
1034
|
"""
|
|
986
1035
|
g90 = G90Alarm(host=mock_device.host, port=mock_device.port)
|
|
987
|
-
sensors = await g90.
|
|
988
|
-
prop_sensors = await g90.sensors
|
|
989
|
-
assert sensors == prop_sensors
|
|
1036
|
+
sensors = await g90.sensors
|
|
990
1037
|
assert sensors[1].enabled
|
|
991
1038
|
await sensors[1].set_enabled(False)
|
|
992
1039
|
assert not sensors[1].enabled
|
|
@@ -1019,9 +1066,7 @@ async def test_sensor_disable_externally_modified(
|
|
|
1019
1066
|
"""
|
|
1020
1067
|
g90 = G90Alarm(host=mock_device.host, port=mock_device.port)
|
|
1021
1068
|
|
|
1022
|
-
sensors = await g90.
|
|
1023
|
-
prop_sensors = await g90.sensors
|
|
1024
|
-
assert sensors == prop_sensors
|
|
1069
|
+
sensors = await g90.sensors
|
|
1025
1070
|
assert sensors[1].enabled
|
|
1026
1071
|
await sensors[1].set_enabled(False)
|
|
1027
1072
|
assert sensors[1].enabled
|
|
@@ -1043,9 +1088,7 @@ async def test_sensor_unsupported_disable(mock_device: DeviceMock) -> None:
|
|
|
1043
1088
|
"""
|
|
1044
1089
|
g90 = G90Alarm(host=mock_device.host, port=mock_device.port)
|
|
1045
1090
|
|
|
1046
|
-
sensors = await g90.
|
|
1047
|
-
prop_sensors = await g90.sensors
|
|
1048
|
-
assert sensors == prop_sensors
|
|
1091
|
+
sensors = await g90.sensors
|
|
1049
1092
|
assert sensors[0].enabled
|
|
1050
1093
|
await sensors[0].set_enabled(False)
|
|
1051
1094
|
assert mock_device.recv_data == [
|
|
@@ -1069,7 +1112,7 @@ async def test_sensor_disable_sensor_not_found_on_refresh(
|
|
|
1069
1112
|
"""
|
|
1070
1113
|
g90 = G90Alarm(host=mock_device.host, port=mock_device.port)
|
|
1071
1114
|
|
|
1072
|
-
sensors = await g90.
|
|
1115
|
+
sensors = await g90.sensors
|
|
1073
1116
|
assert sensors[1].enabled
|
|
1074
1117
|
await sensors[1].set_enabled(False)
|
|
1075
1118
|
assert sensors[1].enabled
|
|
@@ -1117,7 +1160,7 @@ async def test_sensor_set_user_flags(mock_device: DeviceMock) -> None:
|
|
|
1117
1160
|
Tests for setting user flags on a sensor.
|
|
1118
1161
|
"""
|
|
1119
1162
|
g90 = G90Alarm(host=mock_device.host, port=mock_device.port)
|
|
1120
|
-
sensors = await g90.
|
|
1163
|
+
sensors = await g90.sensors
|
|
1121
1164
|
await sensors[0].set_user_flag(
|
|
1122
1165
|
# Intentionally contains non-user settable flag, which should be
|
|
1123
1166
|
# ignored and not configured for the sensor that initial doesn't have
|
|
@@ -196,7 +196,7 @@ async def test_simulate_alerts_from_history(mock_device: DeviceMock) -> None:
|
|
|
196
196
|
# Stop simulating the alert from history
|
|
197
197
|
await g90.stop_simulating_alerts_from_history()
|
|
198
198
|
|
|
199
|
-
sensors = await g90.
|
|
199
|
+
sensors = await g90.sensors
|
|
200
200
|
# Ensure callbacks have been called and with expected arguments
|
|
201
201
|
alarm_cb.assert_called_once_with(33, 'Sensor 1', None)
|
|
202
202
|
armdisarm_cb.assert_called_once_with(3)
|
|
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
|
|
File without changes
|