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,348 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Implementation specifics for ESP devices with the firmware 5.2.x.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
import io
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import ssl
|
|
10
|
+
import sys
|
|
11
|
+
from collections import deque
|
|
12
|
+
from io import BytesIO
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from time import sleep, time
|
|
15
|
+
|
|
16
|
+
from ..constants import (
|
|
17
|
+
DEFAULT_KEEP_ALIVE_QUEUE_LEN,
|
|
18
|
+
DEFAULT_REQUEST_QUEUE_LEN,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if sys.version_info >= (3, 12):
|
|
22
|
+
from typing import IO, Any, AnyStr, Dict, List, Tuple, override
|
|
23
|
+
else:
|
|
24
|
+
from typing import IO, Any, AnyStr, Dict, List, Tuple
|
|
25
|
+
|
|
26
|
+
from typing_extensions import override
|
|
27
|
+
|
|
28
|
+
import logging
|
|
29
|
+
import threading
|
|
30
|
+
|
|
31
|
+
import websocket
|
|
32
|
+
from websocket import WebSocket
|
|
33
|
+
|
|
34
|
+
from .. import NetioManager
|
|
35
|
+
from ..exceptions import CommunicationError, ElementNotFound
|
|
36
|
+
from ..netio_device import NETIODevice
|
|
37
|
+
from . import esp_api, ws_api
|
|
38
|
+
from .esp_500_device import ESP500Device
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ESP520Device(ESP500Device):
|
|
42
|
+
"""
|
|
43
|
+
A class to control ESP devices with the firmware 5.2.x.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
host: str,
|
|
49
|
+
username: str,
|
|
50
|
+
password: str,
|
|
51
|
+
sn_number: str,
|
|
52
|
+
hostname: str,
|
|
53
|
+
keep_alive: bool = True,
|
|
54
|
+
netio_manager: NetioManager | None = None,
|
|
55
|
+
use_https: bool = False,
|
|
56
|
+
**kwargs: Any,
|
|
57
|
+
):
|
|
58
|
+
"""
|
|
59
|
+
The 5.x.x versions of Netio firmware have been severely redesigned which includes the entirety of the API that is used for communication with the device therefore this init is completely separated from the base classes and does not call super().__init__ but instead makes a new class from scratch. If the base NETIODevice class gets updated this has to be done accordingly here.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
host : str
|
|
64
|
+
|
|
65
|
+
username : str
|
|
66
|
+
|
|
67
|
+
password : str
|
|
68
|
+
|
|
69
|
+
sn_number : str
|
|
70
|
+
|
|
71
|
+
hostname : str
|
|
72
|
+
|
|
73
|
+
: Any
|
|
74
|
+
|
|
75
|
+
keep_alive : bool
|
|
76
|
+
|
|
77
|
+
netio_manager : NetioManager | None
|
|
78
|
+
|
|
79
|
+
use_https : bool
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
self.host = host
|
|
84
|
+
self.username = username
|
|
85
|
+
self.password = password
|
|
86
|
+
self.sn_number = sn_number
|
|
87
|
+
self.hostname = hostname
|
|
88
|
+
self.session_id = ""
|
|
89
|
+
self.supported_features: dict[str, Any] = dict()
|
|
90
|
+
self.output_count = 4
|
|
91
|
+
self.user_permissions: list[str] = list()
|
|
92
|
+
self._keep_alive_flag = keep_alive
|
|
93
|
+
self.ws_req_id = kwargs.get("ws_req_id", 0)
|
|
94
|
+
self.use_https = use_https
|
|
95
|
+
if use_https:
|
|
96
|
+
from urllib3 import disable_warnings
|
|
97
|
+
from urllib3.exceptions import InsecureRequestWarning
|
|
98
|
+
|
|
99
|
+
disable_warnings(category=InsecureRequestWarning)
|
|
100
|
+
|
|
101
|
+
self._request_queue = deque(maxlen=DEFAULT_REQUEST_QUEUE_LEN)
|
|
102
|
+
self._pong_queue = deque(maxlen=DEFAULT_KEEP_ALIVE_QUEUE_LEN)
|
|
103
|
+
|
|
104
|
+
# Init from base ESP Device
|
|
105
|
+
self.ws: WebSocket | None = kwargs.get("ws_connection", None)
|
|
106
|
+
self.logger = logging.getLogger(__name__)
|
|
107
|
+
if kwargs.get("is_ws_auth", False):
|
|
108
|
+
self.session_id = "TODO AUTH"
|
|
109
|
+
else:
|
|
110
|
+
self.session_id = self.login(username, password)
|
|
111
|
+
# self.supported_features = self.get_features()
|
|
112
|
+
# self.output_count: int = self.supported_features["outputCount"]
|
|
113
|
+
# self.user_permissions = self.get_current_user()["privileges"]
|
|
114
|
+
if keep_alive:
|
|
115
|
+
self._ka_thread = threading.Timer(120, self._keep_alive)
|
|
116
|
+
self._ka_thread.daemon = True
|
|
117
|
+
self._ka_thread.start()
|
|
118
|
+
atexit.register(self._cleanup)
|
|
119
|
+
|
|
120
|
+
# New Init
|
|
121
|
+
self.ssl_options: dict[str, Any] | None = kwargs.get("ssl_options", dict())
|
|
122
|
+
self.logger = logging.getLogger(self.__class__.__name__)
|
|
123
|
+
self.netio_manager = netio_manager
|
|
124
|
+
# self.fw_version = self.get_version()
|
|
125
|
+
|
|
126
|
+
_system_info = self.get_system_info()
|
|
127
|
+
self.output_count = _system_info["outputCount"]
|
|
128
|
+
self.input_count = _system_info["inputCount"]
|
|
129
|
+
self.supported_features["wifi"] = _system_info["wifiSupport"]
|
|
130
|
+
self.supported_features["eth"] = _system_info["ethSupport"]
|
|
131
|
+
|
|
132
|
+
# TODO: Handle repeated code for uploading files using chunking
|
|
133
|
+
@override
|
|
134
|
+
def upload_mqtt_client_key(self, key: str) -> None:
|
|
135
|
+
import base64
|
|
136
|
+
|
|
137
|
+
ws_type = "SET"
|
|
138
|
+
ws_topic = "protocols/mqtt/clientkeyupload"
|
|
139
|
+
ws_data: dict[str, Any] = {}
|
|
140
|
+
|
|
141
|
+
self.logger.debug(f"Uploading MQTT client key to device {self.host}.")
|
|
142
|
+
upload_id = ws_api.send_request(self, ws_type, ws_topic, ws_data)["data"][
|
|
143
|
+
"uploadId"
|
|
144
|
+
]
|
|
145
|
+
original_bytes = key.encode("utf-8")
|
|
146
|
+
base64_string = base64.b64encode(original_bytes).decode("utf-8")
|
|
147
|
+
ws_api.chunk_file_upload(self, base64_string, 0, len(key), len(key), upload_id)
|
|
148
|
+
|
|
149
|
+
@override
|
|
150
|
+
def upload_mqtt_ca_certificate(self, ca: str) -> None:
|
|
151
|
+
import base64
|
|
152
|
+
|
|
153
|
+
ws_type = "SET"
|
|
154
|
+
ws_topic = "protocols/mqtt/cacertupload"
|
|
155
|
+
ws_data: dict[str, Any] = {}
|
|
156
|
+
|
|
157
|
+
self.logger.debug(f"Uploading MQTT CA certificate to device {self.host}.")
|
|
158
|
+
upload_id = ws_api.send_request(self, ws_type, ws_topic, ws_data)["data"][
|
|
159
|
+
"uploadId"
|
|
160
|
+
]
|
|
161
|
+
original_bytes = ca.encode("utf-8")
|
|
162
|
+
base64_string = base64.b64encode(original_bytes).decode("utf-8")
|
|
163
|
+
ws_api.chunk_file_upload(self, base64_string, 0, len(ca), len(ca), upload_id)
|
|
164
|
+
|
|
165
|
+
@override
|
|
166
|
+
def upload_mqtt_client_certificate(self, cert: str) -> None:
|
|
167
|
+
import base64
|
|
168
|
+
|
|
169
|
+
ws_type = "SET"
|
|
170
|
+
ws_topic = "protocols/mqtt/clientcertupload"
|
|
171
|
+
ws_data: dict[str, Any] = {}
|
|
172
|
+
|
|
173
|
+
self.logger.debug(f"Uploading MQTT client certificate to device {self.host}.")
|
|
174
|
+
upload_id = ws_api.send_request(self, ws_type, ws_topic, ws_data)["data"][
|
|
175
|
+
"uploadId"
|
|
176
|
+
]
|
|
177
|
+
original_bytes = cert.encode("utf-8")
|
|
178
|
+
base64_string = base64.b64encode(original_bytes).decode("utf-8")
|
|
179
|
+
ws_api.chunk_file_upload(
|
|
180
|
+
self, base64_string, 0, len(cert), len(cert), upload_id
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
@override
|
|
184
|
+
def update_firmware(self, file: os.PathLike[AnyStr] | BytesIO) -> NETIODevice:
|
|
185
|
+
# TODO: Implement permission checks
|
|
186
|
+
# if "can_alter_settings" not in self.user_permissions:
|
|
187
|
+
# raise PermissionError(
|
|
188
|
+
# "You don't have permission to alter settings on this device."
|
|
189
|
+
# )
|
|
190
|
+
if self._ka_thread:
|
|
191
|
+
self._ka_thread.cancel()
|
|
192
|
+
self._ka_thread.join()
|
|
193
|
+
|
|
194
|
+
pre_reconnect_wait = 20
|
|
195
|
+
# TODO: Implement supported_features
|
|
196
|
+
try:
|
|
197
|
+
if self.supported_features["wifi"] == "yes":
|
|
198
|
+
wifi_settings = self.get_wifi_settings()
|
|
199
|
+
if (
|
|
200
|
+
wifi_settings["mode"] == "client"
|
|
201
|
+
and wifi_settings["client"]["status"] == "Connected"
|
|
202
|
+
):
|
|
203
|
+
pre_reconnect_wait = 50
|
|
204
|
+
self.logger.debug(
|
|
205
|
+
"Increased wait time after fimrware update due to active Wi-Fi connection."
|
|
206
|
+
)
|
|
207
|
+
except KeyError:
|
|
208
|
+
pre_reconnect_wait = 100
|
|
209
|
+
|
|
210
|
+
# TODO: Checnk connectivity
|
|
211
|
+
# if esp_api.check_connectivity(self) > (pre_reconnect_wait / 10.0):
|
|
212
|
+
# pre_reconnect_wait = pre_reconnect_wait * 2
|
|
213
|
+
# self.logger.debug(
|
|
214
|
+
# "Increased wait time after firmware update due to poor connection quality."
|
|
215
|
+
# )
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
ws_type = "SET"
|
|
219
|
+
ws_topic = "system/fwupdate"
|
|
220
|
+
ws_data = {}
|
|
221
|
+
upload_id = ws_api.send_request(self, ws_type, ws_topic, ws_data)["data"][
|
|
222
|
+
"uploadId"
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
if isinstance(file, io.BufferedIOBase):
|
|
226
|
+
ws_api.upload_file(self, file, upload_id)
|
|
227
|
+
else:
|
|
228
|
+
self.logger.debug("Unsupported file format.")
|
|
229
|
+
except CommunicationError:
|
|
230
|
+
self.logger.warning(
|
|
231
|
+
f"Device {self.host} couldn't verify firmware update process beginning, this should be harmless if the device connects, waiting for connection."
|
|
232
|
+
)
|
|
233
|
+
self.logger.debug(
|
|
234
|
+
f"Uploaded firmware, device {self.host} might be unresponsive for a while."
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# TODO: Sophisticated
|
|
238
|
+
pre_reconnect_wait = 30
|
|
239
|
+
sleep(pre_reconnect_wait)
|
|
240
|
+
|
|
241
|
+
self.logger.debug(
|
|
242
|
+
f"Retrying connection to device {self.host} after updating firmware to new version, the device class is now going to update."
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# TODO: Check connectivity for new FW
|
|
246
|
+
# device_response_time = esp_api.check_connectivity(self)
|
|
247
|
+
# retry_limit = 3 if device_response_time == -1 else 0
|
|
248
|
+
# for _ in range(0, retry_limit):
|
|
249
|
+
# device_response_time = esp_api.check_connectivity(self)
|
|
250
|
+
#
|
|
251
|
+
# if device_response_time == -1:
|
|
252
|
+
# raise CommunicationError(
|
|
253
|
+
# "Device couldn't establish connection after firmware update."
|
|
254
|
+
# )
|
|
255
|
+
|
|
256
|
+
updated_instance = None
|
|
257
|
+
if type(self.netio_manager) is NetioManager:
|
|
258
|
+
updated_instance = self.netio_manager.update_device(self, ws_expected=True)
|
|
259
|
+
|
|
260
|
+
if isinstance(updated_instance, NETIODevice):
|
|
261
|
+
self.logger.info(
|
|
262
|
+
f"The device has updated to {updated_instance.get_version_detailed()}, the device object is now of the {type(updated_instance)} class, check documentation for supported features and changes."
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
if not updated_instance:
|
|
266
|
+
raise CommunicationError("Coudln't get updated instance.")
|
|
267
|
+
|
|
268
|
+
return updated_instance
|
|
269
|
+
|
|
270
|
+
@override
|
|
271
|
+
def import_config(self, file, **kwargs):
|
|
272
|
+
ws_type = "SET"
|
|
273
|
+
ws_topic = "system/cfgimport"
|
|
274
|
+
ws_data = {}
|
|
275
|
+
upload_id = ws_api.send_request(self, ws_type, ws_topic, ws_data)["data"][
|
|
276
|
+
"uploadId"
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
if isinstance(file, io.BufferedIOBase):
|
|
280
|
+
self.logger.debug(f"Importing configuration to host {self.host}")
|
|
281
|
+
ws_api.upload_file(self, file, upload_id)
|
|
282
|
+
elif isinstance(file, str):
|
|
283
|
+
file_path = Path(file)
|
|
284
|
+
if file_path.exists():
|
|
285
|
+
with open(file, "rb") as file:
|
|
286
|
+
ws_api.upload_file(self, file, upload_id)
|
|
287
|
+
else:
|
|
288
|
+
self.logger.debug("Unsupported file format.")
|
|
289
|
+
|
|
290
|
+
@override
|
|
291
|
+
def ping(self) -> bool:
|
|
292
|
+
ws_type = "PING"
|
|
293
|
+
start_time = time()
|
|
294
|
+
ws_api.send_request(self, ws_type)
|
|
295
|
+
end_time = time()
|
|
296
|
+
self.logger.debug(
|
|
297
|
+
f"Pinging device {self.host} successful. Response time: {end_time - start_time}"
|
|
298
|
+
)
|
|
299
|
+
return True
|
|
300
|
+
|
|
301
|
+
@override
|
|
302
|
+
def export_config(self, save_file: str | None = None) -> dict[str, Any]:
|
|
303
|
+
ws_type = "SET"
|
|
304
|
+
ws_export_topics_list = [
|
|
305
|
+
"export/config/inputs",
|
|
306
|
+
"export/config/network",
|
|
307
|
+
"export/config/outputs",
|
|
308
|
+
"export/config/PAB",
|
|
309
|
+
"export/config/protocols",
|
|
310
|
+
"export/config/rules",
|
|
311
|
+
"export/config/schedules",
|
|
312
|
+
"export/config/system",
|
|
313
|
+
"export/config/users",
|
|
314
|
+
"export/config/wdtpingers",
|
|
315
|
+
"export/config/mqtt",
|
|
316
|
+
"export/config/nbus",
|
|
317
|
+
]
|
|
318
|
+
ws_data: dict[str, Any] = {}
|
|
319
|
+
|
|
320
|
+
config_export: dict[str, Any] = dict()
|
|
321
|
+
|
|
322
|
+
for config_group in ws_export_topics_list:
|
|
323
|
+
self.logger.debug(
|
|
324
|
+
f"Exporting configuration for {config_group.split('/')[-1]} from device {self.host}"
|
|
325
|
+
)
|
|
326
|
+
group_data = ws_api.send_request(self, ws_type, config_group, ws_data).get(
|
|
327
|
+
"data", {}
|
|
328
|
+
)
|
|
329
|
+
config_export |= {config_group.split("/")[-1]: group_data}
|
|
330
|
+
if save_file is not None:
|
|
331
|
+
file_path = Path(save_file)
|
|
332
|
+
if file_path.exists():
|
|
333
|
+
self.logger.warning("Export config file exists, overriding.")
|
|
334
|
+
# TODO: Make override a method parameter
|
|
335
|
+
|
|
336
|
+
with open(save_file, "w+") as file:
|
|
337
|
+
conf_string = json.dumps(config_export, ensure_ascii=False, indent=4)
|
|
338
|
+
file.write(conf_string)
|
|
339
|
+
|
|
340
|
+
return config_export
|
|
341
|
+
|
|
342
|
+
@override
|
|
343
|
+
def _keep_alive(self) -> None:
|
|
344
|
+
self.logger.debug(f"Sending keep-alive to device {self.host}")
|
|
345
|
+
self._ka_thread = threading.Timer(120, self._keep_alive)
|
|
346
|
+
self._ka_thread.daemon = True
|
|
347
|
+
self._ka_thread.start()
|
|
348
|
+
self.ping()
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Implementation specifics for ESP devices with the firmware 5.0.x.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import ssl
|
|
7
|
+
from collections import deque
|
|
8
|
+
from time import sleep
|
|
9
|
+
from typing import Dict, List, Tuple
|
|
10
|
+
|
|
11
|
+
import websocket
|
|
12
|
+
|
|
13
|
+
from PyNetioConf.constants import (
|
|
14
|
+
DEFAULT_KEEP_ALIVE_QUEUE_LEN,
|
|
15
|
+
DEFAULT_REQUEST_QUEUE_LEN,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from .. import NetioManager
|
|
19
|
+
from ..exceptions import CommunicationError
|
|
20
|
+
from . import esp_api, ws_api
|
|
21
|
+
from .esp_400_device import ESP400Device
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ESP5BetaDevice(ESP400Device):
|
|
25
|
+
"""
|
|
26
|
+
A class to control ESP devices with the firmware 5.0.x BETA.
|
|
27
|
+
|
|
28
|
+
DEPRICATED: This beta class will get removed in a future release.
|
|
29
|
+
WARNING: This release was and never will be fully supported, many features are missing and it is provided only for users who implemented their solution
|
|
30
|
+
alongside the development of the 5.0.x firmware. If anything doesn't work then please update the firmware of your device first.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
host: str,
|
|
36
|
+
username: str,
|
|
37
|
+
password: str,
|
|
38
|
+
sn_number: str,
|
|
39
|
+
hostname: str,
|
|
40
|
+
keep_alive: bool = True,
|
|
41
|
+
netio_manager: NetioManager = None,
|
|
42
|
+
use_https: bool = False,
|
|
43
|
+
):
|
|
44
|
+
self.use_https = use_https
|
|
45
|
+
super().__init__(
|
|
46
|
+
host, username, password, sn_number, hostname, keep_alive, netio_manager
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Backporting these to the beta so it can remain compatible with ws_api for as long as possible
|
|
50
|
+
self._request_queue = deque(maxlen=DEFAULT_REQUEST_QUEUE_LEN)
|
|
51
|
+
self._pong_queue = deque(maxlen=DEFAULT_KEEP_ALIVE_QUEUE_LEN)
|
|
52
|
+
|
|
53
|
+
self.fw_version = self.get_version()
|
|
54
|
+
|
|
55
|
+
self._ssl_context = ssl.create_default_context()
|
|
56
|
+
self._ssl_context.check_hostname = False
|
|
57
|
+
self._ssl_context.verify_mode = ssl.CERT_NONE
|
|
58
|
+
if self.use_https:
|
|
59
|
+
self.ws = websocket.create_connection(
|
|
60
|
+
f"wss://{host}/emweb", sslopt={"cert_reqs": ssl.CERT_NONE}
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
self.ws = websocket.create_connection(f"ws://{host}/emweb")
|
|
64
|
+
self.ws_req_id = 0
|
|
65
|
+
self._login_new(username, password)
|
|
66
|
+
|
|
67
|
+
def _login_new(self, username: str, password: str, logout=False) -> str:
|
|
68
|
+
if self.use_https:
|
|
69
|
+
self.ws = websocket.create_connection(
|
|
70
|
+
f"wss://{self.host}/emweb", sslopt={"cert_reqs": ssl.CERT_NONE}
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
self.ws = websocket.create_connection(f"ws://{self.host}/emweb")
|
|
74
|
+
self.ws_req_id = 0
|
|
75
|
+
hello_response = ws_api.send_request(self, "HELO")
|
|
76
|
+
|
|
77
|
+
ws_api.login(
|
|
78
|
+
self,
|
|
79
|
+
hello_response["data"]["localTimestamp"],
|
|
80
|
+
hello_response["data"]["publicKey"],
|
|
81
|
+
username,
|
|
82
|
+
password,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def set_output(self, output_id: int, state: bool) -> None:
|
|
86
|
+
self._check_socket_index(output_id)
|
|
87
|
+
ws_api.send_request(
|
|
88
|
+
self,
|
|
89
|
+
"SET",
|
|
90
|
+
f"outputs/id/{output_id}/ctrl",
|
|
91
|
+
{"request": "on" if state else "off"},
|
|
92
|
+
)
|
|
93
|
+
self.logger.debug(
|
|
94
|
+
f"Setting output {output_id} on device {self.host} to {state}."
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def set_json_api_state(
|
|
98
|
+
self,
|
|
99
|
+
protocol_enabled: bool,
|
|
100
|
+
read_enable: bool = None,
|
|
101
|
+
write_enable: bool = None,
|
|
102
|
+
read_auth: tuple[str, str] = None,
|
|
103
|
+
write_auth: tuple[str, str] = None,
|
|
104
|
+
) -> None:
|
|
105
|
+
old_protocol_data = self.get_json_api_state()
|
|
106
|
+
|
|
107
|
+
protocol_data = {
|
|
108
|
+
"enable": protocol_enabled,
|
|
109
|
+
"port": 80,
|
|
110
|
+
"readOnlyEnable": read_enable
|
|
111
|
+
if read_enable is not None
|
|
112
|
+
else old_protocol_data["read"]["enable"],
|
|
113
|
+
"readUsername": read_auth[0]
|
|
114
|
+
if read_auth is not None
|
|
115
|
+
else old_protocol_data["read"]["username"],
|
|
116
|
+
"readPassword": read_auth[1]
|
|
117
|
+
if read_auth is not None
|
|
118
|
+
else old_protocol_data["read"]["password"],
|
|
119
|
+
"readWriteEnable": write_enable
|
|
120
|
+
if write_enable is not None
|
|
121
|
+
else old_protocol_data["write"]["enable"],
|
|
122
|
+
"writeUsername": write_auth[0]
|
|
123
|
+
if write_auth is not None
|
|
124
|
+
else old_protocol_data["write"]["username"],
|
|
125
|
+
"writePassword": write_auth[1]
|
|
126
|
+
if write_auth is not None
|
|
127
|
+
else old_protocol_data["write"]["password"],
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
ws_api.send_request(self, "SET", "protocols/json/config", protocol_data)
|
|
131
|
+
|
|
132
|
+
def get_output_states(self) -> list[tuple[int, bool]]:
|
|
133
|
+
socket_list = ws_api.send_request(self, "UNSUBSCRIBE", "outputs/measure")
|
|
134
|
+
return [
|
|
135
|
+
(socket["id"], socket["state"] == "on")
|
|
136
|
+
for socket in socket_list["data"]["items"]
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
def get_measurement(self):
|
|
140
|
+
return ws_api.send_request(self, "UNSUBSCRIBE", "outputs/measure")
|
|
141
|
+
|
|
142
|
+
def upload_mqtt_client_key(self, key: str) -> None:
|
|
143
|
+
upload_path = ws_api.send_request(
|
|
144
|
+
self, "SET", "protocols/mqtt/clientkeyupload", {}
|
|
145
|
+
)["data"]["uploadPath"]
|
|
146
|
+
response = esp_api.send_file(self, upload_path, key)
|
|
147
|
+
sleep(0.1)
|
|
148
|
+
|
|
149
|
+
def upload_mqtt_client_certificate(self, cert: str) -> None:
|
|
150
|
+
upload_path = ws_api.send_request(
|
|
151
|
+
self, "SET", "protocols/mqtt/clientcertupload", {}
|
|
152
|
+
)["data"]["uploadPath"]
|
|
153
|
+
response = esp_api.send_file(self, upload_path, cert)
|
|
154
|
+
sleep(0.1)
|
|
155
|
+
|
|
156
|
+
def upload_mqtt_ca_certificate(self, ca: str) -> None:
|
|
157
|
+
upload_path = ws_api.send_request(
|
|
158
|
+
self, "SET", "protocols/mqtt/cacertupload", {}
|
|
159
|
+
)["data"]["uploadPath"]
|
|
160
|
+
response = esp_api.send_file(self, upload_path, ca)
|
|
161
|
+
sleep(0.1)
|
|
162
|
+
|
|
163
|
+
def get_mqttflex_state(self) -> dict:
|
|
164
|
+
return ws_api.send_request(self, "UNSUBSCRIBE", "protocols/mqtt/config")["data"]
|
|
165
|
+
|
|
166
|
+
def set_mqttflex_state(self, state: bool, config: dict = None) -> None:
|
|
167
|
+
if config is None:
|
|
168
|
+
config = self.get_mqttflex_state()["config"]
|
|
169
|
+
ws_api.send_request(
|
|
170
|
+
self,
|
|
171
|
+
"SET",
|
|
172
|
+
"protocols/mqtt/config",
|
|
173
|
+
{"enable": state, "config": json.dumps(config)},
|
|
174
|
+
)
|
|
175
|
+
sleep(1)
|
|
176
|
+
|
|
177
|
+
def export_config(self, save_file: str = None) -> dict:
|
|
178
|
+
config = ws_api.send_request(self, "SET", "system/cfgexport", data={})
|
|
179
|
+
if save_file:
|
|
180
|
+
with open(save_file, "w") as file:
|
|
181
|
+
json.dump(config["data"]["config"], file)
|
|
182
|
+
return config["data"]["config"]
|
|
183
|
+
|
|
184
|
+
def import_config(self, file, **kwargs) -> None:
|
|
185
|
+
if "can_alter_settings" not in self.user_permissions:
|
|
186
|
+
raise PermissionError(
|
|
187
|
+
"You don't have permission to alter settings on this device."
|
|
188
|
+
)
|
|
189
|
+
if self._ka_thread:
|
|
190
|
+
self._ka_thread.cancel()
|
|
191
|
+
self._ka_thread.join()
|
|
192
|
+
data = json.load(file)
|
|
193
|
+
encoded_data = json.dumps(data).encode("utf-8")
|
|
194
|
+
ws_api.send_request(self, "SET", "system/cfgimport", data={})
|
|
195
|
+
esp_api.send_file(self, "/cfgimport", encoded_data)
|
|
196
|
+
self.logger.info(
|
|
197
|
+
f"Imported configuration from {file.name}, device {self.host} is restarting..."
|
|
198
|
+
)
|
|
199
|
+
sleep(kwargs.get("sleep_time", 15))
|
|
200
|
+
username = kwargs.get("username", self.username)
|
|
201
|
+
password = kwargs.get("password", self.password)
|
|
202
|
+
|
|
203
|
+
self._login_new(username, password)
|
|
204
|
+
if self._ka_thread:
|
|
205
|
+
self._keep_alive()
|
|
206
|
+
|
|
207
|
+
def update_firmware(self, file) -> None:
|
|
208
|
+
if "can_alter_settings" not in self.user_permissions:
|
|
209
|
+
raise PermissionError(
|
|
210
|
+
"You don't have permission to alter settings on this device."
|
|
211
|
+
)
|
|
212
|
+
if self._ka_thread:
|
|
213
|
+
self._ka_thread.cancel()
|
|
214
|
+
self._ka_thread.join()
|
|
215
|
+
|
|
216
|
+
pre_reconnect_wait = 20
|
|
217
|
+
if self.supported_features["wifi"] == "yes":
|
|
218
|
+
wifi_settings = self.get_wifi_settings()
|
|
219
|
+
if (
|
|
220
|
+
wifi_settings["mode"] == "client"
|
|
221
|
+
and wifi_settings["client"]["status"] == "Connected"
|
|
222
|
+
):
|
|
223
|
+
pre_reconnect_wait = 50
|
|
224
|
+
self.logger.debug(
|
|
225
|
+
"Increased wait time after fimrware update due to active Wi-Fi connection."
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if esp_api.check_connectivity(self) > (pre_reconnect_wait / 10.0):
|
|
229
|
+
pre_reconnect_wait = pre_reconnect_wait * 2
|
|
230
|
+
self.logger.debug(
|
|
231
|
+
"Increased wait time after firmware update due to poor connection quality."
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
_ = esp_api.send_request(self, "prepFwUpgrade")
|
|
235
|
+
_ = esp_api.send_file(self, "/upload/firmware", file)
|
|
236
|
+
try:
|
|
237
|
+
_ = esp_api.send_request(self, "startUpgrade", close=True)
|
|
238
|
+
except CommunicationError:
|
|
239
|
+
self.logger.warn(
|
|
240
|
+
f"Device {self.host} couldn't verify firmware update process beginning, this should be harmless if the device connects, waiting for connection."
|
|
241
|
+
)
|
|
242
|
+
self.logger.debug(
|
|
243
|
+
f"Uploaded firmware {file.name}, device {self.host} might be unresponsive for a while."
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
sleep(pre_reconnect_wait)
|
|
247
|
+
|
|
248
|
+
self.logger.debug(
|
|
249
|
+
f"Retrying connection to device {self.host} after updating firmware to {file.name}."
|
|
250
|
+
)
|
|
251
|
+
device_response_time = esp_api.check_connectivity(self)
|
|
252
|
+
retry_limit = 3 if device_response_time == -1 else 0
|
|
253
|
+
for _ in range(0, retry_limit):
|
|
254
|
+
device_response_time = esp_api.check_connectivity(self)
|
|
255
|
+
|
|
256
|
+
if device_response_time == -1:
|
|
257
|
+
raise CommunicationError(
|
|
258
|
+
"Device couldn't establish connection after firmware update."
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
updated_instance = self.netio_manager.update_device(self)
|
|
262
|
+
|
|
263
|
+
return updated_instance
|