pyg90alarm 2.5.3__py3-none-any.whl → 2.7.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 CHANGED
@@ -33,7 +33,8 @@ from .entities.sensor import (
33
33
  )
34
34
  from .entities.device import G90Device
35
35
  from .local.host_info import (
36
- G90HostInfo, G90HostInfoWifiStatus, G90HostInfoGsmStatus
36
+ G90HostInfo, G90HostInfoWifiStatus, G90HostInfoGsmStatus,
37
+ G90HostInfoWifiSetupProgress,
37
38
  )
38
39
  from .definitions.sensors import (
39
40
  G90SensorDefinitions
@@ -81,7 +82,7 @@ __all__ = [
81
82
  'G90Device',
82
83
  # Panel information and status
83
84
  'G90HostInfo', 'G90HostInfoWifiStatus', 'G90HostInfoGsmStatus',
84
- 'G90HostStatus',
85
+ 'G90HostStatus', 'G90HostInfoWifiSetupProgress',
85
86
  # Types for alerts and notifications
86
87
  'G90MessageTypes', 'G90NotificationTypes', 'G90ArmDisarmTypes',
87
88
  'G90AlertTypes', 'G90AlertSources', 'G90AlertStates',
pyg90alarm/alarm.py CHANGED
@@ -60,14 +60,14 @@ from typing import (
60
60
  Callable, Coroutine, Union
61
61
  )
62
62
  from .const import (
63
- G90Commands, REMOTE_PORT,
63
+ G90Commands, G90SystemCommands,
64
+ REMOTE_PORT,
64
65
  REMOTE_TARGETED_DISCOVERY_PORT,
65
66
  LOCAL_TARGETED_DISCOVERY_PORT,
66
67
  LOCAL_NOTIFICATIONS_HOST,
67
68
  LOCAL_NOTIFICATIONS_PORT,
68
- CLOUD_NOTIFICATIONS_HOST,
69
- CLOUD_NOTIFICATIONS_PORT,
70
- REMOTE_CLOUD_HOST,
69
+ LOCAL_CLOUD_NOTIFICATIONS_HOST,
70
+ LOCAL_CLOUD_NOTIFICATIONS_PORT,
71
71
  REMOTE_CLOUD_PORT,
72
72
  DEVICE_REGISTRATION_TIMEOUT,
73
73
  ROOM_ID,
@@ -75,7 +75,8 @@ from .const import (
75
75
  G90RemoteButtonStates,
76
76
  G90RFIDKeypadStates,
77
77
  )
78
- from .local.base_cmd import (G90BaseCommand, G90BaseCommandData)
78
+ from .local.base_cmd import G90BaseCommand, BaseCommandsDataT
79
+ from .local.system_cmd import G90SystemCommand, G90SetServerAddressCommand
79
80
  from .local.paginated_result import G90PaginatedResult, G90PaginatedResponse
80
81
  from .entities.base_list import ListChangeCallback
81
82
  from .entities.sensor import G90Sensor
@@ -263,8 +264,8 @@ class G90Alarm(G90NotificationProtocol):
263
264
  return self._port
264
265
 
265
266
  async def command(
266
- self, code: G90Commands, data: Optional[G90BaseCommandData] = None
267
- ) -> G90BaseCommandData:
267
+ self, code: G90Commands, data: Optional[BaseCommandsDataT] = None
268
+ ) -> BaseCommandsDataT:
268
269
  """
269
270
  Invokes a command against the alarm panel.
270
271
 
@@ -272,7 +273,7 @@ class G90Alarm(G90NotificationProtocol):
272
273
  :param data: Command data
273
274
  :return: The result of command invocation
274
275
  """
275
- cmd: G90BaseCommand = await G90BaseCommand(
276
+ cmd = await G90BaseCommand(
276
277
  self._host, self._port, code, data).process()
277
278
  return cmd.result
278
279
 
@@ -292,6 +293,81 @@ class G90Alarm(G90NotificationProtocol):
292
293
  self._host, self._port, code, start, end
293
294
  ).process()
294
295
 
296
+ async def mcu_reboot(self) -> None:
297
+ """
298
+ Reboots the MCU of the alarm panel.
299
+
300
+ Note that underlying command doesn't return any result, so there is no
301
+ feedback from the panel upon execution.
302
+ """
303
+ await G90SystemCommand(
304
+ host=self._host, port=self._port,
305
+ code=G90SystemCommands.MCU_REBOOT
306
+ ).process()
307
+
308
+ async def gsm_reboot(self) -> None:
309
+ """
310
+ Reboots the GSM module of the alarm panel.
311
+
312
+ Note that underlying command doesn't return any result, so there is no
313
+ feedback from the panel upon execution.
314
+ """
315
+ await G90SystemCommand(
316
+ host=self._host, port=self._port,
317
+ code=G90SystemCommands.GSM_REBOOT
318
+ ).process()
319
+
320
+ async def wifi_reboot(self) -> None:
321
+ """
322
+ Reboots the WiFi module of the alarm panel.
323
+
324
+ Note that underlying command doesn't return any result, so there is no
325
+ feedback from the panel upon execution.
326
+ """
327
+ await G90SystemCommand(
328
+ host=self._host, port=self._port,
329
+ code=G90SystemCommands.WIFI_REBOOT
330
+ ).process()
331
+
332
+ async def reboot(self) -> None:
333
+ """
334
+ Reboots the entire alarm panel.
335
+
336
+ The system commands performing reboot of a panel's module don't return
337
+ anything so there is no feedback from the panel upon execution, hence
338
+ the commands are spaced with delays to allow the panel to process
339
+ them.
340
+
341
+ Please be aware that the delays are determined experimentally and might
342
+ be too long or too short.
343
+ """
344
+ await self.gsm_reboot()
345
+ await asyncio.sleep(1)
346
+ await self.mcu_reboot()
347
+ await asyncio.sleep(1)
348
+ await self.wifi_reboot()
349
+ # The MCU likely needs more than 1 second to reboot, but checking it
350
+ # completed the reboot should be done separately
351
+ await asyncio.sleep(1)
352
+
353
+ async def set_cloud_server_address(
354
+ self, cloud_ip: str, cloud_port: int
355
+ ) -> None:
356
+ """
357
+ Sets the cloud server address the alarm panel connects to.
358
+
359
+ :param cloud_ip: IP address of the server to receive cloud protocol
360
+ notifications, should be reachable from the panel. Typically it is the
361
+ IP address of the host running the package
362
+ :param cloud_port: Port number of the server to receive cloud protocol
363
+ notifications, should be reachable from the panel
364
+ """
365
+ await G90SetServerAddressCommand(
366
+ host=self._host, port=self._port,
367
+ cloud_ip=cloud_ip,
368
+ cloud_port=cloud_port
369
+ ).process()
370
+
295
371
  @classmethod
296
372
  async def discover(cls) -> List[G90DiscoveredDevice]:
297
373
  """
@@ -1321,14 +1397,39 @@ class G90Alarm(G90NotificationProtocol):
1321
1397
 
1322
1398
  # pylint: disable=too-many-positional-arguments
1323
1399
  async def use_cloud_notifications(
1324
- self, cloud_local_host: str = CLOUD_NOTIFICATIONS_HOST,
1325
- cloud_local_port: int = CLOUD_NOTIFICATIONS_PORT,
1326
- upstream_host: Optional[str] = REMOTE_CLOUD_HOST,
1400
+ self,
1401
+ cloud_host: str,
1402
+ cloud_port: int,
1403
+ cloud_local_host: str = LOCAL_CLOUD_NOTIFICATIONS_HOST,
1404
+ cloud_local_port: int = LOCAL_CLOUD_NOTIFICATIONS_PORT,
1405
+ upstream_host: Optional[str] = None,
1327
1406
  upstream_port: Optional[int] = REMOTE_CLOUD_PORT,
1328
1407
  keep_single_connection: bool = True
1329
1408
  ) -> None:
1330
1409
  """
1331
1410
  Switches to use cloud notifications for device alerts.
1411
+
1412
+ Please note the method does not configure the panel for the host to
1413
+ receive the notifications - please invoke
1414
+ :meth:`G90Alarm.set_cloud_server_address` method to do that. The reason
1415
+ of that is configuring cloud server address on the panel is one-time
1416
+ operation, while the method will be called multiple times.
1417
+
1418
+ :param cloud_host: The cloud server host to connect to, should be
1419
+ reachable from the panel
1420
+ :param cloud_port: The cloud server port to connect to, should be
1421
+ reachable from the panel
1422
+ :param cloud_local_host: Local host to bind cloud notifications
1423
+ listener to
1424
+ :param cloud_local_port: Local port to bind cloud notifications
1425
+ listener to, should match `cloud_port` above unless network setup
1426
+ dictates otherwise
1427
+ :param upstream_host: Optional upstream host to connect to cloud
1428
+ server through
1429
+ :param upstream_port: Optional upstream port to connect to cloud
1430
+ server through
1431
+ :param keep_single_connection: If enabled, keeps a single connection
1432
+ to the upstream cloud server for both sending and receiving data
1332
1433
  """
1333
1434
  await self.close_notifications()
1334
1435
 
@@ -1338,6 +1439,8 @@ class G90Alarm(G90NotificationProtocol):
1338
1439
  upstream_port=upstream_port,
1339
1440
  local_host=cloud_local_host,
1340
1441
  local_port=cloud_local_port,
1442
+ cloud_host=cloud_host,
1443
+ cloud_port=cloud_port,
1341
1444
  keep_single_connection=keep_single_connection
1342
1445
  )
1343
1446
 
@@ -37,7 +37,7 @@ from .protocol import (
37
37
  )
38
38
  from .const import G90CloudDirection, G90CloudCommand
39
39
  from ..const import (
40
- G90AlertStateChangeTypes, REMOTE_CLOUD_HOST, REMOTE_CLOUD_PORT,
40
+ G90AlertStateChangeTypes,
41
41
  G90AlertTypes, G90AlertSources, G90AlertStates,
42
42
  )
43
43
  from ..definitions.base import G90PeripheralTypes
@@ -212,15 +212,11 @@ class G90CloudHelloDiscoveryRespMessage(G90CloudMessage):
212
212
  _source = G90CloudDirection.CLOUD_DISCOVERY
213
213
  _destination = G90CloudDirection.DEVICE
214
214
 
215
- # Simulated cloud response always contains known IP address of the vendor's
216
- # cloud service - that is, all interactions between alarm panel and
217
- # simulated cloud service will use same IP address for unification (i.e.
218
- # traffic redicrection will always be used to divert panel's cloud traffic
219
- # to the simulated cloud service)
220
- ip_addr: bytes = REMOTE_CLOUD_HOST.encode()
215
+ # The default values are set in `__post_init__()` method below
216
+ ip_addr: bytes = b''
221
217
  flag2: int = 0
222
218
  flag3: int = 0
223
- port: int = REMOTE_CLOUD_PORT
219
+ port: int = 0
224
220
  _timestamp: int = 0 # unix timestamp
225
221
 
226
222
  def __post_init__(self, context: G90CloudMessageContext) -> None:
@@ -45,7 +45,6 @@ from .messages import (
45
45
  )
46
46
  from ..notifications.base import G90NotificationsBase
47
47
  from ..notifications.protocol import G90NotificationProtocol
48
- from ..const import (REMOTE_CLOUD_HOST, REMOTE_CLOUD_PORT)
49
48
 
50
49
 
51
50
  _LOGGER = logging.getLogger(__name__)
@@ -75,6 +74,7 @@ class G90CloudNotifications(G90NotificationsBase, asyncio.Protocol):
75
74
  self,
76
75
  protocol_factory: Callable[[], G90NotificationProtocol],
77
76
  local_host: str, local_port: int,
77
+ cloud_host: str, cloud_port: int,
78
78
  upstream_host: Optional[str] = None,
79
79
  upstream_port: Optional[int] = None,
80
80
  keep_single_connection: bool = True,
@@ -84,6 +84,8 @@ class G90CloudNotifications(G90NotificationsBase, asyncio.Protocol):
84
84
  self._server: Optional[asyncio.Server] = None
85
85
  self._local_host = local_host
86
86
  self._local_port = local_port
87
+ self._cloud_host = cloud_host
88
+ self._cloud_port = cloud_port
87
89
  self._upstream_host = upstream_host
88
90
  self._upstream_port = upstream_port
89
91
  self._keep_single_connection = keep_single_connection
@@ -164,8 +166,8 @@ class G90CloudNotifications(G90NotificationsBase, asyncio.Protocol):
164
166
  device_id=self.device_id,
165
167
  local_host=self._local_host,
166
168
  local_port=self._local_port,
167
- cloud_host=REMOTE_CLOUD_HOST,
168
- cloud_port=REMOTE_CLOUD_PORT,
169
+ cloud_host=self._cloud_host,
170
+ cloud_port=self._cloud_port,
169
171
  upstream_host=self._upstream_host,
170
172
  upstream_port=self._upstream_port,
171
173
  remote_host=host,
pyg90alarm/const.py CHANGED
@@ -30,9 +30,8 @@ REMOTE_TARGETED_DISCOVERY_PORT = 12900
30
30
  LOCAL_TARGETED_DISCOVERY_PORT = 12901
31
31
  LOCAL_NOTIFICATIONS_HOST = '0.0.0.0'
32
32
  LOCAL_NOTIFICATIONS_PORT = 12901
33
- CLOUD_NOTIFICATIONS_HOST = '0.0.0.0'
34
- CLOUD_NOTIFICATIONS_PORT = 5678
35
- REMOTE_CLOUD_HOST = '47.88.7.61'
33
+ LOCAL_CLOUD_NOTIFICATIONS_HOST = '0.0.0.0'
34
+ LOCAL_CLOUD_NOTIFICATIONS_PORT = 5678
36
35
  REMOTE_CLOUD_PORT = 5678
37
36
  DEVICE_REGISTRATION_TIMEOUT = 30
38
37
  ROOM_ID = 0
@@ -42,7 +41,14 @@ CMD_PAGE_SIZE = 10
42
41
  BUG_REPORT_URL = 'https://github.com/hostcc/pyg90alarm/issues'
43
42
 
44
43
 
45
- class G90Commands(IntEnum):
44
+ class G90CommandsBase(IntEnum):
45
+ """
46
+ Base class for G90Commands and G90SystemCommands to allow proper typing
47
+ with subclasses of `G90BaseCommand`.
48
+ """
49
+
50
+
51
+ class G90Commands(G90CommandsBase):
46
52
  """
47
53
  Defines the alarm panel commands and their codes.
48
54
 
@@ -163,6 +169,27 @@ class G90Commands(IntEnum):
163
169
  PING = 219
164
170
 
165
171
 
172
+ class G90SystemCommands(G90CommandsBase):
173
+ """
174
+ Defines system commands for the G90 alarm panel.
175
+ """
176
+ GET_CONFIGURATION = 0
177
+ SET_CONFIGURATION = 1
178
+ GSM_REBOOT = 1129
179
+ MCU_REBOOT = 1123
180
+ WIFI_REBOOT = 1006
181
+
182
+
183
+ class G90SystemConfigurationCommands(IntEnum):
184
+ """
185
+ Defines sub-commands for getting/setting system configuration.
186
+
187
+ Applicable only to `G90SystemCommands.GET_CONFIGURATION` and
188
+ `G90SystemCommands.SET_CONFIGURATION` commands above.
189
+ """
190
+ SERVER_ADDRESS = 78
191
+
192
+
166
193
  class G90MessageTypes(IntEnum):
167
194
  """
168
195
  Defines message types (codes) from messages coming from the alarm panel.
@@ -28,40 +28,34 @@ import asyncio
28
28
  from asyncio import Future
29
29
  from asyncio.protocols import DatagramProtocol
30
30
  from asyncio.transports import DatagramTransport, BaseTransport
31
- from typing import Optional, Tuple, List, Any
31
+ from typing import Optional, Tuple, List, Any, TypeVar, Generic
32
32
  from dataclasses import dataclass
33
+ # `Self` is available in `typing` module only starting from Python 3.11, for
34
+ # older versions need to use typing_extensions`
35
+ from typing_extensions import Self
33
36
  from ..exceptions import (
34
37
  G90Error, G90TimeoutError, G90CommandFailure, G90CommandError
35
38
  )
36
- from ..const import G90Commands
39
+ from ..const import G90Commands, G90CommandsBase
37
40
 
38
41
 
39
42
  _LOGGER = logging.getLogger(__name__)
40
- G90BaseCommandData = List[Any]
41
43
 
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
44
+ CommandT = TypeVar('CommandT', bound=G90CommandsBase)
45
+ CommandDataT = TypeVar('CommandDataT')
52
46
 
53
47
 
54
- class G90BaseCommand(DatagramProtocol):
48
+ class G90Command(DatagramProtocol, Generic[CommandT, CommandDataT]):
55
49
  """
56
- Implements basic command handling for alarm panel protocol.
50
+ Base class for command handling for alarm panel protocol.
57
51
  """
58
52
  # pylint: disable=too-many-instance-attributes
59
53
  # Lock need to be shared across all of the class instances
60
54
  _sk_lock = asyncio.Lock()
61
55
 
62
56
  # pylint: disable=too-many-positional-arguments,too-many-arguments
63
- def __init__(self, host: str, port: int, code: G90Commands,
64
- data: Optional[G90BaseCommandData] = None,
57
+ def __init__(self, host: str, port: int, code: CommandT,
58
+ data: Optional[CommandDataT] = None,
65
59
  local_port: Optional[int] = None,
66
60
  timeout: float = 3.0, retries: int = 3) -> None:
67
61
  self._remote_host = host
@@ -70,18 +64,11 @@ class G90BaseCommand(DatagramProtocol):
70
64
  self._code = code
71
65
  self._timeout = timeout
72
66
  self._retries = retries
73
- self._data = '""'
74
- self._result: G90BaseCommandData = []
67
+ self._result: Optional[CommandDataT] = None
75
68
  self._connection_result: Optional[
76
69
  Future[Tuple[str, int, bytes]]
77
70
  ] = 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()
71
+ self._data = self.encode_data(data)
85
72
 
86
73
  # Implementation of datagram protocol,
87
74
  # https://docs.python.org/3/library/asyncio-protocol.html#datagram-protocols
@@ -139,92 +126,36 @@ class G90BaseCommand(DatagramProtocol):
139
126
 
140
127
  return (transport, protocol)
141
128
 
142
- def to_wire(self) -> bytes:
129
+ def encode_data(self, data: Optional[CommandDataT]) -> str:
143
130
  """
144
- Returns the command in wire format.
131
+ Encodes the command data to JSON string.
145
132
  """
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
133
+ raise NotImplementedError()
150
134
 
151
- def from_wire(self, data: bytes) -> G90BaseCommandData:
135
+ def decode_data(self, payload: Optional[str]) -> CommandDataT:
152
136
  """
153
- Parses the response from the alarm panel.
137
+ Decodes the command data from JSON string.
154
138
  """
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 []
139
+ raise NotImplementedError()
163
140
 
164
- def _parse(self, data: str) -> None:
141
+ def to_wire(self) -> bytes:
165
142
  """
166
- Processes the response from the alarm panel.
143
+ Serializes the command to wire format.
167
144
  """
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
- )
145
+ raise NotImplementedError()
184
146
 
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}")
147
+ def from_wire(self, data: bytes) -> Optional[CommandDataT]:
148
+ """
149
+ Deserializes the command from wire format.
150
+ """
151
+ raise NotImplementedError()
221
152
 
222
153
  @property
223
- def result(self) -> G90BaseCommandData:
154
+ def result(self) -> CommandDataT:
224
155
  """
225
156
  The result of the command.
226
157
  """
227
- return self._result
158
+ raise NotImplementedError()
228
159
 
229
160
  @property
230
161
  def host(self) -> str:
@@ -240,7 +171,14 @@ class G90BaseCommand(DatagramProtocol):
240
171
  """
241
172
  return self._remote_port
242
173
 
243
- async def process(self) -> G90BaseCommand:
174
+ @property
175
+ def expects_response(self) -> bool:
176
+ """
177
+ Indicates whether the command expects a response.
178
+ """
179
+ return True
180
+
181
+ async def process(self) -> Self: # G90Command[CommandT, CommandDataT]:
244
182
  """
245
183
  Processes the command.
246
184
  """
@@ -259,6 +197,9 @@ class G90BaseCommand(DatagramProtocol):
259
197
  _LOGGER.debug('(code %s) Sending request to %s:%s',
260
198
  self._code, self.host, self.port)
261
199
  transport.sendto(self.to_wire())
200
+ if not self.expects_response:
201
+ self._result = None
202
+ return self
262
203
  done, _ = await asyncio.wait([self._connection_result],
263
204
  timeout=self._timeout)
264
205
  if self._connection_result in done:
@@ -290,4 +231,133 @@ class G90BaseCommand(DatagramProtocol):
290
231
  Returns string representation of the command.
291
232
  """
292
233
  return f'Command: {self._code}, request: {self._data},' \
293
- f' response: {self._resp.data}'
234
+ f' response: {self.result}'
235
+
236
+
237
+ BaseCommandsT = G90Commands
238
+ BaseCommandsDataT = List[Any]
239
+
240
+
241
+ @dataclass
242
+ class G90Header:
243
+ """
244
+ Represents JSON structure of the header used in base panel commands.
245
+
246
+ :meta private:
247
+ """
248
+ code: Optional[int] = None
249
+ data: Optional[BaseCommandsDataT] = None
250
+
251
+
252
+ class G90BaseCommand(G90Command[BaseCommandsT, BaseCommandsDataT]):
253
+ """
254
+ Class for handling base G90 panel commands.
255
+ """
256
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
257
+ super().__init__(*args, **kwargs)
258
+ self._resp = G90Header()
259
+
260
+ def encode_data(self, data: Optional[BaseCommandsDataT]) -> str:
261
+ """
262
+ Encodes the command data to JSON string.
263
+ """
264
+ if data is None:
265
+ return '""'
266
+ return json.dumps([self._code, data],
267
+ # No newlines to be inserted
268
+ indent=None,
269
+ # No whitespace around entities
270
+ separators=(',', ':'))
271
+
272
+ def decode_data(self, payload: Optional[str]) -> BaseCommandsDataT:
273
+ """
274
+ Decodes the command data from JSON string.
275
+ """
276
+ # Also, panel may report an error supplying specific reason, e.g.
277
+ # command and its arguments that have failed
278
+ if not payload:
279
+ return []
280
+ if payload.startswith('error'):
281
+ error = payload[5:]
282
+ raise G90CommandError(
283
+ f"Command {self._code.name}"
284
+ f" (code={self._code.value}) failed"
285
+ f" with error: '{error}'")
286
+
287
+ resp = None
288
+ try:
289
+ resp = json.loads(payload, strict=False)
290
+ except json.JSONDecodeError as exc:
291
+ raise G90Error(
292
+ f"Unable to parse response as JSON: '{payload}'"
293
+ ) from exc
294
+
295
+ if not isinstance(resp, list):
296
+ raise G90Error(
297
+ f"Malformed response, 'list' expected: '{payload}'"
298
+ )
299
+
300
+ if resp is not None:
301
+ self._resp = G90Header(*resp)
302
+ _LOGGER.debug('Parsed from wire: %s', self._resp)
303
+
304
+ if not self._resp.code:
305
+ raise G90Error(f"Missing code in response: '{payload}'")
306
+ # Check there is data if the response is non-empty
307
+ if not self._resp.data:
308
+ raise G90Error(f"Missing data in response: '{payload}'")
309
+
310
+ if self._resp.code != self._code:
311
+ raise G90Error(
312
+ 'Wrong response - received code '
313
+ f"{self._resp.code}, expected code {self._code}")
314
+
315
+ return self._resp.data or []
316
+
317
+ def to_wire(self) -> bytes:
318
+ """
319
+ Returns the command in wire format.
320
+ """
321
+ wire = bytes(f'ISTART[{self._code},{self._code},{self._data}]IEND\0',
322
+ 'utf-8')
323
+ _LOGGER.debug('Encoded to wire format %s', wire)
324
+ return wire
325
+
326
+ def from_wire(self, data: bytes) -> BaseCommandsDataT:
327
+ """
328
+ Parses the response from the alarm panel.
329
+ """
330
+ _LOGGER.debug('To be decoded from wire format %s', data)
331
+ try:
332
+ decoded_data = data.decode('utf-8')
333
+ if not decoded_data.startswith('ISTART'):
334
+ raise G90Error('Missing start marker in data')
335
+ if not decoded_data.endswith('IEND\0'):
336
+ raise G90Error('Missing end marker in data')
337
+ payload = decoded_data[6:-5]
338
+ _LOGGER.debug("Decoded from wire: string '%s'", payload)
339
+
340
+ if not payload:
341
+ return []
342
+
343
+ # Panel may report the last command has failed
344
+ if payload == 'fail':
345
+ raise G90CommandFailure(
346
+ f"Command {self._code.name}"
347
+ f" (code={self._code.value}) failed"
348
+ )
349
+
350
+ return self.decode_data(payload)
351
+ except UnicodeDecodeError as exc:
352
+ raise G90Error(
353
+ 'Unable to decode response from UTF-8'
354
+ ) from exc
355
+
356
+ @property
357
+ def result(self) -> BaseCommandsDataT:
358
+ """
359
+ The result of the command.
360
+ """
361
+ if self._result is None:
362
+ return []
363
+ return self._result
@@ -43,10 +43,23 @@ class G90HostInfoWifiStatus(IntEnum):
43
43
  """
44
44
  POWERED_OFF = 0
45
45
  NOT_CONNECTED = 1
46
- NO_SIGNAL = 2
46
+ SERVER_NOT_CONNECTED = 2
47
47
  OPERATIONAL = 3
48
48
 
49
49
 
50
+ class G90HostInfoWifiSetupProgress(IntEnum):
51
+ """
52
+ Defines possible values of Wifi connection progress.
53
+ """
54
+ IDLE = 0
55
+ CONNECTING = 1
56
+ OK = 2
57
+ WRONG_SSID = 3
58
+ WRONG_PASSWORD = 4
59
+ CONNECTION_ERROR = 5
60
+ WIFI_ERROR = 6
61
+
62
+
50
63
  @dataclass
51
64
  class G90HostInfo: # pylint: disable=too-many-instance-attributes
52
65
  """
@@ -60,11 +73,11 @@ class G90HostInfo: # pylint: disable=too-many-instance-attributes
60
73
  wifi_hw_version: str
61
74
  gsm_status_data: int
62
75
  wifi_status_data: int
63
- reserved1: int
64
- reserved2: int
65
- band_frequency: str
66
- gsm_signal_level: int
67
- wifi_signal_level: int
76
+ gprs_3g_active_data: int
77
+ wifi_setup_progress_data: int
78
+ battery_voltage: str # in mV
79
+ gsm_signal_level: int # percentage 0-100
80
+ wifi_signal_level: int # percentage 0-100
68
81
 
69
82
  @property
70
83
  def gsm_status(self) -> G90HostInfoGsmStatus:
@@ -82,6 +95,21 @@ class G90HostInfo: # pylint: disable=too-many-instance-attributes
82
95
  """
83
96
  return G90HostInfoWifiStatus(self.wifi_status_data)
84
97
 
98
+ @property
99
+ def wifi_setup_progress(self) -> G90HostInfoWifiSetupProgress:
100
+ """
101
+ Translates the Wifi connection progress received from the device into
102
+ corresponding enum.
103
+ """
104
+ return G90HostInfoWifiSetupProgress(self.wifi_setup_progress_data)
105
+
106
+ @property
107
+ def gprs_3g_active(self) -> bool:
108
+ """
109
+ Indicates whether GPRS/3G is enabled.
110
+ """
111
+ return bool(self.gprs_3g_active_data)
112
+
85
113
  def _asdict(self) -> Dict[str, Any]:
86
114
  """
87
115
  Returns the host information as dictionary.
@@ -23,9 +23,9 @@ Implements paginated command for G90 alarm panel protocol.
23
23
  """
24
24
  from __future__ import annotations
25
25
  import logging
26
- from typing import Any, cast
26
+ from typing import Any, Optional
27
27
  from dataclasses import dataclass
28
- from .base_cmd import G90BaseCommand, G90BaseCommandData
28
+ from .base_cmd import G90BaseCommand, BaseCommandsDataT
29
29
  from ..exceptions import G90Error
30
30
  from ..const import G90Commands
31
31
 
@@ -82,12 +82,11 @@ class G90PaginatedCommand(G90BaseCommand):
82
82
  """
83
83
  return self._nelems
84
84
 
85
- def _parse(self, data: str) -> None:
85
+ def decode_data(self, payload: Optional[str]) -> BaseCommandsDataT:
86
86
  """
87
87
  Parses the response from the alarm panel.
88
88
  """
89
- super()._parse(data)
90
- resp_data: G90BaseCommandData = self._resp.data or []
89
+ resp_data = super().decode_data(payload)
91
90
  try:
92
91
  page_data = resp_data.pop(0)
93
92
  page_info = G90PaginationFields(*page_data)
@@ -125,8 +124,4 @@ class G90PaginatedCommand(G90BaseCommand):
125
124
  'total records %s, start record %s, record count %s',
126
125
  page_info.total, page_info.start, page_info.nelems)
127
126
 
128
- async def process(self) -> G90PaginatedCommand:
129
- """
130
- Initiates the command processing.
131
- """
132
- return cast(G90PaginatedCommand, await super().process())
127
+ return resp_data
@@ -0,0 +1,276 @@
1
+ # Copyright (c) 2026 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
+ Provides support for system commands of the G90 alarm panel.
22
+ """
23
+ from __future__ import annotations
24
+ from typing import Any, Optional, TypeVar, List
25
+ import logging
26
+
27
+ from ..const import G90SystemCommands, G90SystemConfigurationCommands
28
+ from .base_cmd import G90Command
29
+
30
+ _LOGGER = logging.getLogger(__name__)
31
+
32
+
33
+ SystemCommandsT = TypeVar('SystemCommandsT', bound=G90SystemCommands)
34
+ SystemCommandsDataT = TypeVar('SystemCommandsDataT')
35
+
36
+
37
+ class G90SystemCommandBase(G90Command[SystemCommandsT, SystemCommandsDataT]):
38
+ """
39
+ Base class for system commands of the G90 alarm panel.
40
+
41
+ :param code: System command code
42
+ :param kwargs: Additional arguments passed to the base class
43
+ """
44
+ def __init__(
45
+ self, *, code: SystemCommandsT, **kwargs: Any
46
+ ) -> None:
47
+ super().__init__(code=code, **kwargs)
48
+
49
+ @property
50
+ def expects_response(self) -> bool:
51
+ """
52
+ Indicates whether the command expects a response.
53
+
54
+ System commands do not return any response.
55
+
56
+ :return: False
57
+ """
58
+ return False
59
+
60
+ def encode_data(self, data: Optional[SystemCommandsDataT]) -> str:
61
+ """
62
+ Encodes the command data to string.
63
+
64
+ :param data: Data for the command
65
+ :return: Encoded data string
66
+ """
67
+ # Placeholder as required by the base class
68
+ raise NotImplementedError() # pragma: no cover
69
+
70
+ def decode_data(self, payload: Optional[str]) -> SystemCommandsDataT:
71
+ """
72
+ Decodes the command data from string.
73
+
74
+ :param payload: Payload string
75
+ :return: Decoded data
76
+ """
77
+ # Placeholder as required by the base class
78
+ raise NotImplementedError() # pragma: no cover
79
+
80
+ def to_wire(self) -> bytes:
81
+ """
82
+ Serializes the command to wire format as expected by the panel.
83
+
84
+ :return: Wire data
85
+ """
86
+ wire = bytes(
87
+ f'ISTART[0,100,"AT^IWT={self._code}{self._data},IWT"]IEND\0',
88
+ 'utf-8'
89
+ )
90
+
91
+ _LOGGER.debug('Encoded to wire format %s', wire)
92
+ return wire
93
+
94
+ def from_wire(self, data: bytes) -> SystemCommandsDataT:
95
+ """
96
+ Deserializes the command from wire format.
97
+
98
+ :param data: Wire data
99
+ :return: Decoded data
100
+ """
101
+ # Placeholder as required by the base class
102
+ raise NotImplementedError() # pragma: no cover
103
+
104
+ @property
105
+ def result(self) -> SystemCommandsDataT:
106
+ """
107
+ The result of the command.
108
+
109
+ :return: Decoded data
110
+ """
111
+ # Placeholder as required by the base class
112
+ raise NotImplementedError() # pragma: no cover
113
+
114
+
115
+ class G90SystemCommand(
116
+ G90SystemCommandBase[SystemCommandsT, str]
117
+ ):
118
+ """
119
+ Represents a system command of the G90 alarm panel.
120
+
121
+ :param code: System command code
122
+ :param kwargs: Additional arguments passed to the base class
123
+ """
124
+ def __init__(
125
+ self, *, code: SystemCommandsT, **kwargs: Any
126
+ ) -> None:
127
+ if code in [G90SystemCommands.SET_CONFIGURATION,
128
+ G90SystemCommands.GET_CONFIGURATION]:
129
+ raise ValueError(
130
+ 'Use G90SystemConfigurationCommand class for '
131
+ 'SET_CONFIGURATION and GET_CONFIGURATION commands'
132
+ )
133
+ super().__init__(code=code, **kwargs)
134
+
135
+ def encode_data(self, data: Optional[str]) -> str:
136
+ """
137
+ Encodes the command data to string.
138
+
139
+ :param data: Data for the command
140
+ :return: Encoded data string
141
+ """
142
+ return data or ''
143
+
144
+ def decode_data(self, payload: Optional[str]) -> str:
145
+ """
146
+ Decodes the command data from string.
147
+
148
+ No response is expected for system commands.
149
+
150
+ :param payload: Payload string (ignored)
151
+ :return: Empty string
152
+ """
153
+ # No response is expected for system commands
154
+ return '' # pragma: no cover
155
+
156
+ def from_wire(self, data: bytes) -> str:
157
+ """
158
+ Deserializes the command from wire format.
159
+
160
+ No response is expected for system commands.
161
+
162
+ :param data: Wire data (ignored)
163
+ :return: Empty string
164
+ """
165
+ # No response is expected for system commands
166
+ return '' # pragma: no cover
167
+
168
+ @property
169
+ def result(self) -> str:
170
+ """
171
+ The result of the command.
172
+
173
+ No response is expected for system commands.
174
+
175
+ :return: Empty string
176
+ """
177
+ # No response is expected for system commands
178
+ return ''
179
+
180
+
181
+ class G90SystemConfigurationCommand(
182
+ G90SystemCommandBase[G90SystemCommands, List[str]]
183
+ ):
184
+ """
185
+ Represents a system configuration command of the G90 alarm panel.
186
+
187
+ :param cmd: Sub-command code for configuration command
188
+ :param data: Data for the command
189
+ :param kwargs: Additional arguments passed to the base class
190
+ """
191
+ def __init__(
192
+ self, *, cmd: G90SystemConfigurationCommands,
193
+ data: Optional[List[str]] = None,
194
+ **kwargs: Any
195
+ ) -> None:
196
+ self._cmd = cmd
197
+ super().__init__(
198
+ code=G90SystemCommands.SET_CONFIGURATION, data=data, **kwargs
199
+ )
200
+
201
+ def encode_data(self, data: Optional[List[str]]) -> str:
202
+ """
203
+ Encodes the command data to string.
204
+
205
+ :param data: Data for the command
206
+ :return: Encoded data string
207
+ """
208
+ if data is None:
209
+ raise ValueError('Data must be provided for configuration command')
210
+
211
+ res = f',{self._cmd.value}={"&".join(data)}'
212
+ _LOGGER.debug('Encoded data "%s" to string format: %s', data, res)
213
+ return res
214
+
215
+ def decode_data(self, payload: Optional[str]) -> List[str]:
216
+ """
217
+ Decodes the command data from string.
218
+
219
+ System configuration commands do not return any response.
220
+
221
+ :param payload: Payload string
222
+ :return: Empty list
223
+ """
224
+ # System configuration commands do not return any response.
225
+ return [] # pragma: no cover
226
+
227
+ def from_wire(self, data: bytes) -> List[str]:
228
+ """
229
+ Deserializes the command from wire format.
230
+
231
+ System configuration commands do not return any response.
232
+
233
+ :param data: Wire data
234
+ :return: Empty list
235
+ """
236
+ # System configuration commands do not return any response.
237
+ return [] # pragma: no cover
238
+
239
+ @property
240
+ def result(self) -> List[str]:
241
+ """
242
+ The result of the command.
243
+
244
+ System configuration commands do not return any response.
245
+
246
+ :return: Empty list
247
+ """
248
+ # System configuration commands do not return any response.
249
+ return [] # pragma: no cover
250
+
251
+
252
+ class G90SetServerAddressCommand(
253
+ G90SystemConfigurationCommand
254
+ ):
255
+ """
256
+ Sets the server address for the panel to communicate with using the cloud
257
+ notifications protocol.
258
+
259
+ :param cloud_ip: IP address of the cloud server
260
+ :param cloud_port: Port number of the cloud server
261
+ :param kwargs: Additional arguments passed to the base class
262
+ """
263
+ def __init__(
264
+ self, *, cloud_ip: str, cloud_port: int,
265
+ **kwargs: Any
266
+ ) -> None:
267
+ super().__init__(
268
+ cmd=G90SystemConfigurationCommands.SERVER_ADDRESS,
269
+ # The command requires two fields for the cloud server address, 1st
270
+ # is used by the panel to communicate with the server, while 2nd
271
+ # might potentially be a fallback address or alike. However,
272
+ # experiments did not show any attempts by the panel to use the 2nd
273
+ # address, so we set both to the same value in an avoidance of
274
+ # doubt.
275
+ data=[cloud_ip, cloud_ip, str(cloud_port)], **kwargs
276
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyg90alarm
3
- Version: 2.5.3
3
+ Version: 2.7.0
4
4
  Summary: G90 Alarm system protocol
5
5
  Home-page: https://github.com/hostcc/pyg90alarm
6
6
  Author: Ilia Sotnikov
@@ -189,7 +189,7 @@ Cloud notifications
189
189
  The cloud protocol is native to the panel and is used to interact with mobile application. The package can mimic the cloud server and interpret the messages the panel sends to the cloud, allowing to receive the notifications and alerts.
190
190
  While the protocol also allows to send commands to the panel, it is not implemented and local protocol is used for that - i.e. when cloud notifications are in use the local protocol still utilized for sending commands to the panel.
191
191
 
192
- The cloud protocol is TCP based and typically interacts with cloud service at known IP address and port (not customizable at panel side). To process the cloud notifications all the traffic from panel towards the cloud (IP address ``47.88.7.61`` and TCP port ``5678`` as of writing) needs to be diverted to the node where the package is running - depending on your network equipment it could be port forwarding, DNAT or other means. It is unclear whether the panel utilizes DNS to resolve the cloud service IP address, hence the documentation only mentions IP-based traffic redirection.
192
+ The cloud protocol is TCP based and typically interacts with cloud service at known IP address and port, which could be customized. To process the cloud notifications all the traffic from panel towards the configured IP address service needs to be received by the node where the package is running.
193
193
 
194
194
  Please see
195
195
  `the section <docs/cloud-protocol.rst>`_ for further details on the protocol.
@@ -203,25 +203,6 @@ The package could act as:
203
203
  - Chained cloud server, where in addition to interpreting the notifications it
204
204
  will also forward all packets received from the panel to the cloud server, and pass its responses back to the panel. This allows to have notifications processed by the package and the mobile application working as well.
205
205
 
206
- .. note:: Sending packets upstream to the known IP address and port of the cloud server might result in those looped back (since traffic from panel to cloud service has to be redirected to the host where package runs), if your network equipment can't account for source address in redirection rules (i.e. limiting the port redirection to the panel's IP address). In that case you'll need another redirection, from the host where the package runs to the cloud service using an IP from your network. That way those two redirection rules will coexist correctly. To illustrate:
207
-
208
- Port forwarding rule 1:
209
-
210
- - Source: panel IP address
211
- - Destination: 47.88.7.61
212
- - Port: 5678
213
- - Redirect to host: host where package runs
214
- - Redirect to port: 5678 (or other port if you want to use it)
215
-
216
-
217
- Port forwarding rule 2 (optional):
218
-
219
- - Source: host where package runs
220
- - Destination: an IP address from your network
221
- - Port: 5678 (or other port if you want to use it)
222
- - Redirect to : 47.88.7.61
223
- - Redirect to port: 5678
224
-
225
206
  The code fragments below demonstrate how to utilize both modes - please note those are incomplete, since no callbacks are set to process the notifications.
226
207
 
227
208
  **Standalone mode**
@@ -232,10 +213,20 @@ The code fragments below demonstrate how to utilize both modes - please note tho
232
213
 
233
214
  # Create an instance of the alarm panel
234
215
  alarm = G90Alarm(host='<panel IP address>')
216
+
217
+ # Configure cloud server address the panel should use - the host running the
218
+ # package.
219
+ await alarm.set_cloud_server_address(
220
+ cloud_ip='<host IP address running the package>', cloud_port=5678
221
+ )
222
+
235
223
  # Enable cloud notifications
236
224
  await alarm.use_cloud_notifications(
237
- # Optional, see note above redirecting cloud traffic from panel
238
- local_port=5678,
225
+ # The host/port the package will listen on for the cloud notifications,
226
+ # should match ones above.
227
+ cloud_host='<host IP address running the package>',
228
+ cloud_port=5678,
229
+ cloud_local_port=5678,
239
230
  upstream_host=None
240
231
  )
241
232
  # Start listening for notifications
@@ -250,11 +241,22 @@ The code fragments below demonstrate how to utilize both modes - please note tho
250
241
 
251
242
  # Create an instance of the alarm panel
252
243
  alarm = G90Alarm(host='<panel IP address>')
244
+
245
+ # Configure cloud server address the panel should use - the host running the
246
+ # package.
247
+ await alarm.set_cloud_server_address(
248
+ cloud_ip='<host IP address running the package>', cloud_port=5678
249
+ )
250
+
253
251
  # Enable cloud notifications
254
252
  await alarm.use_cloud_notifications(
255
- # Optional, see note above redirecting cloud traffic from panel
256
- local_port=5678,
257
- # See note above re: cloud service and sending packets to it
253
+ # The host/port the package will listen on for the cloud notifications,
254
+ # should match ones above.
255
+ cloud_host='<host IP address running the package>',
256
+ cloud_port=5678,
257
+ cloud_local_port=5678,
258
+ # Upstream cloud server address the package should forward the
259
+ # notifications to.
258
260
  upstream_host='47.88.7.61',
259
261
  upstream_port=5678
260
262
  )
@@ -1,14 +1,14 @@
1
- pyg90alarm/__init__.py,sha256=ehUcPV8O2jBszU3hPAfI91nXjj2UXIfYjoREvL4QE2c,3719
2
- pyg90alarm/alarm.py,sha256=BDBVQ8qRa2Bx2-786uytShTw8znlFEWakxweCGvgxME,50130
1
+ pyg90alarm/__init__.py,sha256=Z1Y5bwYmtY5iycIv6IDQ464o4Q-sU7fo9S3f94NGOpg,3786
2
+ pyg90alarm/alarm.py,sha256=1Gj19HJVhqGv0ftpOYI7udOsa89Sswb5Fil63YAUYw4,54162
3
3
  pyg90alarm/callback.py,sha256=9PVtjRs2MLn80AgiM-UJNL8ZJF4_PxcopJIpxMmB3vc,4707
4
- pyg90alarm/const.py,sha256=cpaMlb8rqE1EfvmUEh3Yd_29dNO0usIo_Y-esPBh8B0,8012
4
+ pyg90alarm/const.py,sha256=ysGYgpxqQnHXwUS2n29WAnikzJFN7ZO97qnFejJnNrI,8678
5
5
  pyg90alarm/event_mapping.py,sha256=hSmRWkkuA5hlauGvYakdOrw8QFt0TMNfUuDQ4J3vHpQ,3438
6
6
  pyg90alarm/exceptions.py,sha256=9rSd5zIKALHtRHBBLD-R5zxW-5OBkHK04uVupCnAK8s,1937
7
7
  pyg90alarm/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  pyg90alarm/cloud/__init__.py,sha256=u7ZzrIwCsdgmBmgtpCe6c_Kipdo8nG8PEOEcaAHXCxI,1407
9
9
  pyg90alarm/cloud/const.py,sha256=FYUxkj0DSI7nnXZHsJMfv8RNQiXKV3TYKGHSdfHdMjg,2113
10
- pyg90alarm/cloud/messages.py,sha256=L3cpP3IbDRdw3W6FeQxTPXoDlpUv7Fm4t-RkH9Uj4dg,18032
11
- pyg90alarm/cloud/notifications.py,sha256=0RxCBVcvDuwE0I1m3SLDXDQqCJimDcN4f45gr-Hvt1A,15669
10
+ pyg90alarm/cloud/messages.py,sha256=dlsPBcEHKkjtEIS4UJwXa23WkmU9WQrK-Qo8pR6L_wY,17676
11
+ pyg90alarm/cloud/notifications.py,sha256=bCsz27TAwgtTKBArq-yfv4NSWsaoNtqlYhD5BuRrw68,15726
12
12
  pyg90alarm/cloud/protocol.py,sha256=82l2IXSM12tv_iWkTrAQZ-aw5UR4tmWFQJKVcgBfIww,16159
13
13
  pyg90alarm/dataclass/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  pyg90alarm/dataclass/load_save.py,sha256=I2WDeW4GingN8AEiwZm9Epf1gVL_Ynkise2h781Dn3I,8604
@@ -27,24 +27,25 @@ pyg90alarm/entities/sensor_list.py,sha256=0S88bhTn91O45WgbIIMQ0iXaNjlUWmMBOOFwj2
27
27
  pyg90alarm/local/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  pyg90alarm/local/alarm_phones.py,sha256=bGg4aEJwtl7oMlLdOGpW2Vv4q4zS9wzkpW4q3Qy2JCU,4375
29
29
  pyg90alarm/local/alert_config.py,sha256=n2dycEf6TH0MKefQXMcRsCFJyYvUMV55LRsy4FARtDI,6027
30
- pyg90alarm/local/base_cmd.py,sha256=f0PozHJjErIg08vn0sAacHbigJSFor8q5jdxfXiM27c,10491
30
+ pyg90alarm/local/base_cmd.py,sha256=kmjIFwt63sNxz2CdGw1wHv2LOsD8BJhRFxaPszhywSM,12591
31
31
  pyg90alarm/local/config.py,sha256=QYetAc6QLrAN8T-37D4Esifvao52w_uJ01nHciLbGME,1390
32
32
  pyg90alarm/local/discovery.py,sha256=8YVIXuNe57lhas0VSRf7QXZH83pEDGNj9oehNY4Kh2U,3576
33
33
  pyg90alarm/local/history.py,sha256=sL6_Z1BNYkuUwAZUi78d4p5hhcCfsXKw29i4Qu1O60M,10811
34
34
  pyg90alarm/local/host_config.py,sha256=RSvg2UXPTCzzwDW6Iz3CyFtjxPUzSSdlxRMqGpWA61M,7445
35
- pyg90alarm/local/host_info.py,sha256=4lFIaFEpYd3EvgNrDJmKijTrzX9i29nFISLLlXGnkmE,2759
35
+ pyg90alarm/local/host_info.py,sha256=gdneg2XxxMNSFfNTjXCqsBsLZgZr_wg38sudKJ24RD8,3550
36
36
  pyg90alarm/local/host_status.py,sha256=WHGtw-A0wHILqVWP4DnZhXj8DPRGyS26AV0bL1isTz4,1863
37
37
  pyg90alarm/local/net_config.py,sha256=Y4-SUVSukk-h5x3LK-HH3IlM72dP_46nj1H8lN9rTfo,5998
38
38
  pyg90alarm/local/notifications.py,sha256=Vs6NQJciYqDALV-WwzH6wIcTGdX_UD4XBuHWjSOpCDY,4591
39
- pyg90alarm/local/paginated_cmd.py,sha256=5pPVP8f4ydjgu8Yq6MwqINJAUt52fFlD17wO4AI88Pc,4467
39
+ pyg90alarm/local/paginated_cmd.py,sha256=mgk46Xz7vJP9qv2YZlUIzGhxRKReuydKVgcj-f8DkvE,4300
40
40
  pyg90alarm/local/paginated_result.py,sha256=p_e8QAVznp1Q5Xi9ifjb9Bx-S3ZiAkVlPKrY6r0bYLs,5483
41
+ pyg90alarm/local/system_cmd.py,sha256=LtkrGlBEjZKaNsycWDLS2lt8NLasWgRAMpMtpAD7AOk,8770
41
42
  pyg90alarm/local/targeted_discovery.py,sha256=Ik2C2VBtVLurf3-RKko4O2R3B6MrmFdOskd457uyASU,5516
42
43
  pyg90alarm/local/user_data_crc.py,sha256=JQBOPY3RlOgVtvR55R-rM8OuKjYW-BPXQ0W4pi6CEH0,1689
43
44
  pyg90alarm/notifications/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
45
  pyg90alarm/notifications/base.py,sha256=d3N_zNPa_jcTX4QpA78jdgMHDhmrgwqyM3HdvuO14Jk,16682
45
46
  pyg90alarm/notifications/protocol.py,sha256=TlZQ3P8-N-E2X5bzkGefz432x4lBYyIBF9VriwYn9ds,4790
46
- pyg90alarm-2.5.3.dist-info/licenses/LICENSE,sha256=f884inRbeNv-O-hbwz62Ro_1J8xiHRTnJ2cCx6A0WvU,1070
47
- pyg90alarm-2.5.3.dist-info/METADATA,sha256=HLrqjT7wB9IQvmuGlmX7NkJo0nijovzOjaRWlm78ong,12568
48
- pyg90alarm-2.5.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
49
- pyg90alarm-2.5.3.dist-info/top_level.txt,sha256=czHiGxYMyTk5QEDTDb0EpPiKqUMRa8zI4zx58Ii409M,11
50
- pyg90alarm-2.5.3.dist-info/RECORD,,
47
+ pyg90alarm-2.7.0.dist-info/licenses/LICENSE,sha256=f884inRbeNv-O-hbwz62Ro_1J8xiHRTnJ2cCx6A0WvU,1070
48
+ pyg90alarm-2.7.0.dist-info/METADATA,sha256=RpRAJB14iz-B6k5WGzjsjdk6Lhf90fHMgQ8zjfpIQjQ,11959
49
+ pyg90alarm-2.7.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
50
+ pyg90alarm-2.7.0.dist-info/top_level.txt,sha256=czHiGxYMyTk5QEDTDb0EpPiKqUMRa8zI4zx58Ii409M,11
51
+ pyg90alarm-2.7.0.dist-info/RECORD,,