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.
- pyg90alarm/__init__.py +84 -0
- pyg90alarm/alarm.py +1274 -0
- pyg90alarm/callback.py +146 -0
- pyg90alarm/cloud/__init__.py +31 -0
- pyg90alarm/cloud/const.py +56 -0
- pyg90alarm/cloud/messages.py +593 -0
- pyg90alarm/cloud/notifications.py +410 -0
- pyg90alarm/cloud/protocol.py +518 -0
- pyg90alarm/const.py +273 -0
- pyg90alarm/definitions/__init__.py +3 -0
- pyg90alarm/definitions/base.py +247 -0
- pyg90alarm/definitions/devices.py +366 -0
- pyg90alarm/definitions/sensors.py +843 -0
- pyg90alarm/entities/__init__.py +3 -0
- pyg90alarm/entities/base_entity.py +93 -0
- pyg90alarm/entities/base_list.py +268 -0
- pyg90alarm/entities/device.py +97 -0
- pyg90alarm/entities/device_list.py +156 -0
- pyg90alarm/entities/sensor.py +891 -0
- pyg90alarm/entities/sensor_list.py +183 -0
- pyg90alarm/exceptions.py +63 -0
- pyg90alarm/local/__init__.py +0 -0
- pyg90alarm/local/base_cmd.py +293 -0
- pyg90alarm/local/config.py +157 -0
- pyg90alarm/local/discovery.py +103 -0
- pyg90alarm/local/history.py +272 -0
- pyg90alarm/local/host_info.py +89 -0
- pyg90alarm/local/host_status.py +52 -0
- pyg90alarm/local/notifications.py +117 -0
- pyg90alarm/local/paginated_cmd.py +132 -0
- pyg90alarm/local/paginated_result.py +135 -0
- pyg90alarm/local/targeted_discovery.py +162 -0
- pyg90alarm/local/user_data_crc.py +46 -0
- pyg90alarm/notifications/__init__.py +0 -0
- pyg90alarm/notifications/base.py +481 -0
- pyg90alarm/notifications/protocol.py +127 -0
- pyg90alarm/py.typed +0 -0
- pyg90alarm-2.3.0.dist-info/METADATA +277 -0
- pyg90alarm-2.3.0.dist-info/RECORD +42 -0
- pyg90alarm-2.3.0.dist-info/WHEEL +5 -0
- pyg90alarm-2.3.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
))
|