PyNetioConf 0.3.0b1__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.
- PyNetioConf/ESPCore/__init__.py +5 -0
- PyNetioConf/ESPCore/esp_200_device.py +16 -0
- PyNetioConf/ESPCore/esp_300_device.py +16 -0
- PyNetioConf/ESPCore/esp_400_device.py +16 -0
- PyNetioConf/ESPCore/esp_500_device.py +1826 -0
- PyNetioConf/ESPCore/esp_520_device.py +348 -0
- PyNetioConf/ESPCore/esp_5beta_device.py +263 -0
- PyNetioConf/ESPCore/esp_api.py +383 -0
- PyNetioConf/ESPCore/esp_device.py +1516 -0
- PyNetioConf/ESPCore/esp_device_init.py +328 -0
- PyNetioConf/ESPCore/ws_api.py +391 -0
- PyNetioConf/NetioManager.py +183 -0
- PyNetioConf/__init__.py +1 -0
- PyNetioConf/constants.py +12 -0
- PyNetioConf/exceptions.py +56 -0
- PyNetioConf/netio_device.py +1727 -0
- pynetioconf-0.3.0b1.dist-info/METADATA +173 -0
- pynetioconf-0.3.0b1.dist-info/RECORD +20 -0
- pynetioconf-0.3.0b1.dist-info/WHEEL +4 -0
- pynetioconf-0.3.0b1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
import ssl
|
|
4
|
+
from time import perf_counter, sleep
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
import websocket
|
|
9
|
+
from websocket import WebSocket
|
|
10
|
+
|
|
11
|
+
from ..exceptions import CommunicationError, InvalidParameterValueError
|
|
12
|
+
from ..netio_device import NETIODevice
|
|
13
|
+
from . import ws_api
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _setup_ssl(**kwargs: Any) -> tuple[ssl.SSLContext, dict[str, Any]]:
|
|
19
|
+
_ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) # Force TLS 1.2
|
|
20
|
+
|
|
21
|
+
DEFAULT_CIPHER_SUITES = [
|
|
22
|
+
# RSA
|
|
23
|
+
"TLS_RSA_WITH_AES_256_GCM_SHA384",
|
|
24
|
+
"TLS_RSA_WITH_AES_256_CCM",
|
|
25
|
+
"TLS_RSA_WITH_AES_128_GCM_SHA256",
|
|
26
|
+
"TLS_RSA_WITH_AES_128_CCM",
|
|
27
|
+
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
|
|
28
|
+
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
|
|
29
|
+
# ECC
|
|
30
|
+
"TLS_ECDHE_ECDSA_WITH_AES_256_SHA384",
|
|
31
|
+
"TLS_ECDHE_ECDSA_WITH_AES_128_SHA256",
|
|
32
|
+
"TLS_ECDHE_ECDSA_WITH_AES_256_SHA",
|
|
33
|
+
"TLS_ECDHE_ECDSA_WITH_AES_128_SHA",
|
|
34
|
+
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
|
|
35
|
+
"TLS_ECDHE_ECDSA_WITH_AES_256_CCM",
|
|
36
|
+
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
|
|
37
|
+
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
|
|
38
|
+
"TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8",
|
|
39
|
+
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
|
|
40
|
+
"TLS_ECDHE_ECDSA_WITH_AES_128_CCM",
|
|
41
|
+
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
|
|
42
|
+
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
|
|
43
|
+
"TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8",
|
|
44
|
+
"TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384",
|
|
45
|
+
"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384",
|
|
46
|
+
"TLS_ECDH_RSA_WITH_AES_256_CBC_SHA",
|
|
47
|
+
"TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384",
|
|
48
|
+
"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384",
|
|
49
|
+
"TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA",
|
|
50
|
+
"TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256",
|
|
51
|
+
"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256",
|
|
52
|
+
"TLS_ECDH_RSA_WITH_AES_128_CBC_SHA",
|
|
53
|
+
"TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256",
|
|
54
|
+
"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256",
|
|
55
|
+
"TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
cipher_suites = ":".join(DEFAULT_CIPHER_SUITES)
|
|
59
|
+
|
|
60
|
+
ssl_options: dict[str, Any] = {}
|
|
61
|
+
if kwargs.get("ssl_ciphers", None) is not None:
|
|
62
|
+
ssl_options["ciphers"] = kwargs.get("ssl_ciphers", cipher_suites)
|
|
63
|
+
_ssl_context.set_ciphers(kwargs.get("ssl_ciphers", cipher_suites))
|
|
64
|
+
else:
|
|
65
|
+
ssl_options["ciphers"] = cipher_suites
|
|
66
|
+
_ssl_context.set_ciphers(cipher_suites)
|
|
67
|
+
|
|
68
|
+
if kwargs.get("ssl_ca_cert_path", None) is not None:
|
|
69
|
+
ssl_options["ca_cert_path"] = kwargs.get("ssl_ca_cert_path")
|
|
70
|
+
_ssl_context.load_verify_locations(capath=kwargs.get("ssl_ca_cert_path"))
|
|
71
|
+
|
|
72
|
+
if kwargs.get("ssl_ecdh_curve", None) is not None:
|
|
73
|
+
ecdh_curve = kwargs.get("ssl_ecdh_curve")
|
|
74
|
+
if type(ecdh_curve) is str:
|
|
75
|
+
ssl_options["ecdh_curve"] = ecdh_curve
|
|
76
|
+
_ssl_context.set_ecdh_curve(ecdh_curve)
|
|
77
|
+
else:
|
|
78
|
+
raise TypeError("ssl_ecdh_curve must be a str")
|
|
79
|
+
|
|
80
|
+
if kwargs.get("ssl_ca_certs", None) is not None:
|
|
81
|
+
ssl_options["ca_certs"] = kwargs.get("ssl_ca_certs")
|
|
82
|
+
_ssl_context.load_verify_locations(cafile=kwargs.get("ssl_ca_certs"))
|
|
83
|
+
|
|
84
|
+
if kwargs.get("ssl_certfile", None) is not None:
|
|
85
|
+
ssl_options["certfile"] = kwargs.get("ssl_certfile")
|
|
86
|
+
|
|
87
|
+
if kwargs.get("ssl_keyfile", None) is not None:
|
|
88
|
+
ssl_options["keyfile"] = kwargs.get("ssl_keyfile")
|
|
89
|
+
if kwargs.get("ssl_password", None) is not None:
|
|
90
|
+
ssl_options["password"] = kwargs.get("ssl_password")
|
|
91
|
+
_ssl_context.load_cert_chain(
|
|
92
|
+
kwargs.get("ssl_certfile"),
|
|
93
|
+
kwargs.get("ssl_keyfile"),
|
|
94
|
+
kwargs.get("ssl_password"),
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
_ssl_context.load_cert_chain(
|
|
98
|
+
kwargs.get("ssl_certfile"), kwargs.get("ssl_keyfile")
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
_ssl_context.load_cert_chain(kwargs.get("ssl_certfile"))
|
|
102
|
+
|
|
103
|
+
if kwargs.get("ssl_check_hostname", None) is not None:
|
|
104
|
+
check_hostname = kwargs.get("ssl_check_hostname")
|
|
105
|
+
if type(check_hostname) is bool:
|
|
106
|
+
ssl_options["check_hostname"] = check_hostname
|
|
107
|
+
_ssl_context.check_hostname = check_hostname
|
|
108
|
+
else:
|
|
109
|
+
ssl_options["check_hostname"] = False
|
|
110
|
+
_ssl_context.check_hostname = False
|
|
111
|
+
|
|
112
|
+
if kwargs.get("ssl_cert_reqs", None) is not None:
|
|
113
|
+
cert_req = kwargs.get("ssl_cert_reqs")
|
|
114
|
+
if type(cert_req) is ssl.VerifyMode:
|
|
115
|
+
ssl_options["cert_reqs"] = cert_req
|
|
116
|
+
_ssl_context.verify_mode = cert_req
|
|
117
|
+
else:
|
|
118
|
+
ssl_options["cert_reqs"] = ssl.CERT_NONE
|
|
119
|
+
_ssl_context.verify_mode = ssl.CERT_NONE
|
|
120
|
+
|
|
121
|
+
ssl_options["context"] = kwargs.get("ssl_context", _ssl_context)
|
|
122
|
+
return (_ssl_context, ssl_options)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _try_connect_websocket(
|
|
126
|
+
host: str, use_https: bool, try_count: int, **kwargs: dict[str, Any]
|
|
127
|
+
) -> WebSocket:
|
|
128
|
+
ssl_options: dict[str, Any] | None = None
|
|
129
|
+
_ssl_context: ssl.SSLContext | None = None
|
|
130
|
+
if use_https:
|
|
131
|
+
_ssl_context, ssl_options = _setup_ssl(**kwargs)
|
|
132
|
+
|
|
133
|
+
logger.debug(f"Attempting websocket connection to {host}")
|
|
134
|
+
for attempt in range(try_count):
|
|
135
|
+
logger.debug(f"{host} websocket connection attempt {attempt + 1}/{try_count}")
|
|
136
|
+
try:
|
|
137
|
+
connection_dt_start = perf_counter()
|
|
138
|
+
if use_https:
|
|
139
|
+
ws = websocket.create_connection(
|
|
140
|
+
f"wss://{host}/emweb", sslopt=ssl_options, timeout=10
|
|
141
|
+
) # pyright: ignore[reportUnknownMemberType]
|
|
142
|
+
else:
|
|
143
|
+
ws = websocket.create_connection(f"ws://{host}/emweb", timeout=10) # pyright: ignore[reportUnknownMemberType]
|
|
144
|
+
|
|
145
|
+
if isinstance(ws, WebSocket):
|
|
146
|
+
logger.debug(f"Succesfully connected to {host}")
|
|
147
|
+
break
|
|
148
|
+
else:
|
|
149
|
+
elapsed_time = perf_counter() - connection_dt_start
|
|
150
|
+
if elapsed_time < 10:
|
|
151
|
+
logger.debug(
|
|
152
|
+
"Couldn't establish ws connection in time, waiting to reconnect."
|
|
153
|
+
)
|
|
154
|
+
sleep(10 - elapsed_time)
|
|
155
|
+
except Exception as e:
|
|
156
|
+
logger.debug(f"Connection to {host} failed on {attempt + 1}/{try_count}")
|
|
157
|
+
sleep(1)
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
if isinstance(ws, WebSocket):
|
|
161
|
+
logger.debug(f"Setting default timeout for {host}.")
|
|
162
|
+
ws.settimeout(60)
|
|
163
|
+
return ws
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def initialize_esp(
|
|
167
|
+
host: str,
|
|
168
|
+
username: str,
|
|
169
|
+
password: str,
|
|
170
|
+
keep_alive: bool = True,
|
|
171
|
+
netio_manager: Optional["NetioManager"] = None, # type: ignore # noqa
|
|
172
|
+
use_https: bool = False,
|
|
173
|
+
**kwargs: Any,
|
|
174
|
+
) -> NETIODevice:
|
|
175
|
+
if netio_manager is None:
|
|
176
|
+
raise InvalidParameterValueError
|
|
177
|
+
|
|
178
|
+
ws: websocket.WebSocket | None = None
|
|
179
|
+
version = 4 # TODO: Do version parsing here, not in NetioManager as this is ESP thing, not general NETIO thing
|
|
180
|
+
minor = 0
|
|
181
|
+
patch = 0
|
|
182
|
+
try:
|
|
183
|
+
try_count = 3 if not kwargs.get("ws_expected", False) else 15
|
|
184
|
+
|
|
185
|
+
ws = _try_connect_websocket(host, use_https, try_count, **kwargs)
|
|
186
|
+
ws_req_id = 0
|
|
187
|
+
|
|
188
|
+
hello_response = ws_api.device_init_request(ws, ws_req_id, "HELO", host)
|
|
189
|
+
ws_req_id += 1
|
|
190
|
+
ws_api.device_init_login(
|
|
191
|
+
ws,
|
|
192
|
+
ws_req_id,
|
|
193
|
+
hello_response["data"]["localTimestamp"],
|
|
194
|
+
hello_response["data"]["publicKey"],
|
|
195
|
+
username,
|
|
196
|
+
password,
|
|
197
|
+
host,
|
|
198
|
+
)
|
|
199
|
+
ws_req_id += 1
|
|
200
|
+
try:
|
|
201
|
+
version_str = hello_response["data"]["device"]["fwVersion"]
|
|
202
|
+
version_pattern = r"(\d+)\.(\d+)\.(\d+)"
|
|
203
|
+
match = re.search(version_pattern, version_str)
|
|
204
|
+
if match:
|
|
205
|
+
version, minor, patch = match.groups()
|
|
206
|
+
except KeyError:
|
|
207
|
+
logger.debug(
|
|
208
|
+
"Version not found in HELO message, fetching from system/info instead"
|
|
209
|
+
)
|
|
210
|
+
system_info = ws_api.device_init_request(
|
|
211
|
+
ws, ws_req_id, "SUBSCRIBE", host, "system/info"
|
|
212
|
+
)
|
|
213
|
+
ws_req_id += 2
|
|
214
|
+
version_str = system_info["data"]["fwVersion"]
|
|
215
|
+
version_pattern = r"(\d+)\.(\d+)\.(\d+)"
|
|
216
|
+
match = re.search(version_pattern, version_str)
|
|
217
|
+
if match:
|
|
218
|
+
version, minor, patch = match.groups()
|
|
219
|
+
else:
|
|
220
|
+
logger.warn(
|
|
221
|
+
f"Websocket on {host} is connected but version couldn't be verified, defaulting to 5beta firmware"
|
|
222
|
+
)
|
|
223
|
+
version, minor, patch = 5, 0, 0
|
|
224
|
+
except:
|
|
225
|
+
ws_req_id = 0
|
|
226
|
+
ws = None
|
|
227
|
+
|
|
228
|
+
if int(version) == 2:
|
|
229
|
+
from .esp_200_device import ESP200Device
|
|
230
|
+
|
|
231
|
+
netio_device = ESP200Device(
|
|
232
|
+
host,
|
|
233
|
+
username,
|
|
234
|
+
password,
|
|
235
|
+
"",
|
|
236
|
+
"",
|
|
237
|
+
keep_alive,
|
|
238
|
+
netio_manager, # pyright: ignore[reportUnknownArgumentType]
|
|
239
|
+
use_https,
|
|
240
|
+
)
|
|
241
|
+
return netio_device
|
|
242
|
+
|
|
243
|
+
if int(version) == 3:
|
|
244
|
+
from .esp_300_device import ESP300Device
|
|
245
|
+
|
|
246
|
+
netio_device = ESP300Device(
|
|
247
|
+
host,
|
|
248
|
+
username,
|
|
249
|
+
password,
|
|
250
|
+
"",
|
|
251
|
+
"",
|
|
252
|
+
keep_alive,
|
|
253
|
+
netio_manager, # pyright: ignore[reportUnknownArgumentType]
|
|
254
|
+
use_https,
|
|
255
|
+
)
|
|
256
|
+
return netio_device
|
|
257
|
+
|
|
258
|
+
if int(version) == 4:
|
|
259
|
+
from .esp_400_device import ESP400Device
|
|
260
|
+
|
|
261
|
+
netio_device = ESP400Device(
|
|
262
|
+
host,
|
|
263
|
+
username,
|
|
264
|
+
password,
|
|
265
|
+
"",
|
|
266
|
+
"",
|
|
267
|
+
keep_alive,
|
|
268
|
+
netio_manager, # pyright: ignore[reportUnknownArgumentType]
|
|
269
|
+
use_https,
|
|
270
|
+
)
|
|
271
|
+
return netio_device
|
|
272
|
+
|
|
273
|
+
if int(version) == 5:
|
|
274
|
+
if int(minor) >= 2:
|
|
275
|
+
from .esp_520_device import ESP520Device
|
|
276
|
+
|
|
277
|
+
netio_device = ESP520Device(
|
|
278
|
+
host,
|
|
279
|
+
username,
|
|
280
|
+
password,
|
|
281
|
+
"",
|
|
282
|
+
"",
|
|
283
|
+
keep_alive,
|
|
284
|
+
netio_manager, # pyright: ignore[reportUnknownArgumentType]
|
|
285
|
+
use_https,
|
|
286
|
+
ws_connection=ws,
|
|
287
|
+
ws_req_id=ws_req_id,
|
|
288
|
+
is_ws_auth=True if ws is not None else False,
|
|
289
|
+
ws_helo_data=hello_response,
|
|
290
|
+
)
|
|
291
|
+
return netio_device
|
|
292
|
+
elif int(minor) == 1:
|
|
293
|
+
from .esp_500_device import ESP500Device
|
|
294
|
+
|
|
295
|
+
netio_device = ESP500Device(
|
|
296
|
+
host,
|
|
297
|
+
username,
|
|
298
|
+
password,
|
|
299
|
+
"",
|
|
300
|
+
"",
|
|
301
|
+
keep_alive,
|
|
302
|
+
netio_manager, # pyright: ignore[reportUnknownArgumentType]
|
|
303
|
+
use_https,
|
|
304
|
+
ws_connection=ws,
|
|
305
|
+
ws_req_id=ws_req_id,
|
|
306
|
+
is_ws_auth=True if ws is not None else False,
|
|
307
|
+
ws_helo_data=hello_response,
|
|
308
|
+
)
|
|
309
|
+
return netio_device
|
|
310
|
+
else:
|
|
311
|
+
from .esp_5beta_device import ESP5BetaDevice
|
|
312
|
+
|
|
313
|
+
ws = None
|
|
314
|
+
netio_device = ESP5BetaDevice(
|
|
315
|
+
host,
|
|
316
|
+
username,
|
|
317
|
+
password,
|
|
318
|
+
"",
|
|
319
|
+
"",
|
|
320
|
+
keep_alive,
|
|
321
|
+
netio_manager, # pyright: ignore[reportUnknownArgumentType]
|
|
322
|
+
use_https,
|
|
323
|
+
)
|
|
324
|
+
return netio_device
|
|
325
|
+
|
|
326
|
+
raise InvalidParameterValueError(
|
|
327
|
+
f"There is no firmware with major version {version} supported"
|
|
328
|
+
)
|