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,103 @@
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 panels.
23
+ """
24
+ from __future__ import annotations
25
+ import asyncio
26
+ from typing import Any, List, Tuple
27
+ from dataclasses import dataclass
28
+ import logging
29
+
30
+ from .base_cmd import G90BaseCommand
31
+ from .host_info import G90HostInfo
32
+ from ..const import G90Commands
33
+
34
+ _LOGGER = logging.getLogger(__name__)
35
+
36
+
37
+ @dataclass
38
+ class G90DiscoveredDevice(G90HostInfo):
39
+ """
40
+ Represents discovered alarm panel.
41
+ """
42
+ host: str
43
+ port: int
44
+ guid: str
45
+
46
+
47
+ class G90Discovery(G90BaseCommand):
48
+ """
49
+ Discovers alarm panels.
50
+ """
51
+ # pylint: disable=too-few-public-methods
52
+ def __init__(self, timeout: float = 10, **kwargs: Any):
53
+ # pylint: disable=too-many-arguments
54
+ super().__init__(code=G90Commands.GETHOSTINFO, timeout=timeout,
55
+ **kwargs)
56
+ self._discovered_devices: List[G90DiscoveredDevice] = []
57
+
58
+ # Implementation of datagram protocol,
59
+ # https://docs.python.org/3/library/asyncio-protocol.html#datagram-protocols
60
+ def datagram_received(self, data: bytes, addr: Tuple[str, int]) -> None:
61
+ """
62
+ Invoked when some data is received.
63
+ """
64
+ try:
65
+ ret = self.from_wire(data)
66
+ host_info = G90HostInfo(*ret)
67
+ _LOGGER.debug('Received from %s:%s: %s', addr[0], addr[1], ret)
68
+ res = G90DiscoveredDevice(
69
+ host=addr[0],
70
+ port=addr[1],
71
+ guid=host_info.host_guid,
72
+ **host_info._asdict()
73
+ )
74
+ _LOGGER.debug('Discovered device: %s', res)
75
+ self.add_device(res)
76
+
77
+ except Exception as exc: # pylint: disable=broad-except
78
+ _LOGGER.warning('Got exception, ignoring: %s', exc)
79
+
80
+ async def process(self) -> G90Discovery:
81
+ """
82
+ Initiates device discovery.
83
+ """
84
+ _LOGGER.debug('Attempting device discovery...')
85
+ transport, _ = await self._create_connection()
86
+ transport.sendto(self.to_wire())
87
+ await asyncio.sleep(self._timeout)
88
+ transport.close()
89
+ _LOGGER.debug('Discovered %s devices', len(self.devices))
90
+ return self
91
+
92
+ @property
93
+ def devices(self) -> List[G90DiscoveredDevice]:
94
+ """
95
+ The list of discovered devices.
96
+ """
97
+ return self._discovered_devices
98
+
99
+ def add_device(self, value: G90DiscoveredDevice) -> None:
100
+ """
101
+ Adds discovered device to the list.
102
+ """
103
+ self._discovered_devices.append(value)
@@ -0,0 +1,272 @@
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
+ History protocol entity.
23
+ """
24
+ import logging
25
+
26
+ from typing import Any, Optional, Dict
27
+ from dataclasses import dataclass
28
+ from datetime import datetime, timezone
29
+ from ..const import (
30
+ G90AlertTypes,
31
+ G90AlertSources,
32
+ G90AlertStates,
33
+ G90AlertStateChangeTypes,
34
+ G90HistoryStates,
35
+ G90RemoteButtonStates,
36
+ )
37
+ from ..notifications.base import G90DeviceAlert
38
+
39
+ _LOGGER = logging.getLogger(__name__)
40
+
41
+
42
+ # The state of the incoming history entries are mixed of `G90AlertStates`,
43
+ # `G90AlertStateChangeTypes` and `G90RemoteButtonStates`, depending on entry
44
+ # type - hence separate dictionaries, since enums used for keys have
45
+ # conflicting values
46
+ states_mapping_alerts = {
47
+ G90AlertStates.DOOR_CLOSE:
48
+ G90HistoryStates.DOOR_CLOSE,
49
+ G90AlertStates.DOOR_OPEN:
50
+ G90HistoryStates.DOOR_OPEN,
51
+ G90AlertStates.TAMPER:
52
+ G90HistoryStates.TAMPER,
53
+ G90AlertStates.LOW_BATTERY:
54
+ G90HistoryStates.LOW_BATTERY,
55
+ G90AlertStates.ALARM:
56
+ G90HistoryStates.ALARM,
57
+ }
58
+
59
+ states_mapping_state_changes = {
60
+ G90AlertStateChangeTypes.AC_POWER_FAILURE:
61
+ G90HistoryStates.AC_POWER_FAILURE,
62
+ G90AlertStateChangeTypes.AC_POWER_RECOVER:
63
+ G90HistoryStates.AC_POWER_RECOVER,
64
+ G90AlertStateChangeTypes.DISARM:
65
+ G90HistoryStates.DISARM,
66
+ G90AlertStateChangeTypes.ARM_AWAY:
67
+ G90HistoryStates.ARM_AWAY,
68
+ G90AlertStateChangeTypes.ARM_HOME:
69
+ G90HistoryStates.ARM_HOME,
70
+ G90AlertStateChangeTypes.LOW_BATTERY:
71
+ G90HistoryStates.LOW_BATTERY,
72
+ G90AlertStateChangeTypes.WIFI_CONNECTED:
73
+ G90HistoryStates.WIFI_CONNECTED,
74
+ G90AlertStateChangeTypes.WIFI_DISCONNECTED:
75
+ G90HistoryStates.WIFI_DISCONNECTED,
76
+ }
77
+
78
+ states_mapping_remote_buttons = {
79
+ G90RemoteButtonStates.ARM_AWAY:
80
+ G90HistoryStates.REMOTE_BUTTON_ARM_AWAY,
81
+ G90RemoteButtonStates.ARM_HOME:
82
+ G90HistoryStates.REMOTE_BUTTON_ARM_HOME,
83
+ G90RemoteButtonStates.DISARM:
84
+ G90HistoryStates.REMOTE_BUTTON_DISARM,
85
+ G90RemoteButtonStates.SOS:
86
+ G90HistoryStates.REMOTE_BUTTON_SOS,
87
+ }
88
+
89
+
90
+ @dataclass
91
+ class ProtocolData:
92
+ """
93
+ Class representing the data incoming from the device
94
+
95
+ :meta private:
96
+ """
97
+ type: G90AlertTypes
98
+ event_id: G90AlertStateChangeTypes
99
+ source: G90AlertSources
100
+ state: int
101
+ sensor_name: str
102
+ unix_time: int
103
+ other: str
104
+
105
+
106
+ class G90History:
107
+ """
108
+ Represents a history entry from the alarm panel.
109
+ """
110
+ def __init__(self, *args: Any, **kwargs: Any):
111
+ self._raw_data = args
112
+ self._protocol_data = ProtocolData(*args, **kwargs)
113
+
114
+ @property
115
+ def datetime(self) -> datetime:
116
+ """
117
+ Date/time of the history entry.
118
+ """
119
+ return datetime.fromtimestamp(
120
+ self._protocol_data.unix_time, tz=timezone.utc
121
+ )
122
+
123
+ @property
124
+ def type(self) -> Optional[G90AlertTypes]:
125
+ """
126
+ Type of the history entry.
127
+ """
128
+ try:
129
+ return G90AlertTypes(self._protocol_data.type)
130
+ except (ValueError, KeyError):
131
+ _LOGGER.warning(
132
+ "Can't interpret '%s' as alert type (decoded protocol"
133
+ " data '%s', raw data '%s')",
134
+ self._protocol_data.type, self._protocol_data, self._raw_data
135
+ )
136
+ return None
137
+
138
+ @property
139
+ def state(self) -> Optional[G90HistoryStates]:
140
+ """
141
+ State for the history entry.
142
+ """
143
+ # No meaningful state for SOS alerts initiated by the panel itself
144
+ # (host)
145
+ if self.type == G90AlertTypes.HOST_SOS:
146
+ return None
147
+
148
+ try:
149
+ # State of the remote indicate which button has been pressed
150
+ if (
151
+ self.type in [
152
+ G90AlertTypes.SENSOR_ACTIVITY, G90AlertTypes.ALARM
153
+ ] and self.source == G90AlertSources.REMOTE
154
+ ):
155
+ return states_mapping_remote_buttons[
156
+ G90RemoteButtonStates(self._protocol_data.state)
157
+ ]
158
+
159
+ # Door open/close or alert types, mapped against `G90AlertStates`
160
+ # using `state` incoming field
161
+ if self.type in [
162
+ G90AlertTypes.SENSOR_ACTIVITY, G90AlertTypes.ALARM
163
+ ]:
164
+ return G90HistoryStates(
165
+ states_mapping_alerts[
166
+ G90AlertStates(self._protocol_data.state)
167
+ ]
168
+ )
169
+ except (ValueError, KeyError):
170
+ _LOGGER.warning(
171
+ "Can't interpret '%s' as alert state (decoded protocol"
172
+ " data '%s', raw data '%s')",
173
+ self._protocol_data.state, self._protocol_data, self._raw_data
174
+ )
175
+ return None
176
+
177
+ try:
178
+ # Other types are mapped against `G90AlertStateChangeTypes`
179
+ return G90HistoryStates(
180
+ states_mapping_state_changes[
181
+ G90AlertStateChangeTypes(self._protocol_data.event_id)
182
+ ]
183
+ )
184
+ except (ValueError, KeyError):
185
+ _LOGGER.warning(
186
+ "Can't interpret '%s' as state change (decoded protocol"
187
+ " data '%s', raw data '%s')",
188
+ self._protocol_data.event_id, self._protocol_data,
189
+ self._raw_data
190
+ )
191
+ return None
192
+
193
+ @property
194
+ def source(self) -> Optional[G90AlertSources]:
195
+ """
196
+ Source of the history entry.
197
+ """
198
+ try:
199
+ # Device state changes, open/close or alarm events are mapped
200
+ # against `G90AlertSources` using `source` incoming field
201
+ if self.type in [
202
+ G90AlertTypes.STATE_CHANGE, G90AlertTypes.SENSOR_ACTIVITY,
203
+ G90AlertTypes.ALARM
204
+ ]:
205
+ return G90AlertSources(self._protocol_data.source)
206
+ except (ValueError, KeyError):
207
+ _LOGGER.warning(
208
+ "Can't interpret '%s' as alert source (decoded protocol"
209
+ " data '%s', raw data '%s')",
210
+ self._protocol_data.source, self._protocol_data, self._raw_data
211
+ )
212
+ return None
213
+
214
+ # Other sources are assumed to be initiated by device itself
215
+ return G90AlertSources.DEVICE
216
+
217
+ @property
218
+ def sensor_name(self) -> Optional[str]:
219
+ """
220
+ Name of the sensor related to the history entry, might be empty if none
221
+ associated.
222
+ """
223
+ return self._protocol_data.sensor_name or None
224
+
225
+ @property
226
+ def sensor_idx(self) -> Optional[int]:
227
+ """
228
+ ID of the sensor related to the history entry, might be empty if none
229
+ associated.
230
+ """
231
+ # Sensor ID will only be available if entry source is a sensor
232
+ if self.source == G90AlertSources.SENSOR:
233
+ return self._protocol_data.event_id
234
+
235
+ return None
236
+
237
+ def as_device_alert(self) -> G90DeviceAlert:
238
+ """
239
+ Returns the history entry represented as device alert structure,
240
+ suitable for :meth:`G90DeviceNotifications._handle_alert`.
241
+ """
242
+
243
+ return G90DeviceAlert(
244
+ type=self._protocol_data.type,
245
+ event_id=self._protocol_data.event_id,
246
+ source=self._protocol_data.source,
247
+ state=self._protocol_data.state,
248
+ zone_name=self._protocol_data.sensor_name,
249
+ device_id='',
250
+ unix_time=self._protocol_data.unix_time,
251
+ resv4=0,
252
+ other=self._protocol_data.other
253
+ )
254
+
255
+ def _asdict(self) -> Dict[str, Any]:
256
+ """
257
+ Returns the history entry as dictionary.
258
+ """
259
+ return {
260
+ 'type': self.type,
261
+ 'source': self.source,
262
+ 'state': self.state,
263
+ 'sensor_name': self.sensor_name,
264
+ 'sensor_idx': self.sensor_idx,
265
+ 'datetime': self.datetime,
266
+ }
267
+
268
+ def __repr__(self) -> str:
269
+ """
270
+ Textural representation of the history entry.
271
+ """
272
+ return super().__repr__() + f'({repr(self._asdict())})'
@@ -0,0 +1,89 @@
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 information.
23
+ """
24
+ from __future__ import annotations
25
+ from typing import Any, Dict
26
+ from dataclasses import dataclass, asdict
27
+ from enum import IntEnum
28
+
29
+
30
+ class G90HostInfoGsmStatus(IntEnum):
31
+ """
32
+ Defines possible values of GSM module status.
33
+ """
34
+ POWERED_OFF = 0
35
+ SIM_ABSENT = 1
36
+ NO_SIGNAL = 2
37
+ OPERATIONAL = 3
38
+
39
+
40
+ class G90HostInfoWifiStatus(IntEnum):
41
+ """
42
+ Defines possible values of Wifi module status.
43
+ """
44
+ POWERED_OFF = 0
45
+ NOT_CONNECTED = 1
46
+ NO_SIGNAL = 2
47
+ OPERATIONAL = 3
48
+
49
+
50
+ @dataclass
51
+ class G90HostInfo: # pylint: disable=too-many-instance-attributes
52
+ """
53
+ Interprets data fields of GETHOSTINFO command.
54
+ """
55
+ host_guid: str
56
+ product_name: str
57
+ wifi_protocol_version: str
58
+ cloud_protocol_version: str
59
+ mcu_hw_version: str
60
+ wifi_hw_version: str
61
+ gsm_status_data: int
62
+ wifi_status_data: int
63
+ reserved1: int
64
+ reserved2: int
65
+ band_frequency: str
66
+ gsm_signal_level: int
67
+ wifi_signal_level: int
68
+
69
+ @property
70
+ def gsm_status(self) -> G90HostInfoGsmStatus:
71
+ """
72
+ Translates the GSM module status received from the device into
73
+ corresponding enum.
74
+ """
75
+ return G90HostInfoGsmStatus(self.gsm_status_data)
76
+
77
+ @property
78
+ def wifi_status(self) -> G90HostInfoWifiStatus:
79
+ """
80
+ Translates the Wifi module status received from the device into
81
+ corresponding enum.
82
+ """
83
+ return G90HostInfoWifiStatus(self.wifi_status_data)
84
+
85
+ def _asdict(self) -> Dict[str, Any]:
86
+ """
87
+ Returns the host information as dictionary.
88
+ """
89
+ return asdict(self)
@@ -0,0 +1,52 @@
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 status.
23
+ """
24
+ from __future__ import annotations
25
+ from typing import Any, Dict
26
+ from dataclasses import dataclass, asdict
27
+ from ..const import G90ArmDisarmTypes
28
+
29
+
30
+ @dataclass
31
+ class G90HostStatus:
32
+ """
33
+ Interprets data fields of GETHOSTSTATUS command.
34
+ """
35
+ host_status_data: int
36
+ host_phone_number: str
37
+ product_name: str
38
+ mcu_hw_version: str
39
+ wifi_hw_version: str
40
+
41
+ @property
42
+ def host_status(self) -> G90ArmDisarmTypes:
43
+ """
44
+ Translates host status data to G90ArmDisarmTypes.
45
+ """
46
+ return G90ArmDisarmTypes(self.host_status_data)
47
+
48
+ def _asdict(self) -> Dict[str, Any]:
49
+ """
50
+ Returns the host information as dictionary.
51
+ """
52
+ return asdict(self)
@@ -0,0 +1,117 @@
1
+ # Copyright (c) 2021 Ilia Sotnikov
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ # SOFTWARE.
20
+
21
+ """
22
+ Implements support for notifications/alerts sent by G90 alarm panel.
23
+ """
24
+ import logging
25
+ from typing import (
26
+ Optional, Tuple, Callable
27
+ )
28
+ import asyncio
29
+ from asyncio.transports import BaseTransport
30
+ from asyncio.protocols import DatagramProtocol
31
+
32
+ from ..notifications.base import G90NotificationsBase
33
+ from ..notifications.protocol import G90NotificationProtocol
34
+
35
+ _LOGGER = logging.getLogger(__name__)
36
+
37
+
38
+ class G90LocalNotifications(G90NotificationsBase, DatagramProtocol):
39
+ """
40
+ Implements support for notifications/alerts sent by alarm panel.
41
+
42
+ There is a basic check to ensure only notifications/alerts from the correct
43
+ device are processed - the check uses the host and port of the device, and
44
+ the device ID (GUID) that is set by the ancestor class that implements the
45
+ commands (e.g. :class:`.G90Alarm`). The latter to work correctly needs a
46
+ command to be performed first, one that fetches device GUID and then stores
47
+ it using :attr:`.device_id` (e.g. :meth:`.G90Alarm.get_host_info`).
48
+
49
+ :param protocol_factory: A callable that returns a new instance of the
50
+ :class:`.G90NotificationProtocol` class.
51
+ :param port: The port on which the device is listening for notifications.
52
+ :param host: The host on which the device is listening for notifications.
53
+ :param local_port: The port on which the local host is listening for
54
+ notifications.
55
+ :param local_host: The host on which the local host is listening for
56
+ notifications.
57
+ """
58
+ # pylint:disable=too-many-positional-arguments,too-many-arguments
59
+ def __init__(
60
+ self, protocol_factory: Callable[[], G90NotificationProtocol],
61
+ port: int, host: str, local_port: int, local_host: str,
62
+ ):
63
+ super().__init__(protocol_factory)
64
+
65
+ self._host = host
66
+ self._port = port
67
+ self._notifications_local_host = local_host
68
+ self._notifications_local_port = local_port
69
+
70
+ # Implementation of datagram protocol,
71
+ # https://docs.python.org/3/library/asyncio-protocol.html#datagram-protocols
72
+ def connection_made(self, transport: BaseTransport) -> None:
73
+ """
74
+ Invoked when connection from the device is made.
75
+ """
76
+
77
+ def connection_lost(self, exc: Optional[Exception]) -> None:
78
+ """
79
+ Same but when the connection is lost.
80
+ """
81
+
82
+ def datagram_received(
83
+ self, data: bytes, addr: Tuple[str, int]
84
+ ) -> None:
85
+ """
86
+ Invoked when datagram is received from the device.
87
+ """
88
+ if self._host and self._host != addr[0]:
89
+ _LOGGER.error(
90
+ "Received notification/alert from wrong host '%s',"
91
+ " expected from '%s'",
92
+ addr[0], self._host
93
+ )
94
+ return
95
+
96
+ self.set_last_device_packet_time()
97
+
98
+ _LOGGER.debug('Received device message from %s:%s: %s',
99
+ addr[0], addr[1], data)
100
+
101
+ self.handle(data)
102
+
103
+ async def listen(self) -> None:
104
+ """
105
+ Listens for notifications/alerts from the device.
106
+ """
107
+ loop = asyncio.get_running_loop()
108
+
109
+ _LOGGER.debug('Creating UDP endpoint for %s:%s',
110
+ self._notifications_local_host,
111
+ self._notifications_local_port)
112
+ (self._transport,
113
+ _protocol) = await loop.create_datagram_endpoint(
114
+ lambda: self,
115
+ local_addr=(
116
+ self._notifications_local_host, self._notifications_local_port
117
+ ))