pyg90alarm 2.3.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.
Files changed (42) hide show
  1. pyg90alarm/__init__.py +84 -0
  2. pyg90alarm/alarm.py +1274 -0
  3. pyg90alarm/callback.py +146 -0
  4. pyg90alarm/cloud/__init__.py +31 -0
  5. pyg90alarm/cloud/const.py +56 -0
  6. pyg90alarm/cloud/messages.py +593 -0
  7. pyg90alarm/cloud/notifications.py +410 -0
  8. pyg90alarm/cloud/protocol.py +518 -0
  9. pyg90alarm/const.py +273 -0
  10. pyg90alarm/definitions/__init__.py +3 -0
  11. pyg90alarm/definitions/base.py +247 -0
  12. pyg90alarm/definitions/devices.py +366 -0
  13. pyg90alarm/definitions/sensors.py +843 -0
  14. pyg90alarm/entities/__init__.py +3 -0
  15. pyg90alarm/entities/base_entity.py +93 -0
  16. pyg90alarm/entities/base_list.py +268 -0
  17. pyg90alarm/entities/device.py +97 -0
  18. pyg90alarm/entities/device_list.py +156 -0
  19. pyg90alarm/entities/sensor.py +891 -0
  20. pyg90alarm/entities/sensor_list.py +183 -0
  21. pyg90alarm/exceptions.py +63 -0
  22. pyg90alarm/local/__init__.py +0 -0
  23. pyg90alarm/local/base_cmd.py +293 -0
  24. pyg90alarm/local/config.py +157 -0
  25. pyg90alarm/local/discovery.py +103 -0
  26. pyg90alarm/local/history.py +272 -0
  27. pyg90alarm/local/host_info.py +89 -0
  28. pyg90alarm/local/host_status.py +52 -0
  29. pyg90alarm/local/notifications.py +117 -0
  30. pyg90alarm/local/paginated_cmd.py +132 -0
  31. pyg90alarm/local/paginated_result.py +135 -0
  32. pyg90alarm/local/targeted_discovery.py +162 -0
  33. pyg90alarm/local/user_data_crc.py +46 -0
  34. pyg90alarm/notifications/__init__.py +0 -0
  35. pyg90alarm/notifications/base.py +481 -0
  36. pyg90alarm/notifications/protocol.py +127 -0
  37. pyg90alarm/py.typed +0 -0
  38. pyg90alarm-2.3.0.dist-info/METADATA +277 -0
  39. pyg90alarm-2.3.0.dist-info/RECORD +42 -0
  40. pyg90alarm-2.3.0.dist-info/WHEEL +5 -0
  41. pyg90alarm-2.3.0.dist-info/licenses/LICENSE +21 -0
  42. pyg90alarm-2.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,183 @@
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 __future__ import annotations
24
+ from typing import AsyncGenerator, Optional, TYPE_CHECKING
25
+ import logging
26
+ import asyncio
27
+
28
+ from .sensor import G90Sensor
29
+ from .base_list import G90BaseList
30
+ from ..const import G90Commands
31
+ from ..exceptions import G90EntityRegistrationError, G90Error
32
+ from ..definitions.sensors import G90SensorDefinitions
33
+ from ..entities.sensor import G90SensorUserFlags
34
+
35
+ if TYPE_CHECKING:
36
+ from ..alarm import G90Alarm
37
+
38
+ _LOGGER = logging.getLogger(__name__)
39
+
40
+
41
+ class G90SensorList(G90BaseList[G90Sensor]):
42
+ """
43
+ Sensor list class.
44
+ """
45
+ def __init__(self, parent: G90Alarm) -> None:
46
+ super().__init__(parent)
47
+ self._sensor_change_future: Optional[asyncio.Future[G90Sensor]] = None
48
+
49
+ async def _fetch(self) -> AsyncGenerator[G90Sensor, None]:
50
+ """
51
+ Fetch the list of sensors from the panel.
52
+
53
+ :yields: G90Sensor: Sensor entity.
54
+ """
55
+ entities = self._parent.paginated_result(
56
+ G90Commands.GETSENSORLIST
57
+ )
58
+
59
+ async for entity in entities:
60
+ yield G90Sensor(
61
+ *entity.data, parent=self._parent, subindex=0,
62
+ proto_idx=entity.proto_idx
63
+ )
64
+
65
+ async def sensor_change_callback(
66
+ self, idx: int, name: str, added: bool
67
+ ) -> None:
68
+ """
69
+ Sensor change callback.
70
+
71
+ Should be invoked from corresponding panel's notification handler to
72
+ finish the registration process.
73
+
74
+ :param idx: Sensor index.
75
+ :param name: Sensor name.
76
+ :param added: True if the sensor was added, False if removed.
77
+ """
78
+ _LOGGER.debug(
79
+ "Sensor change callback: name='%s', index=%d, added=%s",
80
+ name, idx, added
81
+ )
82
+ # The method depends on the future to be created before it is called
83
+ if not self._sensor_change_future:
84
+ raise G90EntityRegistrationError(
85
+ "Sensor change callback called without a future"
86
+ )
87
+
88
+ # Update the sensor list to get the latest data, unfortunately there is
89
+ # no panel command to get just a single sensor
90
+ try:
91
+ await self.update()
92
+ except G90Error as err:
93
+ _LOGGER.debug(
94
+ "Failed to update the sensor list: %s", err
95
+ )
96
+ # Indicate the error to the caller
97
+ self._sensor_change_future.set_exception(err)
98
+ return
99
+
100
+ # Attempt to find the sensor just changed by its index
101
+ if not (found := await self.find_by_idx(
102
+ idx, exclude_unavailable=False
103
+ )):
104
+ msg = (
105
+ f"Failed to find the added sensor '{name}'"
106
+ f' at index {idx}'
107
+ )
108
+ _LOGGER.debug(msg)
109
+ # Indicate the error to the caller
110
+ self._sensor_change_future.set_exception(
111
+ G90EntityRegistrationError(msg)
112
+ )
113
+ return
114
+
115
+ # Provide the found sensor as the future result so that the caller
116
+ # can use it
117
+ self._sensor_change_future.set_result(found)
118
+
119
+ async def register(
120
+ self, definition_name: str,
121
+ room_id: int, timeout: float, name: Optional[str] = None,
122
+ ) -> G90Sensor:
123
+ """
124
+ Registers sensor to the panel.
125
+
126
+ :param definition_name: Sensor definition name.
127
+ :param room_id: Room ID to assign the sensor to.
128
+ :param timeout: Timeout for the registration process.
129
+ :param name: Optional name for the sensor, if not provided, the
130
+ definition name will be used.
131
+ :raises G90EntityRegistrationError: If the registration fails.
132
+ :return: G90Sensor: The registered sensor entity.
133
+ """
134
+ sensor_definition = G90SensorDefinitions.get_by_name(definition_name)
135
+ dev_name = name or sensor_definition.name
136
+
137
+ # Future is needed to coordinate the registration process with panel
138
+ # notifications (see `sensor_change_callback()` method)
139
+ self._sensor_change_future = asyncio.get_running_loop().create_future()
140
+
141
+ # Register the sensor with the panel
142
+ await self._parent.command(
143
+ G90Commands.ADDSENSOR, [
144
+ dev_name,
145
+ # Registering sensor requires to provide a free index from
146
+ # panel point of view
147
+ await self.find_free_idx(),
148
+ room_id,
149
+ sensor_definition.type,
150
+ sensor_definition.subtype,
151
+ sensor_definition.timeout,
152
+ # Newly registered sensors are enabled by default and set to
153
+ # alarm in away and home modes
154
+ G90SensorUserFlags.ENABLED
155
+ | G90SensorUserFlags.ALERT_WHEN_AWAY_AND_HOME,
156
+ sensor_definition.baudrate,
157
+ sensor_definition.protocol,
158
+ sensor_definition.reserved_data,
159
+ sensor_definition.node_count,
160
+ sensor_definition.rx,
161
+ sensor_definition.tx,
162
+ sensor_definition.private_data
163
+ ]
164
+ )
165
+
166
+ # Waiting for the registration to finish, where panel will send
167
+ # corresponding notification processed by `sensor_change_callback()`
168
+ # method, which manipulates the future created above.
169
+ done, _ = await asyncio.wait(
170
+ [self._sensor_change_future], timeout=timeout
171
+ )
172
+
173
+ if self._sensor_change_future not in done:
174
+ msg = f"Failed to learn the device '{dev_name}', timed out"
175
+ _LOGGER.debug(msg)
176
+ raise G90EntityRegistrationError(msg)
177
+
178
+ # Propagate any exception that might have occurred in
179
+ # `sensor_change_callback()` method during the registration process
180
+ self._sensor_change_future.exception()
181
+
182
+ # Return the registered sensor entity
183
+ return self._sensor_change_future.result()
@@ -0,0 +1,63 @@
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
+ """
23
+ Exceptions specific to G90-based alarm systems.
24
+ """
25
+
26
+ import asyncio
27
+
28
+
29
+ class G90Error(Exception):
30
+ """
31
+ Represents a generic exception raised by many package classes.
32
+ """
33
+
34
+
35
+ class G90TimeoutError(asyncio.TimeoutError): # pylint:disable=R0903
36
+ """
37
+ Raised when particular package class to report an operation (typically
38
+ device command) has timed out.
39
+ """
40
+
41
+
42
+ class G90CommandFailure(G90Error):
43
+ """
44
+ Raised when a command to the alarm panel reports failure.
45
+ """
46
+
47
+
48
+ class G90CommandError(G90Error):
49
+ """
50
+ Raised when a command to the alarm panel reports an error.
51
+ """
52
+
53
+
54
+ class G90EntityRegistrationError(G90Error):
55
+ """
56
+ Raised when registering an entity to the alarm panel fails.
57
+ """
58
+
59
+
60
+ class G90PeripheralDefinitionNotFound(G90Error):
61
+ """
62
+ Raised when a peripheral definition is not found.
63
+ """
File without changes
@@ -0,0 +1,293 @@
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
+ Provides support for basic commands of G90 alarm panel.
23
+ """
24
+ from __future__ import annotations
25
+ import logging
26
+ import json
27
+ import asyncio
28
+ from asyncio import Future
29
+ from asyncio.protocols import DatagramProtocol
30
+ from asyncio.transports import DatagramTransport, BaseTransport
31
+ from typing import Optional, Tuple, List, Any
32
+ from dataclasses import dataclass
33
+ from ..exceptions import (
34
+ G90Error, G90TimeoutError, G90CommandFailure, G90CommandError
35
+ )
36
+ from ..const import G90Commands
37
+
38
+
39
+ _LOGGER = logging.getLogger(__name__)
40
+ G90BaseCommandData = List[Any]
41
+
42
+
43
+ @dataclass
44
+ class G90Header:
45
+ """
46
+ Represents JSON structure of the header used in alarm panel commands.
47
+
48
+ :meta private:
49
+ """
50
+ code: Optional[int] = None
51
+ data: Optional[G90BaseCommandData] = None
52
+
53
+
54
+ class G90BaseCommand(DatagramProtocol):
55
+ """
56
+ Implements basic command handling for alarm panel protocol.
57
+ """
58
+ # pylint: disable=too-many-instance-attributes
59
+ # Lock need to be shared across all of the class instances
60
+ _sk_lock = asyncio.Lock()
61
+
62
+ # pylint: disable=too-many-positional-arguments,too-many-arguments
63
+ def __init__(self, host: str, port: int, code: G90Commands,
64
+ data: Optional[G90BaseCommandData] = None,
65
+ local_port: Optional[int] = None,
66
+ timeout: float = 3.0, retries: int = 3) -> None:
67
+ self._remote_host = host
68
+ self._remote_port = port
69
+ self._local_port = local_port
70
+ self._code = code
71
+ self._timeout = timeout
72
+ self._retries = retries
73
+ self._data = '""'
74
+ self._result: G90BaseCommandData = []
75
+ self._connection_result: Optional[
76
+ Future[Tuple[str, int, bytes]]
77
+ ] = None
78
+ if data:
79
+ self._data = json.dumps([code, data],
80
+ # No newlines to be inserted
81
+ indent=None,
82
+ # No whitespace around entities
83
+ separators=(',', ':'))
84
+ self._resp = G90Header()
85
+
86
+ # Implementation of datagram protocol,
87
+ # https://docs.python.org/3/library/asyncio-protocol.html#datagram-protocols
88
+ def connection_made(self, transport: BaseTransport) -> None:
89
+ """
90
+ Invoked when connection is established.
91
+ """
92
+
93
+ def connection_lost(self, exc: Optional[Exception]) -> None:
94
+ """
95
+ Invoked when connection is lost.
96
+ """
97
+
98
+ def datagram_received(self, data: bytes, addr: Tuple[str, int]) -> None:
99
+ """
100
+ Invoked when datagram is received.
101
+ """
102
+ if asyncio.isfuture(self._connection_result):
103
+ if self._connection_result.done():
104
+ _LOGGER.warning('Excessive packet received'
105
+ ' from %s:%s: %s',
106
+ addr[0], addr[1], data)
107
+ return
108
+ self._connection_result.set_result((*addr, data))
109
+
110
+ def error_received(self, exc: Exception) -> None:
111
+ """
112
+ Invoked when error is received.
113
+ """
114
+ if (
115
+ asyncio.isfuture(self._connection_result) and not
116
+ self._connection_result.done()
117
+ ):
118
+ self._connection_result.set_exception(exc)
119
+
120
+ async def _create_connection(self) -> (
121
+ Tuple[DatagramTransport, DatagramProtocol]
122
+ ):
123
+ """
124
+ Creates UDP connection to the alarm panel.
125
+ """
126
+ loop = asyncio.get_running_loop()
127
+
128
+ _LOGGER.debug('Creating UDP endpoint for %s:%s',
129
+ self.host, self.port)
130
+ local_addr = None
131
+ if self._local_port:
132
+ local_addr = ('0.0.0.0', self._local_port)
133
+
134
+ transport, protocol = await loop.create_datagram_endpoint(
135
+ lambda: self,
136
+ remote_addr=(self.host, self.port),
137
+ allow_broadcast=True,
138
+ local_addr=local_addr)
139
+
140
+ return (transport, protocol)
141
+
142
+ def to_wire(self) -> bytes:
143
+ """
144
+ Returns the command in wire format.
145
+ """
146
+ wire = bytes(f'ISTART[{self._code},{self._code},{self._data}]IEND\0',
147
+ 'utf-8')
148
+ _LOGGER.debug('Encoded to wire format %s', wire)
149
+ return wire
150
+
151
+ def from_wire(self, data: bytes) -> G90BaseCommandData:
152
+ """
153
+ Parses the response from the alarm panel.
154
+ """
155
+ _LOGGER.debug('To be decoded from wire format %s', data)
156
+ try:
157
+ self._parse(data.decode('utf-8'))
158
+ except UnicodeDecodeError as exc:
159
+ raise G90Error(
160
+ 'Unable to decode response from UTF-8'
161
+ ) from exc
162
+ return self._resp.data or []
163
+
164
+ def _parse(self, data: str) -> None:
165
+ """
166
+ Processes the response from the alarm panel.
167
+ """
168
+ if not data.startswith('ISTART'):
169
+ raise G90Error('Missing start marker in data')
170
+ if not data.endswith('IEND\0'):
171
+ raise G90Error('Missing end marker in data')
172
+ payload = data[6:-5]
173
+ _LOGGER.debug("Decoded from wire: string '%s'", payload)
174
+
175
+ if not payload:
176
+ return
177
+
178
+ # Panel may report the last command has failed
179
+ if payload == 'fail':
180
+ raise G90CommandFailure(
181
+ f"Command {self._code.name}"
182
+ f" (code={self._code.value}) failed"
183
+ )
184
+
185
+ # Also, panel may report an error supplying specific reason, e.g.
186
+ # command and its arguments that have failed
187
+ if payload.startswith('error'):
188
+ error = payload[5:]
189
+ raise G90CommandError(
190
+ f"Command {self._code.name}"
191
+ f" (code={self._code.value}) failed"
192
+ f" with error: '{error}'")
193
+
194
+ resp = None
195
+ try:
196
+ resp = json.loads(payload, strict=False)
197
+ except json.JSONDecodeError as exc:
198
+ raise G90Error(
199
+ f"Unable to parse response as JSON: '{payload}'"
200
+ ) from exc
201
+
202
+ if not isinstance(resp, list):
203
+ raise G90Error(
204
+ f"Malformed response, 'list' expected: '{payload}'"
205
+ )
206
+
207
+ if resp is not None:
208
+ self._resp = G90Header(*resp)
209
+ _LOGGER.debug('Parsed from wire: %s', self._resp)
210
+
211
+ if not self._resp.code:
212
+ raise G90Error(f"Missing code in response: '{payload}'")
213
+ # Check there is data if the response is non-empty
214
+ if not self._resp.data:
215
+ raise G90Error(f"Missing data in response: '{payload}'")
216
+
217
+ if self._resp.code != self._code:
218
+ raise G90Error(
219
+ 'Wrong response - received code '
220
+ f"{self._resp.code}, expected code {self._code}")
221
+
222
+ @property
223
+ def result(self) -> G90BaseCommandData:
224
+ """
225
+ The result of the command.
226
+ """
227
+ return self._result
228
+
229
+ @property
230
+ def host(self) -> str:
231
+ """
232
+ The hostname/IP address of the alarm panel.
233
+ """
234
+ return self._remote_host
235
+
236
+ @property
237
+ def port(self) -> int:
238
+ """
239
+ The port of the alarm panel.
240
+ """
241
+ return self._remote_port
242
+
243
+ async def process(self) -> G90BaseCommand:
244
+ """
245
+ Processes the command.
246
+ """
247
+ # Disallow using `NONE` command, which is intended to use by inheriting
248
+ # classes overriding `process()` method
249
+ if self._code == G90Commands.NONE:
250
+ raise G90Error("'NONE' command code is disallowed")
251
+
252
+ transport, _ = await self._create_connection()
253
+ attempts = self._retries
254
+ while True:
255
+ attempts = attempts - 1
256
+ loop = asyncio.get_running_loop()
257
+ self._connection_result = loop.create_future()
258
+ async with self._sk_lock:
259
+ _LOGGER.debug('(code %s) Sending request to %s:%s',
260
+ self._code, self.host, self.port)
261
+ transport.sendto(self.to_wire())
262
+ done, _ = await asyncio.wait([self._connection_result],
263
+ timeout=self._timeout)
264
+ if self._connection_result in done:
265
+ break
266
+ # Cancel the future to signal protocol handler it is no longer
267
+ # valid, the future will be re-created on next retry
268
+ self._connection_result.cancel()
269
+ if not attempts:
270
+ transport.close()
271
+ raise G90TimeoutError()
272
+ _LOGGER.debug('Timed out, retrying')
273
+ transport.close()
274
+ (host, port, data) = self._connection_result.result()
275
+ _LOGGER.debug('Received response from %s:%s', host, port)
276
+ if self.host != '255.255.255.255':
277
+ if self.host != host or host == '255.255.255.255':
278
+ raise G90Error(f'Received response from wrong host {host},'
279
+ f' expected from {self.host}')
280
+ if self.port != port:
281
+ raise G90Error(f'Received response from wrong port {port},'
282
+ f' expected from {self.port}')
283
+
284
+ ret = self.from_wire(data)
285
+ self._result = ret
286
+ return self
287
+
288
+ def __repr__(self) -> str:
289
+ """
290
+ Returns string representation of the command.
291
+ """
292
+ return f'Command: {self._code}, request: {self._data},' \
293
+ f' response: {self._resp.data}'
@@ -0,0 +1,157 @@
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
+ Represents various configuration aspects of the alarm panel.
23
+ """
24
+ from __future__ import annotations
25
+ from typing import TYPE_CHECKING
26
+ import logging
27
+ from dataclasses import dataclass
28
+ from enum import IntFlag
29
+ from ..const import G90Commands
30
+ if TYPE_CHECKING:
31
+ from ..alarm import G90Alarm
32
+
33
+
34
+ class G90AlertConfigFlags(IntFlag):
35
+ """
36
+ Alert configuration flags, used bitwise
37
+ """
38
+ AC_POWER_FAILURE = 1
39
+ AC_POWER_RECOVER = 2
40
+ ARM_DISARM = 4
41
+ HOST_LOW_VOLTAGE = 8
42
+ SENSOR_LOW_VOLTAGE = 16
43
+ WIFI_AVAILABLE = 32
44
+ WIFI_UNAVAILABLE = 64
45
+ DOOR_OPEN = 128
46
+ DOOR_CLOSE = 256
47
+ SMS_PUSH = 512
48
+ UNKNOWN1 = 2048
49
+ UNKNOWN2 = 8192
50
+
51
+
52
+ _LOGGER = logging.getLogger(__name__)
53
+
54
+
55
+ @dataclass
56
+ class G90AlertConfigData:
57
+ """
58
+ Represents alert configuration data as received from the alarm panel.
59
+ """
60
+ flags_data: int
61
+
62
+ @property
63
+ def flags(self) -> G90AlertConfigFlags:
64
+ """
65
+ :return: The alert configuration flags
66
+ """
67
+ return G90AlertConfigFlags(self.flags_data)
68
+
69
+ @flags.setter
70
+ def flags(self, value: G90AlertConfigFlags) -> None:
71
+ """
72
+ :param value: The alert configuration flags
73
+ """
74
+ self.flags_data = value.value
75
+
76
+
77
+ class G90AlertConfig:
78
+ """
79
+ Represents alert configuration as received from the alarm panel.
80
+ """
81
+ def __init__(self, parent: G90Alarm) -> None:
82
+ self.parent = parent
83
+
84
+ async def _get(self) -> G90AlertConfigData:
85
+ """
86
+ Retrieves the alert configuration flags directly from the device.
87
+
88
+ :return: The alerts configured
89
+ """
90
+ _LOGGER.debug('Retrieving alert configuration from the device')
91
+ res = await self.parent.command(G90Commands.GETNOTICEFLAG)
92
+ data = G90AlertConfigData(*res)
93
+ _LOGGER.debug(
94
+ 'Alert configuration: %s, flags: %s', data,
95
+ repr(data.flags)
96
+ )
97
+ return data
98
+
99
+ async def set(self, flags: G90AlertConfigFlags) -> None:
100
+ """
101
+ .. deprecated:: 2.3.0
102
+
103
+ This method is deprecated and will always raise a RuntimeError.
104
+ Please use :meth:`set_flag` to set individual flags.
105
+ """
106
+ raise RuntimeError(
107
+ 'The set() method is deprecated. Please use set_flag() to set'
108
+ ' individual flags instead.'
109
+ )
110
+
111
+ async def _set(self, flags: G90AlertConfigFlags) -> None:
112
+ """
113
+ Sets the alert configuration flags on the device.
114
+ """
115
+ _LOGGER.debug('Setting alert configuration to %s', repr(flags))
116
+ await self.parent.command(G90Commands.SETNOTICEFLAG, [flags.value])
117
+
118
+ async def get_flag(self, flag: G90AlertConfigFlags) -> bool:
119
+ """
120
+ :param flag: The flag to check
121
+ """
122
+ return flag in await self.flags
123
+
124
+ async def set_flag(self, flag: G90AlertConfigFlags, value: bool) -> None:
125
+ """
126
+ Sets the given flag to the desired value.
127
+
128
+ Uses read-modify-write approach.
129
+
130
+ :param flag: The flag to set
131
+ :param value: The value to set
132
+ """
133
+ # Retrieve current flags
134
+ current_flags = await self.flags
135
+ # Skip updating the flag if it has the desired value
136
+ if (flag in current_flags) == value:
137
+ _LOGGER.debug(
138
+ 'Flag %s already set to %s, skipping update',
139
+ repr(flag), value
140
+ )
141
+ return
142
+
143
+ # Set or reset corresponding user flag depending on desired value
144
+ if value:
145
+ current_flags |= flag
146
+ else:
147
+ current_flags &= ~flag
148
+
149
+ # Set the updated flags
150
+ await self._set(current_flags)
151
+
152
+ @property
153
+ async def flags(self) -> G90AlertConfigFlags:
154
+ """
155
+ :return: Symbolic names for corresponding flag bits
156
+ """
157
+ return (await self._get()).flags