pyg90alarm 1.19.0__py3-none-any.whl → 2.0.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/__init__.py +5 -5
- pyg90alarm/alarm.py +159 -114
- pyg90alarm/cloud/__init__.py +31 -0
- pyg90alarm/cloud/const.py +56 -0
- pyg90alarm/cloud/messages.py +593 -0
- pyg90alarm/cloud/notifications.py +409 -0
- pyg90alarm/cloud/protocol.py +518 -0
- pyg90alarm/const.py +5 -0
- 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/local/__init__.py +0 -0
- pyg90alarm/{base_cmd.py → local/base_cmd.py} +3 -6
- pyg90alarm/{discovery.py → local/discovery.py} +1 -1
- pyg90alarm/{history.py → local/history.py} +4 -2
- pyg90alarm/{host_status.py → local/host_status.py} +1 -1
- pyg90alarm/local/notifications.py +116 -0
- pyg90alarm/{paginated_cmd.py → local/paginated_cmd.py} +2 -2
- pyg90alarm/{paginated_result.py → local/paginated_result.py} +1 -1
- pyg90alarm/{targeted_discovery.py → local/targeted_discovery.py} +2 -2
- pyg90alarm/notifications/__init__.py +0 -0
- pyg90alarm/{device_notifications.py → notifications/base.py} +115 -173
- pyg90alarm/notifications/protocol.py +116 -0
- {pyg90alarm-1.19.0.dist-info → pyg90alarm-2.0.0.dist-info}/METADATA +112 -18
- pyg90alarm-2.0.0.dist-info/RECORD +40 -0
- {pyg90alarm-1.19.0.dist-info → pyg90alarm-2.0.0.dist-info}/WHEEL +1 -1
- pyg90alarm-1.19.0.dist-info/RECORD +0 -27
- /pyg90alarm/{config.py → local/config.py} +0 -0
- /pyg90alarm/{host_info.py → local/host_info.py} +0 -0
- /pyg90alarm/{user_data_crc.py → local/user_data_crc.py} +0 -0
- {pyg90alarm-1.19.0.dist-info → pyg90alarm-2.0.0.dist-info/licenses}/LICENSE +0 -0
- {pyg90alarm-1.19.0.dist-info → pyg90alarm-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
+
)
|
|
File without changes
|
|
@@ -30,8 +30,8 @@ from asyncio.protocols import DatagramProtocol
|
|
|
30
30
|
from asyncio.transports import DatagramTransport, BaseTransport
|
|
31
31
|
from typing import Optional, Tuple, List, Any
|
|
32
32
|
from dataclasses import dataclass
|
|
33
|
-
from
|
|
34
|
-
from
|
|
33
|
+
from ..exceptions import (G90Error, G90TimeoutError)
|
|
34
|
+
from ..const import G90Commands
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -121,10 +121,7 @@ class G90BaseCommand(DatagramProtocol):
|
|
|
121
121
|
"""
|
|
122
122
|
Creates UDP connection to the alarm panel.
|
|
123
123
|
"""
|
|
124
|
-
|
|
125
|
-
loop = asyncio.get_running_loop()
|
|
126
|
-
except AttributeError:
|
|
127
|
-
loop = asyncio.get_event_loop()
|
|
124
|
+
loop = asyncio.get_running_loop()
|
|
128
125
|
|
|
129
126
|
_LOGGER.debug('Creating UDP endpoint for %s:%s',
|
|
130
127
|
self.host, self.port)
|
|
@@ -26,7 +26,7 @@ import logging
|
|
|
26
26
|
from typing import Any, Optional, Dict
|
|
27
27
|
from dataclasses import dataclass
|
|
28
28
|
from datetime import datetime, timezone
|
|
29
|
-
from
|
|
29
|
+
from ..const import (
|
|
30
30
|
G90AlertTypes,
|
|
31
31
|
G90AlertSources,
|
|
32
32
|
G90AlertStates,
|
|
@@ -34,7 +34,7 @@ from .const import (
|
|
|
34
34
|
G90HistoryStates,
|
|
35
35
|
G90RemoteButtonStates,
|
|
36
36
|
)
|
|
37
|
-
from .
|
|
37
|
+
from ..notifications.base import G90DeviceAlert
|
|
38
38
|
|
|
39
39
|
_LOGGER = logging.getLogger(__name__)
|
|
40
40
|
|
|
@@ -52,6 +52,8 @@ states_mapping_alerts = {
|
|
|
52
52
|
G90HistoryStates.TAMPER,
|
|
53
53
|
G90AlertStates.LOW_BATTERY:
|
|
54
54
|
G90HistoryStates.LOW_BATTERY,
|
|
55
|
+
G90AlertStates.ALARM:
|
|
56
|
+
G90HistoryStates.ALARM,
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
states_mapping_state_changes = {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Copyright (c) 2021 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
|
+
Implements support for notifications/alerts sent by G90 alarm panel.
|
|
23
|
+
"""
|
|
24
|
+
import logging
|
|
25
|
+
from typing import (
|
|
26
|
+
Optional, Tuple, Callable
|
|
27
|
+
)
|
|
28
|
+
import asyncio
|
|
29
|
+
from asyncio.transports import BaseTransport
|
|
30
|
+
from asyncio.protocols import DatagramProtocol
|
|
31
|
+
|
|
32
|
+
from ..notifications.base import G90NotificationsBase
|
|
33
|
+
from ..notifications.protocol import G90NotificationProtocol
|
|
34
|
+
|
|
35
|
+
_LOGGER = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class G90LocalNotifications(G90NotificationsBase, DatagramProtocol):
|
|
39
|
+
"""
|
|
40
|
+
Implements support for notifications/alerts sent by alarm panel.
|
|
41
|
+
|
|
42
|
+
There is a basic check to ensure only notifications/alerts from the correct
|
|
43
|
+
device are processed - the check uses the host and port of the device, and
|
|
44
|
+
the device ID (GUID) that is set by the ancestor class that implements the
|
|
45
|
+
commands (e.g. :class:`G90Alarm`). The latter to work correctly needs a
|
|
46
|
+
command to be performed first, one that fetches device GUID and then stores
|
|
47
|
+
it using :attr:`.device_id` (e.g. :meth:`G90Alarm.get_host_info`).
|
|
48
|
+
|
|
49
|
+
:param protocol_factory: A callable that returns a new instance of the
|
|
50
|
+
:class:`G90NotificationProtocol` class.
|
|
51
|
+
:param port: The port on which the device is listening for notifications.
|
|
52
|
+
:param host: The host on which the device is listening for notifications.
|
|
53
|
+
:param local_port: The port on which the local host is listening for
|
|
54
|
+
notifications.
|
|
55
|
+
:param local_host: The host on which the local host is listening for
|
|
56
|
+
notifications.
|
|
57
|
+
"""
|
|
58
|
+
def __init__( # pylint:disable=too-many-arguments
|
|
59
|
+
self, protocol_factory: Callable[[], G90NotificationProtocol],
|
|
60
|
+
port: int, host: str, local_port: int, local_host: str,
|
|
61
|
+
):
|
|
62
|
+
super().__init__(protocol_factory)
|
|
63
|
+
|
|
64
|
+
self._host = host
|
|
65
|
+
self._port = port
|
|
66
|
+
self._notifications_local_host = local_host
|
|
67
|
+
self._notifications_local_port = local_port
|
|
68
|
+
|
|
69
|
+
# Implementation of datagram protocol,
|
|
70
|
+
# https://docs.python.org/3/library/asyncio-protocol.html#datagram-protocols
|
|
71
|
+
def connection_made(self, transport: BaseTransport) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Invoked when connection from the device is made.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def connection_lost(self, exc: Optional[Exception]) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Same but when the connection is lost.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def datagram_received(
|
|
82
|
+
self, data: bytes, addr: Tuple[str, int]
|
|
83
|
+
) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Invoked when datagram is received from the device.
|
|
86
|
+
"""
|
|
87
|
+
if self._host and self._host != addr[0]:
|
|
88
|
+
_LOGGER.error(
|
|
89
|
+
"Received notification/alert from wrong host '%s',"
|
|
90
|
+
" expected from '%s'",
|
|
91
|
+
addr[0], self._host
|
|
92
|
+
)
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
self.set_last_device_packet_time()
|
|
96
|
+
|
|
97
|
+
_LOGGER.debug('Received device message from %s:%s: %s',
|
|
98
|
+
addr[0], addr[1], data)
|
|
99
|
+
|
|
100
|
+
self.handle(data)
|
|
101
|
+
|
|
102
|
+
async def listen(self) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Listens for notifications/alerts from the device.
|
|
105
|
+
"""
|
|
106
|
+
loop = asyncio.get_running_loop()
|
|
107
|
+
|
|
108
|
+
_LOGGER.debug('Creating UDP endpoint for %s:%s',
|
|
109
|
+
self._notifications_local_host,
|
|
110
|
+
self._notifications_local_port)
|
|
111
|
+
(self._transport,
|
|
112
|
+
_protocol) = await loop.create_datagram_endpoint(
|
|
113
|
+
lambda: self,
|
|
114
|
+
local_addr=(
|
|
115
|
+
self._notifications_local_host, self._notifications_local_port
|
|
116
|
+
))
|
|
@@ -26,8 +26,8 @@ import logging
|
|
|
26
26
|
from typing import Any, cast
|
|
27
27
|
from dataclasses import dataclass
|
|
28
28
|
from .base_cmd import G90BaseCommand, G90BaseCommandData
|
|
29
|
-
from
|
|
30
|
-
from
|
|
29
|
+
from ..exceptions import G90Error
|
|
30
|
+
from ..const import G90Commands
|
|
31
31
|
|
|
32
32
|
_LOGGER = logging.getLogger(__name__)
|
|
33
33
|
|
|
@@ -28,8 +28,8 @@ from dataclasses import dataclass, asdict
|
|
|
28
28
|
import asyncio
|
|
29
29
|
from asyncio.transports import BaseTransport
|
|
30
30
|
from .base_cmd import G90BaseCommand
|
|
31
|
-
from
|
|
32
|
-
from
|
|
31
|
+
from ..const import G90Commands
|
|
32
|
+
from ..exceptions import G90Error
|
|
33
33
|
|
|
34
34
|
_LOGGER = logging.getLogger(__name__)
|
|
35
35
|
|
|
File without changes
|