pyg90alarm 1.19.0__py3-none-any.whl → 1.20.0__py3-none-any.whl
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/alarm.py +29 -80
- pyg90alarm/entities/base_entity.py +83 -0
- pyg90alarm/entities/base_list.py +165 -0
- pyg90alarm/entities/device_list.py +58 -0
- pyg90alarm/entities/sensor.py +63 -3
- pyg90alarm/entities/sensor_list.py +50 -0
- {pyg90alarm-1.19.0.dist-info → pyg90alarm-1.20.0.dist-info}/METADATA +13 -2
- {pyg90alarm-1.19.0.dist-info → pyg90alarm-1.20.0.dist-info}/RECORD +11 -7
- {pyg90alarm-1.19.0.dist-info → pyg90alarm-1.20.0.dist-info}/WHEEL +1 -1
- {pyg90alarm-1.19.0.dist-info → pyg90alarm-1.20.0.dist-info}/LICENSE +0 -0
- {pyg90alarm-1.19.0.dist-info → pyg90alarm-1.20.0.dist-info}/top_level.txt +0 -0
pyg90alarm/alarm.py
CHANGED
|
@@ -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
|
pyg90alarm/entities/sensor.py
CHANGED
|
@@ -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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
pyg90alarm/__init__.py,sha256=5AITRm5jZSzuQaL7PS8fZZMZb4-IuGRhSqyAdfTt0Cs,2236
|
|
2
|
-
pyg90alarm/alarm.py,sha256=
|
|
2
|
+
pyg90alarm/alarm.py,sha256=YMqf5nyoqQVZPJJB2vRXzWcHNcRjfVGojN7KDBM-KS8,38093
|
|
3
3
|
pyg90alarm/base_cmd.py,sha256=Bz7yoZ0RpkcjWARya664DKAPo3goD6BeaKtuW-hA804,9902
|
|
4
4
|
pyg90alarm/callback.py,sha256=3JsD_JChmZD24OyjaCP-PxxuBDBX7myGYhkM4RN7bk4,3742
|
|
5
5
|
pyg90alarm/config.py,sha256=2YtIgdT7clQXmYvkdn_fhIdS05CY8E1Yc90R8_tAmRI,1961
|
|
@@ -18,10 +18,14 @@ pyg90alarm/user_data_crc.py,sha256=JQBOPY3RlOgVtvR55R-rM8OuKjYW-BPXQ0W4pi6CEH0,1
|
|
|
18
18
|
pyg90alarm/definitions/__init__.py,sha256=s0NZnkW_gMH718DJbgez28z9WA231CyszUf1O_ojUiI,68
|
|
19
19
|
pyg90alarm/definitions/sensors.py,sha256=rKOu21ZpI44xk6aMh_vBjniFqnsNTc1CKwAvnv4PYLQ,19904
|
|
20
20
|
pyg90alarm/entities/__init__.py,sha256=hHb6AOiC4Tz--rOWiiICMdLaZDs1Tf_xpWk_HeS_gO4,66
|
|
21
|
+
pyg90alarm/entities/base_entity.py,sha256=Ea1r1aP7FMnOiEPhBhkrndmcr5Iy8jcfQuuXCNHajrU,2473
|
|
22
|
+
pyg90alarm/entities/base_list.py,sha256=76DUtdXTwd26vv6HIw_qdtBNd1FqMfoRf0VI9KxV5aA,5703
|
|
21
23
|
pyg90alarm/entities/device.py,sha256=f_LHvKCAqTEebZ4mrRh3CpPUI7o-OvpvOfyTRCbftJs,2818
|
|
22
|
-
pyg90alarm/entities/
|
|
23
|
-
pyg90alarm
|
|
24
|
-
pyg90alarm
|
|
25
|
-
pyg90alarm-1.
|
|
26
|
-
pyg90alarm-1.
|
|
27
|
-
pyg90alarm-1.
|
|
24
|
+
pyg90alarm/entities/device_list.py,sha256=FrsfzP1BgwCKOn6bzVUpQHdatB6MUcWhPeuaJ3c5mqg,2154
|
|
25
|
+
pyg90alarm/entities/sensor.py,sha256=ax-6ttps7sCWfuP6CcnS00Zy5WZVKleO8vAsdhU3KQc,21986
|
|
26
|
+
pyg90alarm/entities/sensor_list.py,sha256=qLNbLZXrPlcdZLoyZw3Mzf1PwX4BiW3z6SKi_NqhVnM,1806
|
|
27
|
+
pyg90alarm-1.20.0.dist-info/LICENSE,sha256=f884inRbeNv-O-hbwz62Ro_1J8xiHRTnJ2cCx6A0WvU,1070
|
|
28
|
+
pyg90alarm-1.20.0.dist-info/METADATA,sha256=UDwmoHFOiwFFQiq0yVLsV0Pl1YV2D09H8AwNtw8zESQ,7951
|
|
29
|
+
pyg90alarm-1.20.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
30
|
+
pyg90alarm-1.20.0.dist-info/top_level.txt,sha256=czHiGxYMyTk5QEDTDb0EpPiKqUMRa8zI4zx58Ii409M,11
|
|
31
|
+
pyg90alarm-1.20.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|