pyg90alarm 2.1.0__py3-none-any.whl → 2.2.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 +28 -5
- pyg90alarm/alarm.py +248 -54
- pyg90alarm/callback.py +41 -3
- pyg90alarm/cloud/messages.py +7 -7
- pyg90alarm/const.py +4 -1
- pyg90alarm/definitions/base.py +247 -0
- pyg90alarm/definitions/devices.py +366 -0
- pyg90alarm/definitions/sensors.py +812 -828
- pyg90alarm/entities/base_entity.py +10 -0
- pyg90alarm/entities/base_list.py +136 -33
- pyg90alarm/entities/device.py +23 -1
- pyg90alarm/entities/device_list.py +99 -1
- pyg90alarm/entities/sensor.py +83 -89
- pyg90alarm/entities/sensor_list.py +136 -3
- pyg90alarm/exceptions.py +24 -0
- pyg90alarm/local/base_cmd.py +34 -12
- pyg90alarm/local/notifications.py +3 -3
- pyg90alarm/notifications/base.py +29 -2
- pyg90alarm/notifications/protocol.py +11 -0
- {pyg90alarm-2.1.0.dist-info → pyg90alarm-2.2.0.dist-info}/METADATA +1 -1
- pyg90alarm-2.2.0.dist-info/RECORD +42 -0
- {pyg90alarm-2.1.0.dist-info → pyg90alarm-2.2.0.dist-info}/WHEEL +1 -1
- pyg90alarm-2.1.0.dist-info/RECORD +0 -40
- {pyg90alarm-2.1.0.dist-info → pyg90alarm-2.2.0.dist-info}/licenses/LICENSE +0 -0
- {pyg90alarm-2.1.0.dist-info → pyg90alarm-2.2.0.dist-info}/top_level.txt +0 -0
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"""
|
|
21
21
|
Base entity.
|
|
22
22
|
"""
|
|
23
|
+
from __future__ import annotations
|
|
23
24
|
from abc import ABC, abstractmethod
|
|
24
25
|
# `Self` has been introduced in Python 3.11, need to use `typing_extensions`
|
|
25
26
|
# for earlier versions
|
|
@@ -81,3 +82,12 @@ class G90BaseEntity(ABC):
|
|
|
81
82
|
|
|
82
83
|
:return: Index of the entity.
|
|
83
84
|
"""
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
@abstractmethod
|
|
88
|
+
def subindex(self) -> int:
|
|
89
|
+
"""
|
|
90
|
+
Get the subindex of the entity.
|
|
91
|
+
|
|
92
|
+
:return: Subindex of the entity.
|
|
93
|
+
"""
|
pyg90alarm/entities/base_list.py
CHANGED
|
@@ -24,11 +24,21 @@ Base entity list.
|
|
|
24
24
|
from abc import ABC, abstractmethod
|
|
25
25
|
from typing import (
|
|
26
26
|
List, AsyncGenerator, Optional, TypeVar, Generic, cast, TYPE_CHECKING,
|
|
27
|
+
Callable, Coroutine, Union
|
|
27
28
|
)
|
|
28
29
|
import asyncio
|
|
29
30
|
import logging
|
|
30
31
|
|
|
32
|
+
from ..exceptions import G90Error
|
|
31
33
|
from .base_entity import G90BaseEntity
|
|
34
|
+
from ..callback import G90Callback
|
|
35
|
+
|
|
36
|
+
T = TypeVar('T', bound=G90BaseEntity)
|
|
37
|
+
ListChangeCallback = Union[
|
|
38
|
+
Callable[[T, bool], None],
|
|
39
|
+
Callable[[T, bool], Coroutine[None, None, None]]
|
|
40
|
+
]
|
|
41
|
+
|
|
32
42
|
if TYPE_CHECKING:
|
|
33
43
|
from ..alarm import G90Alarm
|
|
34
44
|
else:
|
|
@@ -36,7 +46,6 @@ else:
|
|
|
36
46
|
# (`G90Alarm` -> `G90SensorList` -> `G90BaseList` -> `G90Alarm`)
|
|
37
47
|
G90Alarm = object
|
|
38
48
|
|
|
39
|
-
T = TypeVar('T', bound=G90BaseEntity)
|
|
40
49
|
_LOGGER = logging.getLogger(__name__)
|
|
41
50
|
|
|
42
51
|
|
|
@@ -50,6 +59,7 @@ class G90BaseList(Generic[T], ABC):
|
|
|
50
59
|
self._entities: List[T] = []
|
|
51
60
|
self._lock = asyncio.Lock()
|
|
52
61
|
self._parent = parent
|
|
62
|
+
self._list_change_cb: Optional[ListChangeCallback[T]] = None
|
|
53
63
|
|
|
54
64
|
@abstractmethod
|
|
55
65
|
async def _fetch(self) -> AsyncGenerator[T, None]:
|
|
@@ -91,26 +101,41 @@ class G90BaseList(Generic[T], ABC):
|
|
|
91
101
|
entities = self._fetch()
|
|
92
102
|
|
|
93
103
|
non_existing_entities = self._entities.copy()
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
104
|
+
try:
|
|
105
|
+
async for entity in entities:
|
|
106
|
+
try:
|
|
107
|
+
existing_entity = next(
|
|
108
|
+
x for x in self._entities if x == entity
|
|
109
|
+
)
|
|
110
|
+
except StopIteration:
|
|
111
|
+
existing_entity = None
|
|
112
|
+
|
|
113
|
+
if existing_entity is not None:
|
|
114
|
+
# Update the existing entity with the new data
|
|
115
|
+
_LOGGER.debug(
|
|
116
|
+
"Updating existing entity '%s' from protocol"
|
|
117
|
+
" data '%s'", existing_entity, entity
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
existing_entity.update(entity)
|
|
121
|
+
non_existing_entities.remove(existing_entity)
|
|
122
|
+
|
|
123
|
+
# Invoke the list change callback for the existing
|
|
124
|
+
# entity to notify about the update
|
|
125
|
+
G90Callback.invoke(
|
|
126
|
+
self._list_change_cb, existing_entity, False
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
# Add the new entity to the list
|
|
130
|
+
_LOGGER.debug('Adding new entity: %s', entity)
|
|
131
|
+
self._entities.append(entity)
|
|
132
|
+
# Invoke the list change callback for the new entity
|
|
133
|
+
G90Callback.invoke(self._list_change_cb, entity, True)
|
|
134
|
+
except TypeError as err:
|
|
135
|
+
_LOGGER.error(
|
|
136
|
+
'Failed to fetch entities: %s', err
|
|
137
|
+
)
|
|
138
|
+
raise G90Error(err) from err
|
|
114
139
|
|
|
115
140
|
# Mark the entities that are no longer in the list
|
|
116
141
|
for unavailable_entity in non_existing_entities:
|
|
@@ -126,30 +151,30 @@ class G90BaseList(Generic[T], ABC):
|
|
|
126
151
|
|
|
127
152
|
return self._entities
|
|
128
153
|
|
|
129
|
-
async def
|
|
130
|
-
self, idx: int,
|
|
154
|
+
async def find_by_idx(
|
|
155
|
+
self, idx: int, exclude_unavailable: bool, subindex: int = 0
|
|
131
156
|
) -> Optional[T]:
|
|
132
157
|
"""
|
|
133
|
-
Finds entity by index
|
|
158
|
+
Finds entity by index.
|
|
134
159
|
|
|
135
160
|
:param idx: Entity index
|
|
136
|
-
:param name: Entity name
|
|
137
161
|
:param exclude_unavailable: Exclude unavailable entities
|
|
138
|
-
:
|
|
162
|
+
:param subindex: Entity subindex
|
|
163
|
+
:return: Entity instance or None if not found
|
|
139
164
|
"""
|
|
140
165
|
entities = await self.entities
|
|
141
166
|
|
|
142
167
|
found = None
|
|
143
|
-
|
|
144
|
-
if idx < len(entities) and entities[idx].name == name:
|
|
168
|
+
if idx < len(entities):
|
|
145
169
|
entity = entities[idx]
|
|
146
|
-
|
|
147
|
-
|
|
170
|
+
if entity.index == idx and entity.subindex == subindex:
|
|
171
|
+
# Fast lookup by direct index
|
|
172
|
+
_LOGGER.debug('Found entity via fast lookup: %s', entity)
|
|
173
|
+
found = entity
|
|
148
174
|
|
|
149
|
-
# Fast lookup failed, perform slow one over the whole entities list
|
|
150
175
|
if not found:
|
|
151
176
|
for entity in entities:
|
|
152
|
-
if entity.index == idx and entity.
|
|
177
|
+
if entity.index == idx and entity.subindex == subindex:
|
|
153
178
|
_LOGGER.debug('Found entity: %s', entity)
|
|
154
179
|
found = entity
|
|
155
180
|
|
|
@@ -161,5 +186,83 @@ class G90BaseList(Generic[T], ABC):
|
|
|
161
186
|
'Entity is found but unavailable, will result in none returned'
|
|
162
187
|
)
|
|
163
188
|
|
|
164
|
-
_LOGGER.error(
|
|
189
|
+
_LOGGER.error(
|
|
190
|
+
'Entity not found by index=%s and subindex=%s', idx, subindex
|
|
191
|
+
)
|
|
165
192
|
return None
|
|
193
|
+
|
|
194
|
+
async def find(
|
|
195
|
+
self, idx: int, name: str, exclude_unavailable: bool, subindex: int = 0
|
|
196
|
+
) -> Optional[T]:
|
|
197
|
+
"""
|
|
198
|
+
Finds entity by index, subindex and name.
|
|
199
|
+
|
|
200
|
+
:param idx: Entity index
|
|
201
|
+
:param name: Entity name
|
|
202
|
+
:param exclude_unavailable: Exclude unavailable entities
|
|
203
|
+
:param subindex: Entity subindex
|
|
204
|
+
:return: Entity instance or None if not found
|
|
205
|
+
"""
|
|
206
|
+
found = await self.find_by_idx(idx, exclude_unavailable, subindex)
|
|
207
|
+
if not found:
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
if found.name == name:
|
|
211
|
+
return found
|
|
212
|
+
|
|
213
|
+
_LOGGER.error(
|
|
214
|
+
'Entity not found: index=%s, subindex=%s, name=%s',
|
|
215
|
+
idx, subindex, name
|
|
216
|
+
)
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
async def find_free_idx(self) -> int:
|
|
220
|
+
"""
|
|
221
|
+
Finds the first free index in the list.
|
|
222
|
+
|
|
223
|
+
The index is from protocol point of view (`.index` attribute of the
|
|
224
|
+
protocol data), not the index in the list. The index is required when
|
|
225
|
+
registering a new entity on the panel.
|
|
226
|
+
|
|
227
|
+
:return: Free index
|
|
228
|
+
"""
|
|
229
|
+
entities = await self.entities
|
|
230
|
+
|
|
231
|
+
# Collect indexes in use by the existing entities
|
|
232
|
+
occupied_indexes = set(x.index for x in entities)
|
|
233
|
+
# Generate a set of possible indexes from 0 to the maximum index in
|
|
234
|
+
# use
|
|
235
|
+
possible_indexes = set(range(0, max(occupied_indexes)))
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
# Find the first free index by taking difference between
|
|
239
|
+
# possible indexes and occupied ones, and then taking the minimum
|
|
240
|
+
# value off the difference
|
|
241
|
+
free_idx = min(
|
|
242
|
+
set(possible_indexes).difference(occupied_indexes)
|
|
243
|
+
)
|
|
244
|
+
_LOGGER.debug(
|
|
245
|
+
'Found free index: %s out of occupied indexes: %s',
|
|
246
|
+
free_idx, occupied_indexes
|
|
247
|
+
)
|
|
248
|
+
return free_idx
|
|
249
|
+
except ValueError:
|
|
250
|
+
# If no gaps in existing indexes, then return the index next to
|
|
251
|
+
# the last existing entity
|
|
252
|
+
return len(entities)
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def list_change_callback(self) -> Optional[ListChangeCallback[T]]:
|
|
256
|
+
"""
|
|
257
|
+
List change callback.
|
|
258
|
+
|
|
259
|
+
Invoked when the list of entities is changed, i.e. when a new entity is
|
|
260
|
+
added or an existing one is updated.
|
|
261
|
+
|
|
262
|
+
:return: Callback
|
|
263
|
+
"""
|
|
264
|
+
return self._list_change_cb
|
|
265
|
+
|
|
266
|
+
@list_change_callback.setter
|
|
267
|
+
def list_change_callback(self, value: ListChangeCallback[T]) -> None:
|
|
268
|
+
self._list_change_cb = value
|
pyg90alarm/entities/device.py
CHANGED
|
@@ -22,10 +22,13 @@
|
|
|
22
22
|
Provides interface to devices (switches) of G90 alarm panel.
|
|
23
23
|
"""
|
|
24
24
|
from __future__ import annotations
|
|
25
|
+
from typing import Optional
|
|
25
26
|
import logging
|
|
26
27
|
from .sensor import G90Sensor
|
|
27
28
|
from ..const import G90Commands
|
|
28
|
-
|
|
29
|
+
from ..definitions.base import G90PeripheralDefinition
|
|
30
|
+
from ..definitions.devices import G90DeviceDefinitions
|
|
31
|
+
from ..exceptions import G90PeripheralDefinitionNotFound
|
|
29
32
|
|
|
30
33
|
_LOGGER = logging.getLogger(__name__)
|
|
31
34
|
|
|
@@ -34,6 +37,25 @@ class G90Device(G90Sensor):
|
|
|
34
37
|
"""
|
|
35
38
|
Interacts with device (relay) on G90 alarm panel.
|
|
36
39
|
"""
|
|
40
|
+
@property
|
|
41
|
+
def definition(self) -> Optional[G90PeripheralDefinition]:
|
|
42
|
+
"""
|
|
43
|
+
Returns the definition for the device.
|
|
44
|
+
|
|
45
|
+
:return: Device definition
|
|
46
|
+
"""
|
|
47
|
+
if not self._definition:
|
|
48
|
+
# No definition has been cached, try to find it by type, subtype
|
|
49
|
+
# and protocol
|
|
50
|
+
try:
|
|
51
|
+
self._definition = (
|
|
52
|
+
G90DeviceDefinitions.get_by_id(
|
|
53
|
+
self.type, self.subtype, self.protocol
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
except G90PeripheralDefinitionNotFound:
|
|
57
|
+
return None
|
|
58
|
+
return self._definition
|
|
37
59
|
|
|
38
60
|
async def turn_on(self) -> None:
|
|
39
61
|
"""
|
|
@@ -20,10 +20,17 @@
|
|
|
20
20
|
"""
|
|
21
21
|
Device list.
|
|
22
22
|
"""
|
|
23
|
-
from typing import AsyncGenerator
|
|
23
|
+
from typing import AsyncGenerator, Optional
|
|
24
|
+
import logging
|
|
25
|
+
import asyncio
|
|
24
26
|
from .device import G90Device
|
|
25
27
|
from .base_list import G90BaseList
|
|
26
28
|
from ..const import G90Commands
|
|
29
|
+
from ..definitions.devices import G90DeviceDefinitions
|
|
30
|
+
from ..entities.sensor import G90SensorUserFlags
|
|
31
|
+
from ..exceptions import G90EntityRegistrationError
|
|
32
|
+
|
|
33
|
+
_LOGGER = logging.getLogger(__name__)
|
|
27
34
|
|
|
28
35
|
|
|
29
36
|
class G90DeviceList(G90BaseList[G90Device]):
|
|
@@ -56,3 +63,94 @@ class G90DeviceList(G90BaseList[G90Device]):
|
|
|
56
63
|
subindex=node, proto_idx=device.proto_idx
|
|
57
64
|
)
|
|
58
65
|
yield obj
|
|
66
|
+
|
|
67
|
+
async def register(
|
|
68
|
+
self, definition_name: str,
|
|
69
|
+
room_id: int, timeout: float, name: Optional[str] = None,
|
|
70
|
+
) -> G90Device:
|
|
71
|
+
"""
|
|
72
|
+
Register the devices (switches) to the panel.
|
|
73
|
+
|
|
74
|
+
Contrary to registering the sensors, the registration of devices does
|
|
75
|
+
not have an associated notification from the panel, hence the list of
|
|
76
|
+
devices is polled to determine when new device is added.
|
|
77
|
+
|
|
78
|
+
:param definition_name: Name of the device definition to register.
|
|
79
|
+
:param room_id: ID of the room to assign the device to.
|
|
80
|
+
:param timeout: Timeout in seconds to wait for the device to be added.
|
|
81
|
+
:param name: Optional name for the device, if not provided, the
|
|
82
|
+
name from the definition will be used.
|
|
83
|
+
:raises G90EntityRegistrationError: If the device could not be
|
|
84
|
+
registered or found after the registration.
|
|
85
|
+
:return: G90Device: The registered device entity.
|
|
86
|
+
"""
|
|
87
|
+
device_definition = G90DeviceDefinitions.get_by_name(definition_name)
|
|
88
|
+
dev_name = name or device_definition.name
|
|
89
|
+
|
|
90
|
+
# Register the device with the panel
|
|
91
|
+
await self._parent.command(
|
|
92
|
+
G90Commands.ADDDEVICE, [
|
|
93
|
+
dev_name,
|
|
94
|
+
# Registering device requires to provide a free index from
|
|
95
|
+
# panel point of view
|
|
96
|
+
await self.find_free_idx(),
|
|
97
|
+
room_id,
|
|
98
|
+
device_definition.type,
|
|
99
|
+
device_definition.subtype,
|
|
100
|
+
device_definition.timeout,
|
|
101
|
+
# Newly registered devices are enabled by default
|
|
102
|
+
G90SensorUserFlags.ENABLED,
|
|
103
|
+
device_definition.baudrate,
|
|
104
|
+
device_definition.protocol,
|
|
105
|
+
device_definition.reserved_data,
|
|
106
|
+
device_definition.node_count,
|
|
107
|
+
device_definition.rx,
|
|
108
|
+
device_definition.tx,
|
|
109
|
+
device_definition.private_data
|
|
110
|
+
]
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Confirm the registration of the device to the panel
|
|
114
|
+
res = await self._parent.command(
|
|
115
|
+
G90Commands.SENDREGDEVICERESULT,
|
|
116
|
+
# 1 = register, 0 = cancel
|
|
117
|
+
[1]
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# The command above returns the index of the added device in the
|
|
121
|
+
# device list from panel point of view
|
|
122
|
+
try:
|
|
123
|
+
added_at = next(iter(res))
|
|
124
|
+
_LOGGER.debug('Device added at index=%s', added_at)
|
|
125
|
+
except StopIteration:
|
|
126
|
+
msg = (
|
|
127
|
+
f"Failed to register device '{dev_name}' - response does not"
|
|
128
|
+
' contain the index in the device list'
|
|
129
|
+
)
|
|
130
|
+
_LOGGER.debug(msg)
|
|
131
|
+
# pylint: disable=raise-missing-from
|
|
132
|
+
raise G90EntityRegistrationError(msg)
|
|
133
|
+
|
|
134
|
+
# Update the list of devices polling for the new entity
|
|
135
|
+
# to appear in the list - it takes some time for the panel
|
|
136
|
+
# to process the registration and add the device to the list
|
|
137
|
+
found = None
|
|
138
|
+
for _ in range(int(timeout)):
|
|
139
|
+
# Update the list of devices from the panel
|
|
140
|
+
await self.update()
|
|
141
|
+
# Try to find the device by the index it was added at
|
|
142
|
+
if found := await self.find_by_idx(
|
|
143
|
+
added_at, exclude_unavailable=False
|
|
144
|
+
):
|
|
145
|
+
break
|
|
146
|
+
await asyncio.sleep(1)
|
|
147
|
+
|
|
148
|
+
if found:
|
|
149
|
+
return found
|
|
150
|
+
|
|
151
|
+
msg = (
|
|
152
|
+
f"Failed to find the added device '{dev_name}'"
|
|
153
|
+
f' at index {added_at}'
|
|
154
|
+
)
|
|
155
|
+
_LOGGER.debug(msg)
|
|
156
|
+
raise G90EntityRegistrationError(msg)
|