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,132 @@
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 paginated command for G90 alarm panel protocol.
23
+ """
24
+ from __future__ import annotations
25
+ import logging
26
+ from typing import Any, cast
27
+ from dataclasses import dataclass
28
+ from .base_cmd import G90BaseCommand, G90BaseCommandData
29
+ from ..exceptions import G90Error
30
+ from ..const import G90Commands
31
+
32
+ _LOGGER = logging.getLogger(__name__)
33
+
34
+
35
+ @dataclass
36
+ class G90PaginationFields:
37
+ """
38
+ Represents structure of the pagination fields used by alarm panel.
39
+
40
+ :meta private:
41
+ """
42
+ total: int
43
+ start: int
44
+ nelems: int
45
+
46
+
47
+ class G90PaginatedCommand(G90BaseCommand):
48
+ """
49
+ Implements paginated command for alarm panel protocol.
50
+ """
51
+ # pylint: disable=too-many-positional-arguments,too-many-arguments
52
+ def __init__(
53
+ self, host: str, port: int, code: G90Commands, start: int, end: int,
54
+ **kwargs: Any
55
+ ) -> None:
56
+ self._start = start
57
+ self._end = end
58
+ self._expected_nelems = end - start + 1
59
+ self._nelems = 0
60
+ self._total = 0
61
+ super().__init__(host, port, code, [self._start, self._end],
62
+ **kwargs)
63
+
64
+ @property
65
+ def total(self) -> int:
66
+ """
67
+ Total number of records available.
68
+ """
69
+ return self._total
70
+
71
+ @property
72
+ def start(self) -> int:
73
+ """
74
+ Index of the first record in the response.
75
+ """
76
+ return self._start
77
+
78
+ @property
79
+ def count(self) -> int:
80
+ """
81
+ Number of records in the response.
82
+ """
83
+ return self._nelems
84
+
85
+ def _parse(self, data: str) -> None:
86
+ """
87
+ Parses the response from the alarm panel.
88
+ """
89
+ super()._parse(data)
90
+ resp_data: G90BaseCommandData = self._resp.data or []
91
+ try:
92
+ page_data = resp_data.pop(0)
93
+ page_info = G90PaginationFields(*page_data)
94
+ except TypeError as exc:
95
+ raise G90Error(f'Wrong pagination data {page_data} - {str(exc)}'
96
+ ) from exc
97
+ except IndexError as exc:
98
+ raise G90Error(f"Missing pagination in response '{self._resp}'"
99
+ ) from exc
100
+
101
+ self._total = page_info.total
102
+ self._start = page_info.start
103
+ self._nelems = page_info.nelems
104
+
105
+ errors = []
106
+ if self._nelems != len(resp_data):
107
+ qualifier = (
108
+ "Truncated" if self._nelems > len(resp_data) else "Extra"
109
+ )
110
+ errors.append(
111
+ f'{qualifier} data provided in paginated response -'
112
+ f' expected {self._nelems} entities as per response,'
113
+ f' received {len(resp_data)}')
114
+
115
+ if self._expected_nelems < len(resp_data):
116
+ errors.append(
117
+ f'Extra data provided in paginated response -'
118
+ f' expected {self._expected_nelems} entities as per request,'
119
+ f' received {len(resp_data)}')
120
+
121
+ if errors:
122
+ raise G90Error('. '.join(errors))
123
+
124
+ _LOGGER.debug('Paginated command response: '
125
+ 'total records %s, start record %s, record count %s',
126
+ page_info.total, page_info.start, page_info.nelems)
127
+
128
+ async def process(self) -> G90PaginatedCommand:
129
+ """
130
+ Initiates the command processing.
131
+ """
132
+ return cast(G90PaginatedCommand, await super().process())
@@ -0,0 +1,135 @@
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
+ Extends paginated command for G90 alarm panel providing convenience interface
23
+ to work with results of paginated commands.
24
+ """
25
+
26
+ import logging
27
+ from typing import Any, Optional, AsyncGenerator, Iterable, cast
28
+ from dataclasses import dataclass
29
+ from .paginated_cmd import G90PaginatedCommand
30
+ from ..const import (
31
+ G90Commands,
32
+ CMD_PAGE_SIZE,
33
+ )
34
+
35
+ _LOGGER = logging.getLogger(__name__)
36
+
37
+
38
+ @dataclass
39
+ class G90PaginatedResponse:
40
+ """
41
+ Response yielded from the :meth:`.G90PaginatedResult.process` method
42
+ """
43
+ proto_idx: int
44
+ data: str
45
+
46
+
47
+ class G90PaginatedResult:
48
+ """
49
+ Processes paginated response from G90 corresponding panel commands.
50
+ """
51
+ # pylint: disable=too-few-public-methods
52
+ # pylint: disable=too-many-positional-arguments,too-many-arguments
53
+ def __init__(
54
+ self, host: str, port: int, code: G90Commands, start: int = 1,
55
+ end: Optional[int] = None, **kwargs: Any
56
+ ):
57
+ self._host = host
58
+ self._port = port
59
+ self._code = code
60
+ self._start = start
61
+ self._end = end
62
+ self._kwargs = kwargs
63
+
64
+ async def process(self) -> AsyncGenerator[G90PaginatedResponse, None]:
65
+ """
66
+ Process paginated response yielding :class:`.G90PaginatedResponse`
67
+ instance for each element.
68
+ """
69
+ page = CMD_PAGE_SIZE
70
+ start = self._start
71
+ count = 0
72
+ while True:
73
+ # The start record number is one-based, so subtract one when
74
+ # calculating the number of the end record for the current
75
+ # iteration
76
+ end = start + page - 1
77
+ # Use the smallest of requested end record number and calculated
78
+ # one (based of page size), allows for number of records less than
79
+ # in page
80
+ if self._end:
81
+ end = min(end, self._end)
82
+
83
+ _LOGGER.debug('Invoking paginated command for %s..%s range',
84
+ start, end)
85
+ cmd = await G90PaginatedCommand(
86
+ host=self._host, port=self._port, code=self._code,
87
+ start=start, end=end,
88
+ **self._kwargs
89
+ ).process()
90
+
91
+ # The caller didn't supply the end record number, use the records
92
+ # total since it is now known
93
+ if not self._end:
94
+ self._end = cmd.total
95
+ # The supplied end record number is higher than total records
96
+ # available, reset to the latter
97
+ if self._end > cmd.total:
98
+ _LOGGER.warning('Requested record range (%i) exceeds number of'
99
+ ' available records (%i), setting to the'
100
+ ' latter', self._end, cmd.total)
101
+ self._end = cmd.total
102
+
103
+ _LOGGER.debug('Retrieved %i records in the iteration,'
104
+ ' %i available in total, target end'
105
+ ' record number is %i',
106
+ cmd.count, cmd.total, self._end)
107
+
108
+ # Produce the resulting records for the consumer
109
+ for idx, data in enumerate(cast(Iterable[str], cmd.result)):
110
+ # Protocol uses one-based indexes, `start` implies that so no
111
+ # further additions to resulting value is needed.
112
+ # Note the index provided here is running one across multiple
113
+ # pages hence use of `start` variable
114
+ yield G90PaginatedResponse(start + idx, data)
115
+
116
+ # Count the number of processed records
117
+ count += cmd.count
118
+
119
+ # End the loop if we processed same number of sensors as in the
120
+ # pagination header (or attempted to process more than that by
121
+ # an error), or no records have been received
122
+ if not cmd.count:
123
+ break
124
+ if cmd.start + cmd.count - 1 >= self._end:
125
+ break
126
+ # Move to the next page for another iteration
127
+ start = start + page
128
+
129
+ _LOGGER.debug('Total number of paginated entries:'
130
+ ' processed %s, expected %s',
131
+ count,
132
+ # Again, both end and start record numbers are one-based,
133
+ # so need to add one to calculate how many records have
134
+ # been requested
135
+ self._end - self._start + 1)
@@ -0,0 +1,162 @@
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
+ Discovers G90 alarm panel devices with specific ID.
23
+ """
24
+ from __future__ import annotations
25
+ import logging
26
+ from typing import Tuple, Any, Optional, Dict, List
27
+ from dataclasses import dataclass, asdict
28
+ import asyncio
29
+ from asyncio.transports import BaseTransport
30
+ from .base_cmd import G90BaseCommand
31
+ from ..const import G90Commands
32
+ from ..exceptions import G90Error
33
+
34
+ _LOGGER = logging.getLogger(__name__)
35
+
36
+
37
+ @dataclass
38
+ # pylint: disable=too-many-instance-attributes
39
+ class G90TargetedDiscoveryInfo:
40
+ """
41
+ Wire representation of the information about discovered device.
42
+ """
43
+ message: str
44
+ product_name: str
45
+ wifi_protocol_version: str
46
+ cloud_protocol_version: str
47
+ mcu_hw_version: str
48
+ fw_version: str
49
+ gsm_status: str
50
+ wifi_status: str
51
+ server_status: str
52
+ reserved1: str
53
+ reserved2: str
54
+ gsm_signal_level: str
55
+ wifi_signal_level: str
56
+
57
+ def _asdict(self) -> Dict[str, Any]:
58
+ """
59
+ Returns the information about discovered device as dictionary.
60
+ """
61
+ return asdict(self)
62
+
63
+
64
+ @dataclass
65
+ class G90DiscoveredDeviceTargeted(G90TargetedDiscoveryInfo):
66
+ """
67
+ Discovered device with specific ID.
68
+ """
69
+ host: str
70
+ port: int
71
+ guid: str
72
+
73
+
74
+ class G90TargetedDiscovery(G90BaseCommand):
75
+ """
76
+ Discovers alarm panel devices with specific ID.
77
+ """
78
+ # pylint: disable=too-few-public-methods
79
+ def __init__(self, device_id: str, **kwargs: Any):
80
+ super().__init__(
81
+ # No actual command will be processed by base class, `NONE` is used
82
+ # for proper typing only
83
+ code=G90Commands.NONE, **kwargs
84
+ )
85
+ self._device_id = device_id
86
+ self._discovered_devices: List[G90DiscoveredDeviceTargeted] = []
87
+
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
+ # Implementation of datagram protocol,
99
+ # https://docs.python.org/3/library/asyncio-protocol.html#datagram-protocols
100
+ def datagram_received(self, data: bytes, addr: Tuple[str, int]) -> None:
101
+ """
102
+ Invoked when datagram is received.
103
+ """
104
+ try:
105
+ _LOGGER.debug('Received from %s:%s: %s', addr[0], addr[1], data)
106
+ try:
107
+ decoded = data.decode('utf-8')
108
+ except UnicodeDecodeError as exc:
109
+ raise G90Error(
110
+ 'Unable to decode discovery response from UTF-8'
111
+ ) from exc
112
+ if not decoded.endswith('\0'):
113
+ raise G90Error('Invalid discovery response')
114
+ host_info = G90TargetedDiscoveryInfo(*decoded[:-1].split(','))
115
+ if host_info.message != 'IWTAC_PROBE_DEVICE_ACK':
116
+ raise G90Error('Invalid discovery response')
117
+ res = G90DiscoveredDeviceTargeted(
118
+ host=addr[0],
119
+ port=addr[1],
120
+ guid=self._device_id,
121
+ **host_info._asdict()
122
+ )
123
+ _LOGGER.debug('Discovered device: %s', res)
124
+ self.add_device(res)
125
+ except Exception as exc: # pylint: disable=broad-except
126
+ _LOGGER.warning('Got exception, ignoring: %s', exc)
127
+
128
+ def error_received(self, exc: Exception) -> None:
129
+ """
130
+ Invoked when error is received.
131
+ """
132
+
133
+ def to_wire(self) -> bytes:
134
+ """
135
+ Converts the command to wire representation.
136
+ """
137
+ return bytes(f'IWTAC_PROBE_DEVICE,{self._device_id}\0', 'ascii')
138
+
139
+ async def process(self) -> G90TargetedDiscovery:
140
+ """
141
+ Initiates the device discovery process.
142
+ """
143
+ _LOGGER.debug('Attempting device discovery...')
144
+ transport, _ = await self._create_connection()
145
+ transport.sendto(self.to_wire())
146
+ await asyncio.sleep(self._timeout)
147
+ transport.close()
148
+ _LOGGER.debug('Discovered %s devices', len(self.devices))
149
+ return self
150
+
151
+ @property
152
+ def devices(self) -> List[G90DiscoveredDeviceTargeted]:
153
+ """
154
+ The list of discovered devices.
155
+ """
156
+ return self._discovered_devices
157
+
158
+ def add_device(self, value: G90DiscoveredDeviceTargeted) -> None:
159
+ """
160
+ Adds discovered device to the list.
161
+ """
162
+ self._discovered_devices.append(value)
@@ -0,0 +1,46 @@
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
+ Protocol entity for G90 alarm panel that provides checksums of different
23
+ on-device databases.
24
+ """
25
+ from __future__ import annotations
26
+ from typing import Any, Dict
27
+ from dataclasses import dataclass, asdict
28
+
29
+
30
+ @dataclass
31
+ class G90UserDataCRC:
32
+ """
33
+ Represents structure of GETUSERDATACRC command response.
34
+ """
35
+ sensor_list: str
36
+ device_list: str
37
+ history_list: str
38
+ scene_list: str
39
+ ifttt_list: str
40
+ fingerprint_list: str
41
+
42
+ def _asdict(self) -> Dict[str, Any]:
43
+ """
44
+ Returns the host information as dictionary.
45
+ """
46
+ return asdict(self)
File without changes