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,1826 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Implementation specifics for ESP devices with the firmware 5.0.x.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import ssl
|
|
9
|
+
import sys
|
|
10
|
+
from collections import deque
|
|
11
|
+
from io import BytesIO
|
|
12
|
+
from time import perf_counter, sleep
|
|
13
|
+
from xml.etree.ElementTree import Element
|
|
14
|
+
|
|
15
|
+
from PyNetioConf.constants import (
|
|
16
|
+
CLOUD_ACTION_COMMUNICATION_DELAY,
|
|
17
|
+
CLOUD_DEFAULT_CONNECTION_WAIT,
|
|
18
|
+
DEVICE_RESET_GRACE_PERIOD,
|
|
19
|
+
WS_DEFAULT_TIMEOUT,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if sys.version_info >= (3, 12):
|
|
23
|
+
from typing import IO, Any, AnyStr, Dict, List, Tuple, override
|
|
24
|
+
else:
|
|
25
|
+
from typing import IO, Any, AnyStr, Dict, List, Tuple
|
|
26
|
+
|
|
27
|
+
from typing_extensions import override
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
import threading
|
|
31
|
+
|
|
32
|
+
import websocket
|
|
33
|
+
from websocket import WebSocket
|
|
34
|
+
|
|
35
|
+
from .. import NetioManager
|
|
36
|
+
from ..constants import DEFAULT_KEEP_ALIVE_QUEUE_LEN, DEFAULT_REQUEST_QUEUE_LEN
|
|
37
|
+
from ..exceptions import (
|
|
38
|
+
CommunicationError,
|
|
39
|
+
ElementNotFound,
|
|
40
|
+
InvalidParameterValueError,
|
|
41
|
+
InvalidSocketIndex,
|
|
42
|
+
)
|
|
43
|
+
from ..netio_device import NETIODevice
|
|
44
|
+
from . import esp_api, ws_api
|
|
45
|
+
from .esp_400_device import ESP400Device
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _value_or_current(value: Any, current: dict[str, Any], key: str) -> Any:
|
|
49
|
+
"""Returns value, or the device's current value for key when value is None."""
|
|
50
|
+
return value if value is not None else current[key]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _auth_or_current(
|
|
54
|
+
auth: tuple[str, str] | None, current: dict[str, Any], prefix: str
|
|
55
|
+
) -> tuple[str, str]:
|
|
56
|
+
"""Returns auth, or the device's current (username, password) when auth is None."""
|
|
57
|
+
if auth is not None:
|
|
58
|
+
return auth
|
|
59
|
+
return current[f"{prefix}Username"], current[f"{prefix}Password"]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ESP500Device(NETIODevice):
|
|
63
|
+
"""
|
|
64
|
+
A class to control ESP devices with the firmware 5.0.x.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
@override
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
host: str,
|
|
71
|
+
username: str,
|
|
72
|
+
password: str,
|
|
73
|
+
sn_number: str,
|
|
74
|
+
hostname: str,
|
|
75
|
+
keep_alive: bool = True,
|
|
76
|
+
netio_manager: NetioManager | None = None,
|
|
77
|
+
use_https: bool = False,
|
|
78
|
+
**kwargs: Any,
|
|
79
|
+
):
|
|
80
|
+
"""
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
host : str
|
|
86
|
+
|
|
87
|
+
username : str
|
|
88
|
+
|
|
89
|
+
password : str
|
|
90
|
+
|
|
91
|
+
sn_number : str
|
|
92
|
+
|
|
93
|
+
hostname : str
|
|
94
|
+
|
|
95
|
+
: Any
|
|
96
|
+
|
|
97
|
+
keep_alive : bool
|
|
98
|
+
|
|
99
|
+
netio_manager : NetioManager | None
|
|
100
|
+
|
|
101
|
+
use_https : bool
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
self.host = host
|
|
106
|
+
self.username = username
|
|
107
|
+
self.password = password
|
|
108
|
+
self.sn_number = sn_number
|
|
109
|
+
self.hostname = hostname
|
|
110
|
+
self.session_id = ""
|
|
111
|
+
self.supported_features: dict[str, Any] = dict()
|
|
112
|
+
self.output_count = 4
|
|
113
|
+
self.user_permissions: list[str] = list()
|
|
114
|
+
self._keep_alive_flag = keep_alive
|
|
115
|
+
self.ws_req_id = kwargs.get("ws_req_id", 0)
|
|
116
|
+
self.use_https = use_https
|
|
117
|
+
if use_https:
|
|
118
|
+
from urllib3 import disable_warnings
|
|
119
|
+
from urllib3.exceptions import InsecureRequestWarning
|
|
120
|
+
|
|
121
|
+
disable_warnings(category=InsecureRequestWarning)
|
|
122
|
+
|
|
123
|
+
# Before we start dealing with websocket communication past initiation we create a deque for the responses.
|
|
124
|
+
# This is mainly to mitigate potential desynchronization between the device requests and the library posting them.
|
|
125
|
+
# This is mostly due to SUBSCRIBE topics sending events which we don't react to given the request-response architecture
|
|
126
|
+
# of PyNetioConf. PING PONG topics also cause potential desynchronization
|
|
127
|
+
self._request_queue = deque(maxlen=DEFAULT_REQUEST_QUEUE_LEN)
|
|
128
|
+
self._pong_queue = deque(maxlen=DEFAULT_KEEP_ALIVE_QUEUE_LEN)
|
|
129
|
+
|
|
130
|
+
# Init from base ESP Device
|
|
131
|
+
self.ws: WebSocket | None = kwargs.get("ws_connection", None)
|
|
132
|
+
self.logger = logging.getLogger(__name__)
|
|
133
|
+
if kwargs.get("is_ws_auth", False):
|
|
134
|
+
self.session_id = "TODO AUTH"
|
|
135
|
+
else:
|
|
136
|
+
self.session_id = self.login(username, password)
|
|
137
|
+
# self.supported_features = self.get_features()
|
|
138
|
+
# self.output_count: int = self.supported_features["outputCount"]
|
|
139
|
+
# self.user_permissions = self.get_current_user()["privileges"]
|
|
140
|
+
if keep_alive:
|
|
141
|
+
self._ka_thread = threading.Timer(120, self._keep_alive)
|
|
142
|
+
self._ka_thread.daemon = True
|
|
143
|
+
self._ka_thread.start()
|
|
144
|
+
atexit.register(self._cleanup)
|
|
145
|
+
|
|
146
|
+
# New Init
|
|
147
|
+
self.ssl_options: dict[str, Any] | None = kwargs.get("ssl_options", dict())
|
|
148
|
+
self.logger = logging.getLogger(self.__class__.__name__)
|
|
149
|
+
self.netio_manager = netio_manager
|
|
150
|
+
# self.fw_version = self.get_version()
|
|
151
|
+
|
|
152
|
+
_system_info = self.get_system_info()
|
|
153
|
+
self.output_count = _system_info["outputCount"]
|
|
154
|
+
self.input_count = _system_info["inputCount"]
|
|
155
|
+
self.supported_features["wifi"] = _system_info["wifiSupport"]
|
|
156
|
+
self.supported_features["eth"] = _system_info["ethSupport"]
|
|
157
|
+
self._ws_helo_data = kwargs.get(
|
|
158
|
+
"ws_helo_data", ws_api.send_request(self, "HELO")
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def _keep_alive(self) -> None:
|
|
162
|
+
self.logger.debug(
|
|
163
|
+
f"Keep-alive stub called for device {self.host}, keep-alive functianolity is restored on devices running 5.2.x and up, please update."
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
@override
|
|
167
|
+
def login(self, username: str, password: str, logout: bool = False) -> str:
|
|
168
|
+
if logout:
|
|
169
|
+
self.ws = None # Disables a connection if one was present, the device handles the logout automatically
|
|
170
|
+
|
|
171
|
+
if self.ws is None:
|
|
172
|
+
try:
|
|
173
|
+
# If the KA thread is alive it can send requests while the device is reconnecting on the old pipe.
|
|
174
|
+
if self._ka_thread:
|
|
175
|
+
self._ka_thread.cancel()
|
|
176
|
+
self._ka_thread.join()
|
|
177
|
+
self.logger.debug(
|
|
178
|
+
"Disabled keep-alive thread due to new login process."
|
|
179
|
+
)
|
|
180
|
+
except AttributeError:
|
|
181
|
+
pass # No need to clean up a non existant thread
|
|
182
|
+
|
|
183
|
+
try_count = 3
|
|
184
|
+
self.logger.debug(f"Attempting websocket connection to {self.host}")
|
|
185
|
+
for attempt in range(try_count):
|
|
186
|
+
self.logger.debug(
|
|
187
|
+
f"{self.host} websocket connection attempt {attempt + 1}/{try_count}"
|
|
188
|
+
)
|
|
189
|
+
try:
|
|
190
|
+
connection_dt_start = perf_counter()
|
|
191
|
+
if self.use_https:
|
|
192
|
+
self.ws = websocket.create_connection(
|
|
193
|
+
f"wss://{self.host}/emweb",
|
|
194
|
+
sslopt=self.ssl_options,
|
|
195
|
+
timeout=10,
|
|
196
|
+
) # pyright: ignore[reportUnknownMemberType]
|
|
197
|
+
else:
|
|
198
|
+
self.ws = websocket.create_connection(
|
|
199
|
+
f"ws://{self.host}/emweb", timeout=10
|
|
200
|
+
) # pyright: ignore[reportUnknownMemberType]
|
|
201
|
+
|
|
202
|
+
if isinstance(self.ws, WebSocket):
|
|
203
|
+
self.logger.debug(f"Succesfully connected to {self.host}")
|
|
204
|
+
break
|
|
205
|
+
else:
|
|
206
|
+
elapsed_time = perf_counter() - connection_dt_start
|
|
207
|
+
if elapsed_time < 10:
|
|
208
|
+
self.logger.debug(
|
|
209
|
+
"Couldn't establish ws connection in time, waiting to reconnect."
|
|
210
|
+
)
|
|
211
|
+
sleep(10 - elapsed_time)
|
|
212
|
+
except Exception as e:
|
|
213
|
+
self.logger.debug(
|
|
214
|
+
f"Connection to {self.host} failed on {attempt + 1}/{try_count}"
|
|
215
|
+
)
|
|
216
|
+
sleep(1)
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
if isinstance(self.ws, WebSocket):
|
|
220
|
+
self.logger.debug(f"Setting default timeout for {self.host}.")
|
|
221
|
+
self.ws.settimeout(WS_DEFAULT_TIMEOUT)
|
|
222
|
+
|
|
223
|
+
self.ws_req_id = 0
|
|
224
|
+
|
|
225
|
+
if self._keep_alive_flag:
|
|
226
|
+
self._ka_thread = threading.Timer(120, self._keep_alive)
|
|
227
|
+
self._ka_thread.daemon = True
|
|
228
|
+
self._ka_thread.start()
|
|
229
|
+
|
|
230
|
+
hello_response = ws_api.send_request(self, "HELO")
|
|
231
|
+
|
|
232
|
+
ws_api.login(
|
|
233
|
+
self,
|
|
234
|
+
hello_response["data"]["localTimestamp"],
|
|
235
|
+
hello_response["data"]["publicKey"],
|
|
236
|
+
username,
|
|
237
|
+
password,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return "TODO: Authenticate Session ID on 5.x.x"
|
|
241
|
+
|
|
242
|
+
@override
|
|
243
|
+
def logout(self) -> None:
|
|
244
|
+
self.logger.debug(f"Logging out of device {self.host}")
|
|
245
|
+
self.ws = None
|
|
246
|
+
self.ws_req_id = 0
|
|
247
|
+
|
|
248
|
+
@override
|
|
249
|
+
def get_current_user(self) -> dict[str, Any]:
|
|
250
|
+
self.logger.debug(
|
|
251
|
+
f"Fetching current user ({self.username}) data from device {self.host}"
|
|
252
|
+
)
|
|
253
|
+
ws_type = "SUBSCRIBE"
|
|
254
|
+
ws_topic = f"users/name/{self.username}/config"
|
|
255
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
256
|
+
return response.get("data", {})
|
|
257
|
+
|
|
258
|
+
@override
|
|
259
|
+
def get_user_privileges(self, username: str) -> list[str]:
|
|
260
|
+
self.logger.debug(
|
|
261
|
+
f"Fetching privileges for user {username} on device {self.host}"
|
|
262
|
+
)
|
|
263
|
+
ws_type = "SUBSCRIBE"
|
|
264
|
+
ws_topic = f"users/name/{username}/config"
|
|
265
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
266
|
+
data = response.get("data")
|
|
267
|
+
if not data:
|
|
268
|
+
raise ElementNotFound(f"User {username} not found on device {self.host}")
|
|
269
|
+
priv_dict = data.get("privileges", {})
|
|
270
|
+
return [key for key, val in priv_dict.items() if val is True]
|
|
271
|
+
|
|
272
|
+
@override
|
|
273
|
+
def get_users(self) -> dict[str, Any]:
|
|
274
|
+
ws_type = "SUBSCRIBE"
|
|
275
|
+
ws_topic = "users/list"
|
|
276
|
+
|
|
277
|
+
self.logger.debug(f"Fetching users list on device {self.host}")
|
|
278
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
279
|
+
items = response.get("data", {}).get("items", [])
|
|
280
|
+
return {item["name"]: item for item in items if "name" in item}
|
|
281
|
+
|
|
282
|
+
@override
|
|
283
|
+
def create_user(
|
|
284
|
+
self, username: str, password: str, privileges: list[str] | None = None
|
|
285
|
+
) -> None:
|
|
286
|
+
self.logger.debug(f"Creating user {username} on device {self.host}")
|
|
287
|
+
hello_response = ws_api.send_request(self, "HELO")
|
|
288
|
+
public_key = hello_response["data"]["publicKey"]
|
|
289
|
+
password_hash = ws_api.generate_password_hash(username, password, public_key)
|
|
290
|
+
|
|
291
|
+
all_privileges = [
|
|
292
|
+
"can_alter_rules",
|
|
293
|
+
"can_alter_settings",
|
|
294
|
+
"can_alter_users",
|
|
295
|
+
"can_login",
|
|
296
|
+
"can_control_outputs",
|
|
297
|
+
"can_alter_outputs",
|
|
298
|
+
"can_view_settings",
|
|
299
|
+
"can_alter_devices",
|
|
300
|
+
"can_view_history",
|
|
301
|
+
"can_control_user_buttons",
|
|
302
|
+
"can_browse_logs",
|
|
303
|
+
"can_use_tunnels",
|
|
304
|
+
]
|
|
305
|
+
if privileges is None or len(privileges) == 0:
|
|
306
|
+
privileges = ["can_login"]
|
|
307
|
+
|
|
308
|
+
privileges_dict = {key: (key in privileges) for key in all_privileges}
|
|
309
|
+
|
|
310
|
+
ws_type = "SET"
|
|
311
|
+
ws_topic = "users/create"
|
|
312
|
+
ws_data = {
|
|
313
|
+
"name": username,
|
|
314
|
+
"password": "",
|
|
315
|
+
"passwordHash": password_hash,
|
|
316
|
+
"privileges": privileges_dict,
|
|
317
|
+
}
|
|
318
|
+
ws_api.send_request(self, ws_type, ws_topic, ws_data)
|
|
319
|
+
|
|
320
|
+
@override
|
|
321
|
+
def remove_user(self, username: str) -> None:
|
|
322
|
+
self.logger.debug(f"Removing user {username} on device {self.host}")
|
|
323
|
+
ws_type = "SET"
|
|
324
|
+
ws_topic = "users/delete"
|
|
325
|
+
ws_data = {"name": username}
|
|
326
|
+
ws_api.send_request(self, ws_type, ws_topic, ws_data)
|
|
327
|
+
|
|
328
|
+
@override
|
|
329
|
+
def get_system_info(self) -> dict[str, Any]:
|
|
330
|
+
ws_type = "SUBSCRIBE"
|
|
331
|
+
ws_topic = "system/info"
|
|
332
|
+
self.logger.debug(f"Fetching system info on {self.host}")
|
|
333
|
+
return ws_api.send_request(self, ws_type, ws_topic)["data"]
|
|
334
|
+
|
|
335
|
+
@override
|
|
336
|
+
def get_uptime(self) -> int:
|
|
337
|
+
self.logger.debug(f"Fetching system uptime from device {self.host}")
|
|
338
|
+
ws_type = "SUBSCRIBE"
|
|
339
|
+
ws_topic = "system/uptime"
|
|
340
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
341
|
+
return int(response.get("data", {}).get("uptime", 0))
|
|
342
|
+
|
|
343
|
+
@override
|
|
344
|
+
def get_version_detailed(self) -> str:
|
|
345
|
+
self.logger.debug(f"Fetching detailed firmware version from device {self.host}")
|
|
346
|
+
return self.get_system_info()["fwVersion"]
|
|
347
|
+
|
|
348
|
+
@override
|
|
349
|
+
def get_version(self) -> str:
|
|
350
|
+
self.logger.debug(f"Fetching firmware version from device {self.host}")
|
|
351
|
+
return self.get_version_detailed().split("-")[0].strip()
|
|
352
|
+
|
|
353
|
+
@override
|
|
354
|
+
def get_output_data(self, output_id: int) -> dict[str, Any]:
|
|
355
|
+
ws_type = "SUBSCRIBE"
|
|
356
|
+
ws_topic = f"outputs/id/{output_id}/config"
|
|
357
|
+
self.logger.debug(
|
|
358
|
+
f"Fetching output configuration data for output id {output_id} on {self.host}"
|
|
359
|
+
)
|
|
360
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
361
|
+
return response.get("data", {})
|
|
362
|
+
|
|
363
|
+
def _set_output_config(
|
|
364
|
+
self,
|
|
365
|
+
output_id: int,
|
|
366
|
+
default_state: str | None = None,
|
|
367
|
+
is_schedule_active: bool | None = None,
|
|
368
|
+
output_name: str | None = None,
|
|
369
|
+
power_up_interval: int | None = None,
|
|
370
|
+
reset_delay: int | None = None,
|
|
371
|
+
schedule_id: int | None = None,
|
|
372
|
+
) -> None:
|
|
373
|
+
ws_type = "SET"
|
|
374
|
+
ws_topic = f"outputs/id/{output_id}/config"
|
|
375
|
+
|
|
376
|
+
self.logger.debug(f"Setting output {output_id} config on device {self.host}")
|
|
377
|
+
current = self._require_config(
|
|
378
|
+
self.get_output_data(output_id), f"output {output_id}"
|
|
379
|
+
)
|
|
380
|
+
ws_data = {
|
|
381
|
+
"default": _value_or_current(default_state, current, "default"),
|
|
382
|
+
"isScheduleActive": _value_or_current(
|
|
383
|
+
is_schedule_active, current, "isScheduleActive"
|
|
384
|
+
),
|
|
385
|
+
"name": _value_or_current(output_name, current, "name"),
|
|
386
|
+
"powerUpInterval": _value_or_current(
|
|
387
|
+
power_up_interval, current, "powerUpInterval"
|
|
388
|
+
),
|
|
389
|
+
"resetDelay": _value_or_current(reset_delay, current, "resetDelay"),
|
|
390
|
+
"scheduleId": _value_or_current(schedule_id, current, "scheduleId"),
|
|
391
|
+
}
|
|
392
|
+
_ = ws_api.send_request(self, ws_type, ws_topic, ws_data)
|
|
393
|
+
|
|
394
|
+
def _require_config(
|
|
395
|
+
self, current: dict[str, Any], config_name: str
|
|
396
|
+
) -> dict[str, Any]:
|
|
397
|
+
if not current:
|
|
398
|
+
message = (
|
|
399
|
+
f"Device {self.host} returned no {config_name} configuration, "
|
|
400
|
+
"settings were left unchanged"
|
|
401
|
+
)
|
|
402
|
+
self.logger.error(message)
|
|
403
|
+
raise CommunicationError(message)
|
|
404
|
+
return current
|
|
405
|
+
|
|
406
|
+
def _check_socket_index(self, socket_index: int) -> None:
|
|
407
|
+
if socket_index < 1 or socket_index > self.output_count:
|
|
408
|
+
raise InvalidSocketIndex(
|
|
409
|
+
f"You tried to access socket with id {socket_index},"
|
|
410
|
+
f" the device supports <1;{self.output_count}>."
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
@override
|
|
414
|
+
def rename_output(self, output_id: int, output_name: str) -> None:
|
|
415
|
+
self.logger.debug(f"Renaming output {output_id} to new name: {output_name}")
|
|
416
|
+
self._set_output_config(output_id, output_name=output_name)
|
|
417
|
+
|
|
418
|
+
@override
|
|
419
|
+
def set_output(self, output_id: int, state: bool) -> None:
|
|
420
|
+
ws_type = "SET"
|
|
421
|
+
ws_topic = f"outputs/id/{output_id}/ctrl"
|
|
422
|
+
ws_data = {"request": "on" if state else "off"}
|
|
423
|
+
|
|
424
|
+
self._check_socket_index(output_id)
|
|
425
|
+
self.logger.debug(
|
|
426
|
+
f"Setting output {output_id} to {state} on device {self.host}"
|
|
427
|
+
)
|
|
428
|
+
ws_api.send_request(
|
|
429
|
+
self,
|
|
430
|
+
ws_type,
|
|
431
|
+
ws_topic,
|
|
432
|
+
ws_data,
|
|
433
|
+
)
|
|
434
|
+
self.logger.debug(
|
|
435
|
+
f"Setting output {output_id} on device {self.host} to {state}."
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
@override
|
|
439
|
+
def set_outputs_unified(self, state: bool) -> None:
|
|
440
|
+
self.logger.debug(
|
|
441
|
+
f"Setting all outputs unified to {state} on device {self.host}"
|
|
442
|
+
)
|
|
443
|
+
for output in range(1, self.output_count + 1):
|
|
444
|
+
self.set_output(output, state)
|
|
445
|
+
|
|
446
|
+
@override
|
|
447
|
+
def reset_output(self, output_id: int) -> None:
|
|
448
|
+
raise NotImplementedError("reset_output is not supported on 5.0.0+ firmware")
|
|
449
|
+
|
|
450
|
+
@override
|
|
451
|
+
def reset_output_consumption_counter(self, output_id: int) -> None:
|
|
452
|
+
ws_type = "SET"
|
|
453
|
+
ws_topic = f"outputs/id/{output_id}/resetOutputConsumption"
|
|
454
|
+
ws_data = {"request": True}
|
|
455
|
+
|
|
456
|
+
self._check_socket_index(output_id)
|
|
457
|
+
self.logger.debug(
|
|
458
|
+
f"Resetting power consumption counters for output {output_id} on device {self.host}"
|
|
459
|
+
)
|
|
460
|
+
ws_api.send_request(
|
|
461
|
+
self,
|
|
462
|
+
ws_type,
|
|
463
|
+
ws_topic,
|
|
464
|
+
ws_data,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
@override
|
|
468
|
+
def reset_power_consumption_counters(self) -> None:
|
|
469
|
+
self.logger.debug(
|
|
470
|
+
f"Resetting power consumption counters for all outputs on device {self.host}"
|
|
471
|
+
)
|
|
472
|
+
for output in range(1, self.output_count + 1):
|
|
473
|
+
self.reset_output_consumption_counter(output)
|
|
474
|
+
sleep(0.1)
|
|
475
|
+
|
|
476
|
+
@override
|
|
477
|
+
def get_output_schedule(self, output_id: int) -> dict[str, Any]:
|
|
478
|
+
self.logger.debug(
|
|
479
|
+
f"Fetching output {output_id} schedule from device {self.host}"
|
|
480
|
+
)
|
|
481
|
+
output_data = self.get_output_data(output_id)
|
|
482
|
+
return {
|
|
483
|
+
"id": output_data.get("scheduleId", 0),
|
|
484
|
+
"on": output_data.get("isScheduleActive", False),
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
@override
|
|
488
|
+
def get_output_schedule_id(self, output_id: int) -> int:
|
|
489
|
+
self.logger.debug(
|
|
490
|
+
f"Fetching output {output_id} schedule ID from device {self.host}"
|
|
491
|
+
)
|
|
492
|
+
return int(self.get_output_schedule(output_id)["id"])
|
|
493
|
+
|
|
494
|
+
@override
|
|
495
|
+
def set_output_schedule(
|
|
496
|
+
self, output_id: int, schedule_id: int, enabled: bool | None = None
|
|
497
|
+
) -> None:
|
|
498
|
+
self.logger.debug(
|
|
499
|
+
f"Setting output {output_id} schedule to schedule ID {schedule_id} (active={enabled}) on {self.host}"
|
|
500
|
+
)
|
|
501
|
+
self._set_output_config(
|
|
502
|
+
output_id, schedule_id=schedule_id, is_schedule_active=enabled
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
@override
|
|
506
|
+
def set_output_schedule_state(self, output_id: int, schedule_enabled: bool) -> None:
|
|
507
|
+
self.logger.debug(
|
|
508
|
+
f"Setting output {output_id} schedule state to {schedule_enabled} on {self.host}"
|
|
509
|
+
)
|
|
510
|
+
self._set_output_config(output_id, is_schedule_active=schedule_enabled)
|
|
511
|
+
|
|
512
|
+
@override
|
|
513
|
+
def set_output_schedule_by_name(
|
|
514
|
+
self, output_id: int, schedule_name: str, enabled: bool | None = None
|
|
515
|
+
) -> None:
|
|
516
|
+
self.logger.debug(
|
|
517
|
+
f"Setting output {output_id} schedule to '{schedule_name}' on {self.host}"
|
|
518
|
+
)
|
|
519
|
+
schedule_id = self.get_schedule_id(schedule_name)
|
|
520
|
+
self.set_output_schedule(output_id, schedule_id, enabled)
|
|
521
|
+
|
|
522
|
+
def _create_configurable_block(
|
|
523
|
+
self, enable: bool, name: str, config: dict[str, Any], topic: str
|
|
524
|
+
) -> None:
|
|
525
|
+
ws_type = "SET"
|
|
526
|
+
|
|
527
|
+
self.logger.debug(f"Creating '{name}' via {topic} on device {self.host}")
|
|
528
|
+
rule_data = {
|
|
529
|
+
"enabled": enable,
|
|
530
|
+
"name": name,
|
|
531
|
+
"config": json.dumps(config),
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
ws_api.send_request(self, ws_type, topic, rule_data)
|
|
535
|
+
|
|
536
|
+
def create_rule(self, enable: bool, name: str, config: dict[str, Any]) -> None:
|
|
537
|
+
topic = "/rules/create"
|
|
538
|
+
try:
|
|
539
|
+
self.get_rule_by_name(name)
|
|
540
|
+
raise
|
|
541
|
+
except ElementNotFound:
|
|
542
|
+
pass
|
|
543
|
+
self.logger.debug(f"Attempting to create rule {name} on device {self.host}.")
|
|
544
|
+
self.logger.debug(f"Rule configuration: {json.dumps(config)}")
|
|
545
|
+
self._create_configurable_block(enable, name, config, topic)
|
|
546
|
+
|
|
547
|
+
@override
|
|
548
|
+
def get_rules(self) -> list[dict[str, Any]]:
|
|
549
|
+
self.logger.debug(f"Getting rules from device {self.host}")
|
|
550
|
+
ws_type = "SUBSCRIBE"
|
|
551
|
+
ws_topic = "rules/list"
|
|
552
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
553
|
+
return response.get("data", {}).get("items", [])
|
|
554
|
+
|
|
555
|
+
@override
|
|
556
|
+
def get_rule_by_name(self, rule_name: str) -> dict[str, Any]:
|
|
557
|
+
self.logger.debug(f"Filtering for rule {rule_name} on device {self.host}")
|
|
558
|
+
ws_type = "SUBSCRIBE"
|
|
559
|
+
ws_topic = f"rules/name/{rule_name}/config"
|
|
560
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
561
|
+
data = response.get("data")
|
|
562
|
+
if not data:
|
|
563
|
+
raise ElementNotFound(f"Rule {rule_name} not found on device {self.host}")
|
|
564
|
+
return data
|
|
565
|
+
|
|
566
|
+
@override
|
|
567
|
+
def get_enabled_rules(self) -> list[dict[str, Any]]:
|
|
568
|
+
rules = self.get_rules()
|
|
569
|
+
self.logger.debug(f"Filtering for enabled rules from device {self.host}")
|
|
570
|
+
return [rule for rule in rules if rule.get("enabled") is True]
|
|
571
|
+
|
|
572
|
+
@override
|
|
573
|
+
def get_disabled_rules(self) -> list[dict[str, Any]]:
|
|
574
|
+
rules = self.get_rules()
|
|
575
|
+
self.logger.debug(f"Filtering for disabled rules from device {self.host}")
|
|
576
|
+
return [rule for rule in rules if rule.get("enabled") is False]
|
|
577
|
+
|
|
578
|
+
def create_pab(self, enable: bool, name: str, config: dict[str, Any]) -> None:
|
|
579
|
+
topic = "/pab/create"
|
|
580
|
+
try:
|
|
581
|
+
self.get_pab_by_name(name)
|
|
582
|
+
raise
|
|
583
|
+
except ElementNotFound:
|
|
584
|
+
pass
|
|
585
|
+
self.logger.debug(f"Attempting to create PAB {name} on device {self.host}.")
|
|
586
|
+
self.logger.debug(f"PAB configuration: {json.dumps(config)}")
|
|
587
|
+
self._create_configurable_block(enable, name, config, topic)
|
|
588
|
+
|
|
589
|
+
@override
|
|
590
|
+
def get_pabs(self) -> list[dict[str, Any]]:
|
|
591
|
+
self.logger.debug(f"Getting PABs from device {self.host}")
|
|
592
|
+
ws_type = "SUBSCRIBE"
|
|
593
|
+
ws_topic = "pab/list"
|
|
594
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
595
|
+
return response.get("data", {}).get("items", [])
|
|
596
|
+
|
|
597
|
+
@override
|
|
598
|
+
def get_pab_by_name(self, pab_name: str) -> dict[str, Any]:
|
|
599
|
+
self.logger.debug(f"Filtering for PAB {pab_name} on device {self.host}")
|
|
600
|
+
ws_type = "SUBSCRIBE"
|
|
601
|
+
ws_topic = f"pab/name/{pab_name}/config"
|
|
602
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
603
|
+
data = response.get("data")
|
|
604
|
+
if not data:
|
|
605
|
+
raise ElementNotFound(f"PAB {pab_name} not found on device {self.host}")
|
|
606
|
+
return data
|
|
607
|
+
|
|
608
|
+
@override
|
|
609
|
+
def get_enabled_pabs(self) -> list[dict[str, Any]]:
|
|
610
|
+
pabs = self.get_pabs()
|
|
611
|
+
self.logger.debug(f"Filtering for enabled PABs from device {self.host}")
|
|
612
|
+
return [pab for pab in pabs if pab.get("enabled") is True]
|
|
613
|
+
|
|
614
|
+
@override
|
|
615
|
+
def get_disabled_pabs(self) -> list[dict[str, Any]]:
|
|
616
|
+
pabs = self.get_pabs()
|
|
617
|
+
self.logger.debug(f"Filtering for disabled PABs from device {self.host}")
|
|
618
|
+
return [pab for pab in pabs if pab.get("enabled") is False]
|
|
619
|
+
|
|
620
|
+
def create_watchdog(self, enable: bool, name: str, config: dict[str, Any]) -> None:
|
|
621
|
+
topic = "/wdtpingers/create"
|
|
622
|
+
try:
|
|
623
|
+
self.get_watchdog_by_name(name)
|
|
624
|
+
raise
|
|
625
|
+
except ElementNotFound:
|
|
626
|
+
pass
|
|
627
|
+
self.logger.debug(
|
|
628
|
+
f"Attempting to create watchdog {name} on device {self.host}."
|
|
629
|
+
)
|
|
630
|
+
self.logger.debug(f"Watchdog configuration: {json.dumps(config)}")
|
|
631
|
+
self._create_configurable_block(enable, name, config, topic)
|
|
632
|
+
|
|
633
|
+
@override
|
|
634
|
+
def get_watchdogs(self) -> list[dict[str, Any]]:
|
|
635
|
+
self.logger.debug(f"Getting watchdogs from device {self.host}")
|
|
636
|
+
ws_type = "SUBSCRIBE"
|
|
637
|
+
ws_topic = "wdtpingers/list"
|
|
638
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
639
|
+
return response.get("data", {}).get("items", [])
|
|
640
|
+
|
|
641
|
+
@override
|
|
642
|
+
def get_watchdog_by_name(self, watchdog_name: str) -> dict[str, Any]:
|
|
643
|
+
self.logger.debug(
|
|
644
|
+
f"Filtering for watchdog {watchdog_name} on device {self.host}"
|
|
645
|
+
)
|
|
646
|
+
ws_type = "SUBSCRIBE"
|
|
647
|
+
ws_topic = f"wdtpingers/name/{watchdog_name}/config"
|
|
648
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
649
|
+
data = response.get("data")
|
|
650
|
+
if not data:
|
|
651
|
+
raise ElementNotFound(
|
|
652
|
+
f"Watchdog {watchdog_name} not found on device {self.host}"
|
|
653
|
+
)
|
|
654
|
+
return data
|
|
655
|
+
|
|
656
|
+
@override
|
|
657
|
+
def get_enabled_watchdogs(self) -> list[dict[str, Any]]:
|
|
658
|
+
watchdogs = self.get_watchdogs()
|
|
659
|
+
self.logger.debug(f"Filtering for enabled watchdogs from device {self.host}")
|
|
660
|
+
return [w for w in watchdogs if w.get("enabled") is True]
|
|
661
|
+
|
|
662
|
+
@override
|
|
663
|
+
def get_disabled_watchdogs(self) -> list[dict[str, Any]]:
|
|
664
|
+
watchdogs = self.get_watchdogs()
|
|
665
|
+
self.logger.debug(f"Filtering for disabled watchdogs from device {self.host}")
|
|
666
|
+
return [w for w in watchdogs if w.get("enabled") is False]
|
|
667
|
+
|
|
668
|
+
def create_schedule(self, name: str, intervals: list[dict[int, Any]]) -> None:
|
|
669
|
+
topic = "/schedules/create"
|
|
670
|
+
ws_type = "SET"
|
|
671
|
+
ws_data = {
|
|
672
|
+
"name": name,
|
|
673
|
+
"intervals": intervals,
|
|
674
|
+
}
|
|
675
|
+
try:
|
|
676
|
+
self.get_schedule_by_name(name)
|
|
677
|
+
raise
|
|
678
|
+
except ElementNotFound:
|
|
679
|
+
pass
|
|
680
|
+
self.logger.debug(
|
|
681
|
+
f"Attempting to create schedule {name} on device {self.host}."
|
|
682
|
+
)
|
|
683
|
+
self.logger.debug(f"Schedule configuration: {intervals}")
|
|
684
|
+
ws_api.send_request(self, ws_type, topic, ws_data)
|
|
685
|
+
|
|
686
|
+
@override
|
|
687
|
+
def get_schedules(self) -> list[dict[str, Any]]:
|
|
688
|
+
self.logger.debug(f"Getting a schedule list from device {self.host}")
|
|
689
|
+
ws_type = "SUBSCRIBE"
|
|
690
|
+
ws_topic = "schedules/list"
|
|
691
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
692
|
+
return response.get("data", {}).get("items", [])
|
|
693
|
+
|
|
694
|
+
@override
|
|
695
|
+
def get_schedule_by_name(self, schedule_name: str) -> dict[str, Any]:
|
|
696
|
+
self.logger.debug(
|
|
697
|
+
f"Filtering for schedule {schedule_name} on device {self.host}"
|
|
698
|
+
)
|
|
699
|
+
ws_type = "SUBSCRIBE"
|
|
700
|
+
ws_topic = f"schedules/name/{schedule_name}/config"
|
|
701
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
702
|
+
data = response.get("data")
|
|
703
|
+
if not data:
|
|
704
|
+
raise ElementNotFound(
|
|
705
|
+
f"Schedule {schedule_name} not found on device {self.host}"
|
|
706
|
+
)
|
|
707
|
+
return data
|
|
708
|
+
|
|
709
|
+
@override
|
|
710
|
+
def get_schedule_id(self, schedule_name: str) -> int:
|
|
711
|
+
self.logger.debug(
|
|
712
|
+
f"Fetching ID of schedule {schedule_name} from device {self.host}"
|
|
713
|
+
)
|
|
714
|
+
schedules = self.get_schedules()
|
|
715
|
+
for schedule in schedules:
|
|
716
|
+
if schedule.get("name") == schedule_name:
|
|
717
|
+
return int(schedule["id"])
|
|
718
|
+
raise ElementNotFound(
|
|
719
|
+
f"Schedule {schedule_name} not found on device {self.host}"
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
@override
|
|
723
|
+
def get_schedule_names(self) -> list[str]:
|
|
724
|
+
schedules = self.get_schedules()
|
|
725
|
+
self.logger.debug(f"Filtering for schedule names on device {self.host}")
|
|
726
|
+
return [schedule["name"] for schedule in schedules if "name" in schedule]
|
|
727
|
+
|
|
728
|
+
@override
|
|
729
|
+
def get_active_schedules(self) -> list[dict[str, Any]]:
|
|
730
|
+
schedules = self.get_schedules()
|
|
731
|
+
self.logger.debug(f"Filtering for active schedules on device {self.host}")
|
|
732
|
+
return [schedule for schedule in schedules if schedule.get("active") is True]
|
|
733
|
+
|
|
734
|
+
@override
|
|
735
|
+
def delete_schedule(self, schedule_id: int) -> None:
|
|
736
|
+
self.logger.debug(f"Deleting schedule {schedule_id} from device {self.host}")
|
|
737
|
+
schedules = self.get_schedules()
|
|
738
|
+
schedule_name = None
|
|
739
|
+
for schedule in schedules:
|
|
740
|
+
if schedule.get("id") == schedule_id:
|
|
741
|
+
schedule_name = schedule.get("name")
|
|
742
|
+
break
|
|
743
|
+
|
|
744
|
+
if not schedule_name:
|
|
745
|
+
raise ElementNotFound(
|
|
746
|
+
f"Schedule with ID {schedule_id} not found on device {self.host}"
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
self.delete_schedule_by_name(schedule_name)
|
|
750
|
+
|
|
751
|
+
@override
|
|
752
|
+
def delete_schedule_by_name(self, schedule_name: str) -> None:
|
|
753
|
+
# Check for schedule existing, error out if it doesn't
|
|
754
|
+
try:
|
|
755
|
+
self.get_schedule_by_name(schedule_name)
|
|
756
|
+
except ElementNotFound:
|
|
757
|
+
raise ElementNotFound(
|
|
758
|
+
f"Schedule {schedule_name} not found on device {self.host}"
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
topic = "schedules/delete"
|
|
762
|
+
ws_type = "SET"
|
|
763
|
+
ws_data = {"name": schedule_name}
|
|
764
|
+
self.logger.debug(
|
|
765
|
+
f"Attempting to delete schedule {schedule_name} on device {self.host}."
|
|
766
|
+
)
|
|
767
|
+
ws_api.send_request(self, ws_type, topic, ws_data)
|
|
768
|
+
|
|
769
|
+
def delete_rule_by_name(self, name: str) -> None:
|
|
770
|
+
# Check for rule existing, error out if it doesn't
|
|
771
|
+
try:
|
|
772
|
+
self.get_rule_by_name(name)
|
|
773
|
+
except ElementNotFound:
|
|
774
|
+
raise ElementNotFound(f"Rule {name} not found on device {self.host}")
|
|
775
|
+
topic = "/rules/delete"
|
|
776
|
+
ws_type = "SET"
|
|
777
|
+
ws_data = {"name": name}
|
|
778
|
+
self.logger.debug(f"Attempting to delete rule {name} on device {self.host}.")
|
|
779
|
+
ws_api.send_request(self, ws_type, topic, ws_data)
|
|
780
|
+
|
|
781
|
+
@override
|
|
782
|
+
def delete_pab_by_name(self, pab_name: str) -> None:
|
|
783
|
+
# Check for PAB existing, error out if it doesn't
|
|
784
|
+
try:
|
|
785
|
+
self.get_pab_by_name(pab_name)
|
|
786
|
+
except ElementNotFound:
|
|
787
|
+
raise ElementNotFound(f"PAB {pab_name} not found on device {self.host}")
|
|
788
|
+
topic = "/pab/delete"
|
|
789
|
+
ws_type = "SET"
|
|
790
|
+
ws_data = {"name": pab_name}
|
|
791
|
+
self.logger.debug(f"Attempting to delete PAB {pab_name} on device {self.host}.")
|
|
792
|
+
ws_api.send_request(self, ws_type, topic, ws_data)
|
|
793
|
+
|
|
794
|
+
@override
|
|
795
|
+
def get_active_protocols(self) -> list[int]:
|
|
796
|
+
self.logger.debug(f"Fetching active protocols on {self.host}")
|
|
797
|
+
ws_type = "SUBSCRIBE"
|
|
798
|
+
ws_topic = "protocols/info"
|
|
799
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
800
|
+
|
|
801
|
+
active_dict = response.get("data", {}).get("active", {})
|
|
802
|
+
active_ids = []
|
|
803
|
+
protocol_map = {
|
|
804
|
+
"xml": 103,
|
|
805
|
+
"json": 104,
|
|
806
|
+
"url": 105,
|
|
807
|
+
"telnet": 106,
|
|
808
|
+
"modbus": 107,
|
|
809
|
+
"mqtt": 108,
|
|
810
|
+
"push": 109,
|
|
811
|
+
"snmp": 110,
|
|
812
|
+
}
|
|
813
|
+
for name, is_active in active_dict.items():
|
|
814
|
+
if is_active and name in protocol_map:
|
|
815
|
+
active_ids.append(protocol_map[name])
|
|
816
|
+
return active_ids
|
|
817
|
+
|
|
818
|
+
@override
|
|
819
|
+
def get_supported_protocols(self) -> list[int]:
|
|
820
|
+
self.logger.debug(f"Fetching supported protocols on {self.host}")
|
|
821
|
+
ws_type = "SUBSCRIBE"
|
|
822
|
+
ws_topic = "protocols/info"
|
|
823
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
824
|
+
|
|
825
|
+
active_dict = response.get("data", {}).get("active", {})
|
|
826
|
+
protocol_map = {
|
|
827
|
+
"xml": 103,
|
|
828
|
+
"json": 104,
|
|
829
|
+
"url": 105,
|
|
830
|
+
"telnet": 106,
|
|
831
|
+
"modbus": 107,
|
|
832
|
+
"mqtt": 108,
|
|
833
|
+
"push": 109,
|
|
834
|
+
"snmp": 110,
|
|
835
|
+
}
|
|
836
|
+
supported_ids = []
|
|
837
|
+
for name in active_dict.keys():
|
|
838
|
+
if name in protocol_map:
|
|
839
|
+
supported_ids.append(protocol_map[name])
|
|
840
|
+
return supported_ids
|
|
841
|
+
|
|
842
|
+
def get_xml_api_state(self) -> dict[str, Any]:
|
|
843
|
+
self.logger.debug(f"Getting XML API state on device {self.host}")
|
|
844
|
+
ws_type = "SUBSCRIBE"
|
|
845
|
+
ws_topic = "protocols/xml/config"
|
|
846
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
847
|
+
return response["data"]
|
|
848
|
+
|
|
849
|
+
@override
|
|
850
|
+
def set_xml_api_state(
|
|
851
|
+
self,
|
|
852
|
+
protocol_enabled: bool | None = None,
|
|
853
|
+
read_enable: bool | None = None,
|
|
854
|
+
write_enable: bool | None = None,
|
|
855
|
+
read_auth: tuple[str, str] | None = None,
|
|
856
|
+
write_auth: tuple[str, str] | None = None,
|
|
857
|
+
) -> None:
|
|
858
|
+
ws_type = "SET"
|
|
859
|
+
ws_topic = "protocols/xml/config"
|
|
860
|
+
|
|
861
|
+
self.logger.debug(f"Setting XML API state on device {self.host}")
|
|
862
|
+
current = self._require_config(self.get_xml_api_state(), "XML API")
|
|
863
|
+
read_username, read_password = _auth_or_current(read_auth, current, "read")
|
|
864
|
+
write_username, write_password = _auth_or_current(write_auth, current, "write")
|
|
865
|
+
protocol_data = {
|
|
866
|
+
"enable": _value_or_current(protocol_enabled, current, "enable"),
|
|
867
|
+
"port": current["port"],
|
|
868
|
+
"readOnlyEnable": _value_or_current(read_enable, current, "readOnlyEnable"),
|
|
869
|
+
"readUsername": read_username,
|
|
870
|
+
"readPassword": read_password,
|
|
871
|
+
"readWriteEnable": _value_or_current(
|
|
872
|
+
write_enable, current, "readWriteEnable"
|
|
873
|
+
),
|
|
874
|
+
"writeUsername": write_username,
|
|
875
|
+
"writePassword": write_password,
|
|
876
|
+
}
|
|
877
|
+
_ = ws_api.send_request(self, ws_type, ws_topic, protocol_data)
|
|
878
|
+
|
|
879
|
+
@override
|
|
880
|
+
def get_json_api_state(self) -> dict[str, Any]:
|
|
881
|
+
self.logger.debug(f"Getting JSON API state on device {self.host}")
|
|
882
|
+
ws_type = "SUBSCRIBE"
|
|
883
|
+
ws_topic = "protocols/json/config"
|
|
884
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
885
|
+
return response["data"]
|
|
886
|
+
|
|
887
|
+
@override
|
|
888
|
+
def get_json(self, json_auth: tuple[str, str]) -> dict[str, Any]:
|
|
889
|
+
# TODO: Implement get_json for 5.0.0+ firmware
|
|
890
|
+
raise NotImplementedError("get_json is not yet implemented for 5.0.0+ firmware")
|
|
891
|
+
|
|
892
|
+
@override
|
|
893
|
+
def get_xml(self, xml_auth: tuple[str, str]) -> Element:
|
|
894
|
+
# TODO: Implement get_xml for 5.0.0+ firmware
|
|
895
|
+
raise NotImplementedError("get_xml is not yet implemented for 5.0.0+ firmware")
|
|
896
|
+
|
|
897
|
+
@override
|
|
898
|
+
def get_telnet_api_state(self) -> dict[str, Any]:
|
|
899
|
+
self.logger.debug(f"Getting Telnet API state on device {self.host}")
|
|
900
|
+
ws_type = "SUBSCRIBE"
|
|
901
|
+
ws_topic = "protocols/telnet/config"
|
|
902
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
903
|
+
return response["data"]
|
|
904
|
+
|
|
905
|
+
@override
|
|
906
|
+
def set_telnet_api_state(
|
|
907
|
+
self,
|
|
908
|
+
protocol_enabled: bool | None = None,
|
|
909
|
+
port: int | None = None,
|
|
910
|
+
read_enabled: bool | None = None,
|
|
911
|
+
read_auth: tuple[str, str] | None = None,
|
|
912
|
+
write_enabled: bool | None = None,
|
|
913
|
+
write_auth: tuple[str, str] | None = None,
|
|
914
|
+
) -> None:
|
|
915
|
+
ws_type = "SET"
|
|
916
|
+
ws_topic = "protocols/telnet/config"
|
|
917
|
+
|
|
918
|
+
self.logger.debug(f"Setting Telnet API state on device {self.host}")
|
|
919
|
+
current = self._require_config(self.get_telnet_api_state(), "Telnet API")
|
|
920
|
+
read_username, read_password = _auth_or_current(read_auth, current, "read")
|
|
921
|
+
write_username, write_password = _auth_or_current(write_auth, current, "write")
|
|
922
|
+
protocol_data = {
|
|
923
|
+
"enable": _value_or_current(protocol_enabled, current, "enable"),
|
|
924
|
+
"port": _value_or_current(port, current, "port"),
|
|
925
|
+
"readOnlyEnable": _value_or_current(
|
|
926
|
+
read_enabled, current, "readOnlyEnable"
|
|
927
|
+
),
|
|
928
|
+
"readUsername": read_username,
|
|
929
|
+
"readPassword": read_password,
|
|
930
|
+
"readWriteEnable": _value_or_current(
|
|
931
|
+
write_enabled, current, "readWriteEnable"
|
|
932
|
+
),
|
|
933
|
+
"writeUsername": write_username,
|
|
934
|
+
"writePassword": write_password,
|
|
935
|
+
}
|
|
936
|
+
_ = ws_api.send_request(self, ws_type, ws_topic, protocol_data)
|
|
937
|
+
|
|
938
|
+
@override
|
|
939
|
+
def get_modbus_state(self) -> dict[str, Any]:
|
|
940
|
+
self.logger.debug(f"Getting Modbus M2M API state on device {self.host}")
|
|
941
|
+
ws_type = "SUBSCRIBE"
|
|
942
|
+
ws_topic = "protocols/modbus/config"
|
|
943
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
944
|
+
return response["data"]
|
|
945
|
+
|
|
946
|
+
@override
|
|
947
|
+
def set_modbus_state(
|
|
948
|
+
self,
|
|
949
|
+
protocol_enabled: bool | None = None,
|
|
950
|
+
port: int | None = None,
|
|
951
|
+
ip_filter_enabled: bool | None = None,
|
|
952
|
+
ip_from: str | None = None,
|
|
953
|
+
ip_to: str | None = None,
|
|
954
|
+
) -> None:
|
|
955
|
+
ws_type = "SET"
|
|
956
|
+
ws_topic = "protocols/modbus/config"
|
|
957
|
+
|
|
958
|
+
self.logger.debug(f"Setting Modbus M2M API state on device {self.host}")
|
|
959
|
+
current = self._require_config(self.get_modbus_state(), "Modbus M2M API")
|
|
960
|
+
protocol_data = {
|
|
961
|
+
"enable": _value_or_current(protocol_enabled, current, "enable"),
|
|
962
|
+
"port": _value_or_current(port, current, "port"),
|
|
963
|
+
"lastIp": current["lastIp"],
|
|
964
|
+
"ipFilterEnable": _value_or_current(
|
|
965
|
+
ip_filter_enabled, current, "ipFilterEnable"
|
|
966
|
+
),
|
|
967
|
+
"ipFrom": _value_or_current(ip_from, current, "ipFrom"),
|
|
968
|
+
"ipTo": _value_or_current(ip_to, current, "ipTo"),
|
|
969
|
+
}
|
|
970
|
+
_ = ws_api.send_request(self, ws_type, ws_topic, protocol_data)
|
|
971
|
+
|
|
972
|
+
@override
|
|
973
|
+
def get_netio_push_api_state(self) -> dict[str, Any]:
|
|
974
|
+
self.logger.debug(f"Getting Netio Push API state on device {self.host}")
|
|
975
|
+
ws_type = "SUBSCRIBE"
|
|
976
|
+
ws_topic = "protocols/push/config"
|
|
977
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
978
|
+
return response["data"]
|
|
979
|
+
|
|
980
|
+
@override
|
|
981
|
+
def set_netio_push_api_state(
|
|
982
|
+
self,
|
|
983
|
+
protocol_enabled: bool | None = None,
|
|
984
|
+
url: str | None = None,
|
|
985
|
+
push_protocol: str | None = None,
|
|
986
|
+
delta: int | None = None,
|
|
987
|
+
period: int | None = None,
|
|
988
|
+
) -> None:
|
|
989
|
+
ws_type = "SET"
|
|
990
|
+
ws_topic = "protocols/push/config"
|
|
991
|
+
|
|
992
|
+
self.logger.debug(f"Setting Netio Push API state on device {self.host}")
|
|
993
|
+
if push_protocol is not None and push_protocol.upper() not in ("JSON", "XML"):
|
|
994
|
+
raise InvalidParameterValueError(
|
|
995
|
+
f"push_protocol must be 'json' or 'xml', got {push_protocol!r}"
|
|
996
|
+
)
|
|
997
|
+
current = self._require_config(
|
|
998
|
+
self.get_netio_push_api_state(), "Netio Push API"
|
|
999
|
+
)
|
|
1000
|
+
protocol_data: dict[str, Any] = {
|
|
1001
|
+
"enable": _value_or_current(protocol_enabled, current, "enable"),
|
|
1002
|
+
"url": _value_or_current(url, current, "url"),
|
|
1003
|
+
"delta": _value_or_current(delta, current, "delta"),
|
|
1004
|
+
"period": _value_or_current(period, current, "period"),
|
|
1005
|
+
}
|
|
1006
|
+
# The device doesn't report the push format, so only send it when given.
|
|
1007
|
+
if push_protocol is not None:
|
|
1008
|
+
protocol_data["protocol"] = push_protocol.upper()
|
|
1009
|
+
_ = ws_api.send_request(self, ws_type, ws_topic, protocol_data)
|
|
1010
|
+
|
|
1011
|
+
@override
|
|
1012
|
+
def get_snmp_api_state(self) -> dict[str, Any]:
|
|
1013
|
+
self.logger.debug(f"Getting SNMP API state on device {self.host}")
|
|
1014
|
+
ws_type = "SUBSCRIBE"
|
|
1015
|
+
ws_topic = "protocols/snmp/config"
|
|
1016
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
1017
|
+
return response["data"]
|
|
1018
|
+
|
|
1019
|
+
@override
|
|
1020
|
+
def _set_snmp_api_state(
|
|
1021
|
+
self,
|
|
1022
|
+
protocol_enabled: bool | None,
|
|
1023
|
+
version: str,
|
|
1024
|
+
location: str | None = None,
|
|
1025
|
+
community_read: str | None = None,
|
|
1026
|
+
community_write: str | None = None,
|
|
1027
|
+
security_name: str | None = None,
|
|
1028
|
+
security_level: str | None = None,
|
|
1029
|
+
auth_protocol: str | None = None,
|
|
1030
|
+
auth_key: str | None = None,
|
|
1031
|
+
priv_protocol: str | None = None,
|
|
1032
|
+
priv_key: str | None = None,
|
|
1033
|
+
) -> None:
|
|
1034
|
+
ws_type = "SET"
|
|
1035
|
+
ws_topic = "protocols/snmp/config"
|
|
1036
|
+
|
|
1037
|
+
self.logger.debug(f"Setting SNMP API state on device {self.host}")
|
|
1038
|
+
current = self._require_config(self.get_snmp_api_state(), "SNMP API")
|
|
1039
|
+
ver = "v1-2" if version in ("v1,2c", "v1-2") else "v3"
|
|
1040
|
+
|
|
1041
|
+
request_data = {
|
|
1042
|
+
"enable": _value_or_current(protocol_enabled, current, "enable"),
|
|
1043
|
+
"location": _value_or_current(location, current, "location"),
|
|
1044
|
+
"communityRead": _value_or_current(
|
|
1045
|
+
community_read, current, "communityRead"
|
|
1046
|
+
),
|
|
1047
|
+
"communityWrite": _value_or_current(
|
|
1048
|
+
community_write, current, "communityWrite"
|
|
1049
|
+
),
|
|
1050
|
+
"version": ver,
|
|
1051
|
+
"securityName": _value_or_current(security_name, current, "securityName"),
|
|
1052
|
+
"authAlgo": _value_or_current(auth_protocol, current, "authAlgo"),
|
|
1053
|
+
"authKey": _value_or_current(auth_key, current, "authKey"),
|
|
1054
|
+
"encryptAlgo": _value_or_current(priv_protocol, current, "encryptAlgo"),
|
|
1055
|
+
"encryptKey": _value_or_current(priv_key, current, "encryptKey"),
|
|
1056
|
+
"securityLevel": _value_or_current(
|
|
1057
|
+
security_level, current, "securityLevel"
|
|
1058
|
+
),
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
_ = ws_api.send_request(self, ws_type, ws_topic, request_data)
|
|
1062
|
+
|
|
1063
|
+
@override
|
|
1064
|
+
def set_snmp_v1_2_api_state(
|
|
1065
|
+
self,
|
|
1066
|
+
protocol_enabled: bool | None = None,
|
|
1067
|
+
location: str | None = None,
|
|
1068
|
+
community_read: str | None = None,
|
|
1069
|
+
community_write: str | None = None,
|
|
1070
|
+
) -> None:
|
|
1071
|
+
self.logger.debug(f"Setting SNMP v1,2c API state on device {self.host}")
|
|
1072
|
+
self._set_snmp_api_state(
|
|
1073
|
+
protocol_enabled=protocol_enabled,
|
|
1074
|
+
version="v1-2",
|
|
1075
|
+
location=location,
|
|
1076
|
+
community_read=community_read,
|
|
1077
|
+
community_write=community_write,
|
|
1078
|
+
)
|
|
1079
|
+
|
|
1080
|
+
@override
|
|
1081
|
+
def set_snmp_v3_api_state(
|
|
1082
|
+
self,
|
|
1083
|
+
protocol_enabled: bool | None = None,
|
|
1084
|
+
location: str | None = None,
|
|
1085
|
+
security_name: str | None = None,
|
|
1086
|
+
security_level: str | None = None,
|
|
1087
|
+
auth_protocol: str | None = None,
|
|
1088
|
+
auth_key: str | None = None,
|
|
1089
|
+
priv_protocol: str | None = None,
|
|
1090
|
+
priv_key: str | None = None,
|
|
1091
|
+
) -> None:
|
|
1092
|
+
self.logger.debug(f"Setting SNMP v3 API state on device {self.host}")
|
|
1093
|
+
self._set_snmp_api_state(
|
|
1094
|
+
protocol_enabled=protocol_enabled,
|
|
1095
|
+
version="v3",
|
|
1096
|
+
location=location,
|
|
1097
|
+
security_name=security_name,
|
|
1098
|
+
security_level=security_level,
|
|
1099
|
+
auth_protocol=auth_protocol,
|
|
1100
|
+
auth_key=auth_key,
|
|
1101
|
+
priv_protocol=priv_protocol,
|
|
1102
|
+
priv_key=priv_key,
|
|
1103
|
+
)
|
|
1104
|
+
|
|
1105
|
+
def set_json_api_state(
|
|
1106
|
+
self,
|
|
1107
|
+
protocol_enabled: bool | None = None,
|
|
1108
|
+
read_enable: bool | None = None,
|
|
1109
|
+
write_enable: bool | None = None,
|
|
1110
|
+
read_auth: tuple[str, str] | None = None,
|
|
1111
|
+
write_auth: tuple[str, str] | None = None,
|
|
1112
|
+
) -> None:
|
|
1113
|
+
ws_type = "SET"
|
|
1114
|
+
ws_topic = "protocols/json/config"
|
|
1115
|
+
current = self._require_config(self.get_json_api_state(), "JSON API")
|
|
1116
|
+
read_username, read_password = _auth_or_current(read_auth, current, "read")
|
|
1117
|
+
write_username, write_password = _auth_or_current(write_auth, current, "write")
|
|
1118
|
+
|
|
1119
|
+
protocol_data = {
|
|
1120
|
+
"enable": _value_or_current(protocol_enabled, current, "enable"),
|
|
1121
|
+
"port": current["port"],
|
|
1122
|
+
"readOnlyEnable": _value_or_current(read_enable, current, "readOnlyEnable"),
|
|
1123
|
+
"readUsername": read_username,
|
|
1124
|
+
"readPassword": read_password,
|
|
1125
|
+
"readWriteEnable": _value_or_current(
|
|
1126
|
+
write_enable, current, "readWriteEnable"
|
|
1127
|
+
),
|
|
1128
|
+
"writeUsername": write_username,
|
|
1129
|
+
"writePassword": write_password,
|
|
1130
|
+
}
|
|
1131
|
+
self.logger.debug(f"Setting json api state on device {self.host}.")
|
|
1132
|
+
logged_data = {
|
|
1133
|
+
key: "***" if "Password" in key else value
|
|
1134
|
+
for key, value in protocol_data.items()
|
|
1135
|
+
}
|
|
1136
|
+
self.logger.debug(f"JSON configuration: {json.dumps(logged_data, indent=4)}")
|
|
1137
|
+
_ = ws_api.send_request(self, ws_type, ws_topic, protocol_data)
|
|
1138
|
+
|
|
1139
|
+
def get_urlapi_state(self) -> dict[str, Any]:
|
|
1140
|
+
ws_type = "SUBSCRIBE"
|
|
1141
|
+
ws_topic = "protocols/url/config"
|
|
1142
|
+
|
|
1143
|
+
self.logger.debug(f"Getting urlapi state on device {self.host}.")
|
|
1144
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
1145
|
+
return response["data"]
|
|
1146
|
+
|
|
1147
|
+
def set_urlapi_state(
|
|
1148
|
+
self,
|
|
1149
|
+
protocol_enabled: bool | None = None,
|
|
1150
|
+
write_enable: bool | None = None,
|
|
1151
|
+
write_password: str | None = None,
|
|
1152
|
+
) -> None:
|
|
1153
|
+
ws_type = "SET"
|
|
1154
|
+
ws_topic = "protocols/url/config"
|
|
1155
|
+
|
|
1156
|
+
self.logger.debug(f"Setting urlapi state on device {self.host}.")
|
|
1157
|
+
current = self._require_config(self.get_urlapi_state(), "URL API")
|
|
1158
|
+
protocol_data = {
|
|
1159
|
+
"enable": _value_or_current(protocol_enabled, current, "enable"),
|
|
1160
|
+
"port": current["port"],
|
|
1161
|
+
"writeEnable": _value_or_current(write_enable, current, "writeEnable"),
|
|
1162
|
+
"password": _value_or_current(write_password, current, "password"),
|
|
1163
|
+
}
|
|
1164
|
+
_ = ws_api.send_request(self, ws_type, ws_topic, protocol_data)
|
|
1165
|
+
|
|
1166
|
+
def get_measurement(self):
|
|
1167
|
+
ws_type = "SUBSCRIBE"
|
|
1168
|
+
ws_topic = "outputs/measure"
|
|
1169
|
+
|
|
1170
|
+
self.logger.debug(f"Fetching raw measurements from device {self.host}")
|
|
1171
|
+
return ws_api.send_request(self, ws_type, ws_topic)
|
|
1172
|
+
|
|
1173
|
+
def upload_mqtt_client_key(self, key: str) -> None:
|
|
1174
|
+
ws_type = "SET"
|
|
1175
|
+
ws_topic = "protocols/mqtt/clientkeyupload"
|
|
1176
|
+
ws_data: dict[str, Any] = {}
|
|
1177
|
+
|
|
1178
|
+
self.logger.debug(f"Uploading MQTT client key to device {self.host}.")
|
|
1179
|
+
upload_path = ws_api.send_request(self, ws_type, ws_topic, ws_data)["data"][
|
|
1180
|
+
"uploadPath"
|
|
1181
|
+
]
|
|
1182
|
+
_ = esp_api.send_file(self, upload_path, key)
|
|
1183
|
+
sleep(0.1)
|
|
1184
|
+
|
|
1185
|
+
def upload_mqtt_client_certificate(self, cert: str) -> None:
|
|
1186
|
+
ws_type = "SET"
|
|
1187
|
+
ws_topic = "protocols/mqtt/clientcertupload"
|
|
1188
|
+
ws_data: dict[str, Any] = {}
|
|
1189
|
+
|
|
1190
|
+
self.logger.debug(f"Uploading MQTT client certificate to device {self.host}.")
|
|
1191
|
+
upload_path = ws_api.send_request(self, ws_type, ws_topic, ws_data)["data"][
|
|
1192
|
+
"uploadPath"
|
|
1193
|
+
]
|
|
1194
|
+
_ = esp_api.send_file(self, upload_path, cert)
|
|
1195
|
+
sleep(0.1)
|
|
1196
|
+
|
|
1197
|
+
def upload_mqtt_ca_certificate(self, ca: str) -> None:
|
|
1198
|
+
ws_type = "SET"
|
|
1199
|
+
ws_topic = "protocols/mqtt/cacertupload"
|
|
1200
|
+
ws_data: dict[str, Any] = {}
|
|
1201
|
+
|
|
1202
|
+
self.logger.debug(f"Uploading MQTT CA certificate to device {self.host}.")
|
|
1203
|
+
upload_path = ws_api.send_request(self, ws_type, ws_topic, ws_data)["data"][
|
|
1204
|
+
"uploadPath"
|
|
1205
|
+
]
|
|
1206
|
+
_ = esp_api.send_file(self, upload_path, ca)
|
|
1207
|
+
sleep(0.1)
|
|
1208
|
+
|
|
1209
|
+
def get_mqttflex_state(self) -> dict:
|
|
1210
|
+
ws_type = "SUBSCRIBE"
|
|
1211
|
+
ws_topic = "protocols/mqtt/config"
|
|
1212
|
+
|
|
1213
|
+
self.logger.debug(f"Getting MQTT flex state on device {self.host}")
|
|
1214
|
+
return ws_api.send_request(self, ws_type, ws_topic)["data"]
|
|
1215
|
+
|
|
1216
|
+
@override
|
|
1217
|
+
def get_outputs_data(self) -> list[dict[str, Any]]:
|
|
1218
|
+
ws_type = "SUBSCRIBE"
|
|
1219
|
+
ws_topic = "outputs/measure"
|
|
1220
|
+
|
|
1221
|
+
self.logger.debug(f"Fetching outputs measurement data from device {self.host}")
|
|
1222
|
+
return ws_api.send_request(self, ws_type, ws_topic)["data"]["items"]
|
|
1223
|
+
|
|
1224
|
+
@override
|
|
1225
|
+
def get_output_states(self) -> list[tuple[int, bool]]:
|
|
1226
|
+
self.logger.debug(f"Fetching output states from device {self.host}")
|
|
1227
|
+
output_data = self.get_outputs_data()
|
|
1228
|
+
output_states = []
|
|
1229
|
+
for output in output_data:
|
|
1230
|
+
output_states.append(
|
|
1231
|
+
(int(output["id"]), True if output["state"] == "on" else False)
|
|
1232
|
+
)
|
|
1233
|
+
return output_states
|
|
1234
|
+
|
|
1235
|
+
@override
|
|
1236
|
+
def get_output_state(self, output_id: int) -> bool:
|
|
1237
|
+
self.logger.debug(f"Fetching output {output_id} state from device {self.host}")
|
|
1238
|
+
return self.get_output_states()[output_id - 1][1] # TODO: Polish
|
|
1239
|
+
|
|
1240
|
+
def set_mqttflex_state(
|
|
1241
|
+
self, state: bool | None = None, config: dict[str, Any] | None = None
|
|
1242
|
+
) -> None:
|
|
1243
|
+
ws_type = "SET"
|
|
1244
|
+
ws_topic = "protocols/mqtt/config"
|
|
1245
|
+
|
|
1246
|
+
self.logger.debug(f"Setting MQTT flex state to {state} on device {self.host}")
|
|
1247
|
+
current = self._require_config(self.get_mqttflex_state(), "MQTT flex")
|
|
1248
|
+
ws_data = {
|
|
1249
|
+
"enable": _value_or_current(state, current, "enable"),
|
|
1250
|
+
# The device returns the config JSON-encoded; only encode a given dict.
|
|
1251
|
+
"config": json.dumps(config) if config is not None else current["config"],
|
|
1252
|
+
}
|
|
1253
|
+
_ = ws_api.send_request(self, ws_type, ws_topic, ws_data)
|
|
1254
|
+
sleep(1)
|
|
1255
|
+
|
|
1256
|
+
def upload_https_private_key(self, keyfile: str) -> None:
|
|
1257
|
+
topic = "system/srvkeyupload"
|
|
1258
|
+
ws_type = "SET"
|
|
1259
|
+
ws_data: dict[str, Any] = {}
|
|
1260
|
+
self.logger.debug(
|
|
1261
|
+
f"Uploading https private key for https to device {self.host}."
|
|
1262
|
+
)
|
|
1263
|
+
with open(keyfile, "rb") as f:
|
|
1264
|
+
endpoint_data = ws_api.send_request(self, ws_type, topic, ws_data)
|
|
1265
|
+
endpoint = endpoint_data["data"]["uploadPath"]
|
|
1266
|
+
esp_api.send_file(self, endpoint, f)
|
|
1267
|
+
|
|
1268
|
+
def upload_https_certificate(self, certfile: str) -> None:
|
|
1269
|
+
topic = "system/srvcertupload"
|
|
1270
|
+
ws_type = "SET"
|
|
1271
|
+
ws_data = {}
|
|
1272
|
+
self.logger.debug("Uploading https certificate for https.")
|
|
1273
|
+
with open(certfile, "rb") as f:
|
|
1274
|
+
endpoint_data = ws_api.send_request(self, ws_type, topic, ws_data)
|
|
1275
|
+
endpoint = endpoint_data["data"]["uploadPath"]
|
|
1276
|
+
esp_api.send_file(self, endpoint, f)
|
|
1277
|
+
|
|
1278
|
+
def export_config(self, save_file: str | None = None) -> dict[str, Any]:
|
|
1279
|
+
ws_type = "SET"
|
|
1280
|
+
ws_topic = "system/cfgexport"
|
|
1281
|
+
ws_data: dict[str, Any] = {}
|
|
1282
|
+
|
|
1283
|
+
self.logger.debug(f"Exporting configuration from device {self.host}")
|
|
1284
|
+
config = ws_api.send_request(self, ws_type, ws_topic, ws_data)
|
|
1285
|
+
if save_file:
|
|
1286
|
+
with open(save_file, "w") as file:
|
|
1287
|
+
json.dump(config["data"]["config"], file)
|
|
1288
|
+
return config["data"]["config"]
|
|
1289
|
+
|
|
1290
|
+
def import_config(self, file, **kwargs) -> None:
|
|
1291
|
+
ws_type = "SET"
|
|
1292
|
+
ws_topic = "system/cfgimport"
|
|
1293
|
+
ws_data: dict[str, Any] = {}
|
|
1294
|
+
|
|
1295
|
+
if "can_alter_settings" not in self.user_permissions:
|
|
1296
|
+
raise PermissionError(
|
|
1297
|
+
"You don't have permission to alter settings on this device."
|
|
1298
|
+
)
|
|
1299
|
+
if self._ka_thread:
|
|
1300
|
+
self._ka_thread.cancel()
|
|
1301
|
+
self._ka_thread.join()
|
|
1302
|
+
self.logger.debug(f"Importing configuration to device {self.host}")
|
|
1303
|
+
data = json.load(file)
|
|
1304
|
+
encoded_data = json.dumps(data).encode("utf-8")
|
|
1305
|
+
ws_api.send_request(self, ws_type, ws_topic, ws_data)
|
|
1306
|
+
esp_api.send_file(self, "/cfgimport", encoded_data)
|
|
1307
|
+
self.logger.info(
|
|
1308
|
+
f"Imported configuration from {file.name}, device {self.host} is restarting..."
|
|
1309
|
+
)
|
|
1310
|
+
sleep(kwargs.get("sleep_time", 15))
|
|
1311
|
+
username = kwargs.get("username", self.username)
|
|
1312
|
+
password = kwargs.get("password", self.password)
|
|
1313
|
+
|
|
1314
|
+
if kwargs.get("login", True):
|
|
1315
|
+
self._login_new(username, password)
|
|
1316
|
+
if self._ka_thread:
|
|
1317
|
+
self._keep_alive()
|
|
1318
|
+
|
|
1319
|
+
@override
|
|
1320
|
+
def update_firmware(self, file: os.PathLike[AnyStr] | BytesIO) -> NETIODevice:
|
|
1321
|
+
if "can_alter_settings" not in self.user_permissions:
|
|
1322
|
+
raise PermissionError(
|
|
1323
|
+
"You don't have permission to alter settings on this device."
|
|
1324
|
+
)
|
|
1325
|
+
if self._ka_thread:
|
|
1326
|
+
self._ka_thread.cancel()
|
|
1327
|
+
self._ka_thread.join()
|
|
1328
|
+
|
|
1329
|
+
pre_reconnect_wait = 20
|
|
1330
|
+
if self.supported_features["wifi"] == "yes":
|
|
1331
|
+
wifi_settings = self.get_wifi_settings()
|
|
1332
|
+
if (
|
|
1333
|
+
wifi_settings["mode"] == "client"
|
|
1334
|
+
and wifi_settings["client"]["status"] == "Connected"
|
|
1335
|
+
):
|
|
1336
|
+
pre_reconnect_wait = 50
|
|
1337
|
+
self.logger.debug(
|
|
1338
|
+
"Increased wait time after fimrware update due to active Wi-Fi connection."
|
|
1339
|
+
)
|
|
1340
|
+
|
|
1341
|
+
if esp_api.check_connectivity(self) > (pre_reconnect_wait / 10.0):
|
|
1342
|
+
pre_reconnect_wait = pre_reconnect_wait * 2
|
|
1343
|
+
self.logger.debug(
|
|
1344
|
+
"Increased wait time after firmware update due to poor connection quality."
|
|
1345
|
+
)
|
|
1346
|
+
|
|
1347
|
+
_ = esp_api.send_request(self, "prepFwUpgrade")
|
|
1348
|
+
_ = esp_api.send_file(self, "/upload/firmware", file)
|
|
1349
|
+
try:
|
|
1350
|
+
_ = esp_api.send_request(self, "startUpgrade", close=True)
|
|
1351
|
+
except CommunicationError:
|
|
1352
|
+
self.logger.warn(
|
|
1353
|
+
f"Device {self.host} couldn't verify firmware update process beginning, this should be harmless if the device connects, waiting for connection."
|
|
1354
|
+
)
|
|
1355
|
+
self.logger.debug(
|
|
1356
|
+
f"Uploaded firmware {file.name}, device {self.host} might be unresponsive for a while."
|
|
1357
|
+
)
|
|
1358
|
+
|
|
1359
|
+
sleep(pre_reconnect_wait)
|
|
1360
|
+
|
|
1361
|
+
self.logger.debug(
|
|
1362
|
+
f"Retrying connection to device {self.host} after updating firmware to {file.name}."
|
|
1363
|
+
)
|
|
1364
|
+
device_response_time = esp_api.check_connectivity(self)
|
|
1365
|
+
retry_limit = 3 if device_response_time == -1 else 0
|
|
1366
|
+
for _ in range(0, retry_limit):
|
|
1367
|
+
device_response_time = esp_api.check_connectivity(self)
|
|
1368
|
+
|
|
1369
|
+
if device_response_time == -1:
|
|
1370
|
+
raise CommunicationError(
|
|
1371
|
+
"Device couldn't establish connection after firmware update."
|
|
1372
|
+
)
|
|
1373
|
+
|
|
1374
|
+
updated_instance = None
|
|
1375
|
+
if type(self.netio_manager) is NetioManager:
|
|
1376
|
+
updated_instance = self.netio_manager.update_device(self)
|
|
1377
|
+
|
|
1378
|
+
if not updated_instance:
|
|
1379
|
+
raise CommunicationError("Coudln't get updated instance.")
|
|
1380
|
+
|
|
1381
|
+
return updated_instance
|
|
1382
|
+
|
|
1383
|
+
def get_features(self) -> dict[str, Any]:
|
|
1384
|
+
self.logger.debug(f"Fetching supported features from device {self.host}")
|
|
1385
|
+
return self.get_system_info()
|
|
1386
|
+
|
|
1387
|
+
@override
|
|
1388
|
+
def ping(self) -> bool:
|
|
1389
|
+
self.logger.warning(
|
|
1390
|
+
"Device pinging is not supported on 5.0.x to 5.1.x, please update your device."
|
|
1391
|
+
)
|
|
1392
|
+
return True
|
|
1393
|
+
|
|
1394
|
+
@override
|
|
1395
|
+
def get_system_log(self) -> list[dict[str, Any]]:
|
|
1396
|
+
ws_topic = "log/messages"
|
|
1397
|
+
ws_type = "SUBSCRIBE"
|
|
1398
|
+
ws_data = None
|
|
1399
|
+
self.logger.debug(f"Fetching system log messages from device {self.host}.")
|
|
1400
|
+
|
|
1401
|
+
log_messages: list[dict[str, Any]] = ws_api.send_request(
|
|
1402
|
+
self, ws_type, ws_topic, ws_data
|
|
1403
|
+
)["data"]
|
|
1404
|
+
return log_messages
|
|
1405
|
+
|
|
1406
|
+
@override
|
|
1407
|
+
def set_wifi_settings(self, ssid: str, password: str) -> None:
|
|
1408
|
+
ws_topic = "network/wifi/config"
|
|
1409
|
+
ws_type = "SET"
|
|
1410
|
+
ws_data = {
|
|
1411
|
+
"ssid": ssid,
|
|
1412
|
+
"secured": True if password else False,
|
|
1413
|
+
"password": password,
|
|
1414
|
+
}
|
|
1415
|
+
self.logger.debug(f"Sending Wi-Fi configuration to {self.host}")
|
|
1416
|
+
_ = ws_api.send_request(self, ws_type, ws_topic, ws_data)
|
|
1417
|
+
|
|
1418
|
+
@override
|
|
1419
|
+
def get_wifi_settings(self) -> dict[str, Any]:
|
|
1420
|
+
ws_topic = "network/wifi/config"
|
|
1421
|
+
ws_type = "SUBSCRIBE"
|
|
1422
|
+
ws_data = None
|
|
1423
|
+
self.logger.debug(f"Fetching Wi-Fi configuration from {self.host}")
|
|
1424
|
+
return ws_api.send_request(self, ws_type, ws_topic, ws_data)["data"]
|
|
1425
|
+
|
|
1426
|
+
@override
|
|
1427
|
+
def set_wifi_static_address(
|
|
1428
|
+
self,
|
|
1429
|
+
address: str | None = None,
|
|
1430
|
+
net_mask: str | None = None,
|
|
1431
|
+
gateway: str | None = None,
|
|
1432
|
+
dns_server: str | None = None,
|
|
1433
|
+
hostname: str | None = None,
|
|
1434
|
+
) -> None:
|
|
1435
|
+
ws_type = "SUBSCRIBE"
|
|
1436
|
+
ws_topic = "network/netwifi/config"
|
|
1437
|
+
|
|
1438
|
+
self.logger.debug(f"Setting Wi-Fi static address on device {self.host}")
|
|
1439
|
+
if hostname is not None:
|
|
1440
|
+
self.logger.warning(
|
|
1441
|
+
f"The hostname of device {self.host} is taken from the device name "
|
|
1442
|
+
"on 5.0.0+ firmware and was not changed, use rename_device to change it"
|
|
1443
|
+
)
|
|
1444
|
+
netwifi_res = ws_api.send_request(self, ws_type, ws_topic)
|
|
1445
|
+
netwifi_data = self._require_config(
|
|
1446
|
+
netwifi_res.get("data", {}), "Wi-Fi network"
|
|
1447
|
+
)
|
|
1448
|
+
if netwifi_data["networkMode"] != "static" and None in (
|
|
1449
|
+
address,
|
|
1450
|
+
net_mask,
|
|
1451
|
+
gateway,
|
|
1452
|
+
):
|
|
1453
|
+
raise InvalidParameterValueError(
|
|
1454
|
+
"address, net_mask and gateway are required when switching Wi-Fi "
|
|
1455
|
+
"from DHCP to a static address"
|
|
1456
|
+
)
|
|
1457
|
+
netwifi_data["networkMode"] = "static"
|
|
1458
|
+
if address is not None:
|
|
1459
|
+
netwifi_data["ip"] = address
|
|
1460
|
+
if net_mask is not None:
|
|
1461
|
+
netwifi_data["netmask"] = net_mask
|
|
1462
|
+
if gateway is not None:
|
|
1463
|
+
netwifi_data["gateway"] = gateway
|
|
1464
|
+
if dns_server is not None:
|
|
1465
|
+
netwifi_data["dns"] = dns_server
|
|
1466
|
+
|
|
1467
|
+
ws_type = "SET"
|
|
1468
|
+
_ = ws_api.send_request(self, ws_type, ws_topic, netwifi_data)
|
|
1469
|
+
|
|
1470
|
+
@override
|
|
1471
|
+
def get_version_revision(self) -> str:
|
|
1472
|
+
self.logger.debug(f"Fetching firmware revision from device {self.host}")
|
|
1473
|
+
return self.get_system_info()["fwRevision"]
|
|
1474
|
+
|
|
1475
|
+
@override
|
|
1476
|
+
def set_periodic_restart(
|
|
1477
|
+
self, enable: bool | None = None, restart_period: int | None = None
|
|
1478
|
+
) -> None:
|
|
1479
|
+
self.logger.debug(f"Setting periodic restart on device {self.host}")
|
|
1480
|
+
self.set_system_settings(periodic_restart=enable, restart_period=restart_period)
|
|
1481
|
+
|
|
1482
|
+
@override
|
|
1483
|
+
def rename_device(self, device_name: str) -> None:
|
|
1484
|
+
self.logger.debug(f"Renaming device {self.host} to {device_name}")
|
|
1485
|
+
self.set_system_settings(device_name=device_name)
|
|
1486
|
+
|
|
1487
|
+
@override
|
|
1488
|
+
def set_system_settings(
|
|
1489
|
+
self,
|
|
1490
|
+
device_name: str | None = None,
|
|
1491
|
+
port: int | None = None,
|
|
1492
|
+
periodic_restart: bool | None = None,
|
|
1493
|
+
restart_period: int | None = None,
|
|
1494
|
+
) -> None:
|
|
1495
|
+
ws_type = "SUBSCRIBE"
|
|
1496
|
+
ws_topic = "system/config"
|
|
1497
|
+
|
|
1498
|
+
self.logger.debug(f"Setting system settings on device {self.host}")
|
|
1499
|
+
if port is not None:
|
|
1500
|
+
self.logger.warning(
|
|
1501
|
+
f"The port of device {self.host} is not part of the system settings "
|
|
1502
|
+
"on 5.0.0+ firmware and was not changed, "
|
|
1503
|
+
"use set_server_settings to change it"
|
|
1504
|
+
)
|
|
1505
|
+
cfg_res = ws_api.send_request(self, ws_type, ws_topic)
|
|
1506
|
+
cfg_data = self._require_config(cfg_res.get("data", {}), "system")
|
|
1507
|
+
if device_name is not None:
|
|
1508
|
+
cfg_data["devname"] = device_name
|
|
1509
|
+
if periodic_restart is not None:
|
|
1510
|
+
cfg_data["periodRestartEnable"] = periodic_restart
|
|
1511
|
+
if restart_period is not None:
|
|
1512
|
+
cfg_data["periodRestartPeriod"] = restart_period
|
|
1513
|
+
|
|
1514
|
+
ws_type = "SET"
|
|
1515
|
+
_ = ws_api.send_request(self, ws_type, ws_topic, cfg_data)
|
|
1516
|
+
|
|
1517
|
+
@override
|
|
1518
|
+
def locate(self) -> None:
|
|
1519
|
+
ws_type = "SET"
|
|
1520
|
+
ws_topic = "system/locate"
|
|
1521
|
+
ws_data = {"locate": True}
|
|
1522
|
+
|
|
1523
|
+
self.logger.debug(f"Locating device {self.host}")
|
|
1524
|
+
ws_api.send_request(self, ws_type, ws_topic, ws_data)
|
|
1525
|
+
|
|
1526
|
+
@override
|
|
1527
|
+
def clear_system_log(self) -> None:
|
|
1528
|
+
ws_type = "SET"
|
|
1529
|
+
ws_topic = "log/clear"
|
|
1530
|
+
ws_data: dict[str, Any] = {}
|
|
1531
|
+
|
|
1532
|
+
self.logger.debug(f"Clearing system log on device {self.host}")
|
|
1533
|
+
ws_api.send_request(self, ws_type, ws_topic, ws_data)
|
|
1534
|
+
|
|
1535
|
+
@override
|
|
1536
|
+
def change_user_password(
|
|
1537
|
+
self, username: str, old_password: str, new_password: str
|
|
1538
|
+
) -> None:
|
|
1539
|
+
ws_type = "SUBSCRIBE"
|
|
1540
|
+
ws_topic = f"users/name/{username}/config"
|
|
1541
|
+
|
|
1542
|
+
self.logger.debug(
|
|
1543
|
+
f"Changing password for user {username} on device {self.host}"
|
|
1544
|
+
)
|
|
1545
|
+
user_cfg_res = ws_api.send_request(self, ws_type, ws_topic)
|
|
1546
|
+
user_data = user_cfg_res["data"]
|
|
1547
|
+
public_key = self._ws_helo_data.get("publicKey", "")
|
|
1548
|
+
password_hash = ws_api.generate_password_hash(
|
|
1549
|
+
username, new_password, public_key
|
|
1550
|
+
)
|
|
1551
|
+
user_data["passwordHash"] = password_hash
|
|
1552
|
+
user_data["password"] = ""
|
|
1553
|
+
|
|
1554
|
+
ws_type = "SET"
|
|
1555
|
+
ws_api.send_request(self, ws_type, ws_topic, user_data)
|
|
1556
|
+
|
|
1557
|
+
@override
|
|
1558
|
+
def change_password(self, new_password: str) -> None:
|
|
1559
|
+
self.logger.debug(f"Changing password of the current user on {self.host}")
|
|
1560
|
+
self.change_user_password(self.username, "", new_password)
|
|
1561
|
+
|
|
1562
|
+
@override
|
|
1563
|
+
def get_cloud_state(self) -> dict[str, Any]:
|
|
1564
|
+
ws_type = "SUBSCRIBE"
|
|
1565
|
+
ws_topic = "protocols/cloud"
|
|
1566
|
+
|
|
1567
|
+
self.logger.debug(f"Getting cloud state on device {self.host}")
|
|
1568
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
1569
|
+
return response["data"]
|
|
1570
|
+
|
|
1571
|
+
@override
|
|
1572
|
+
def set_cloud_state(self, state: bool) -> None:
|
|
1573
|
+
ws_type = "SET"
|
|
1574
|
+
ws_topic = "protocols/cloud"
|
|
1575
|
+
|
|
1576
|
+
self.logger.debug(f"Setting cloud state to {state} on device {self.host}")
|
|
1577
|
+
ws_data = self.get_cloud_state()
|
|
1578
|
+
ws_data["enabled"] = state
|
|
1579
|
+
ws_api.send_request(self, ws_type, ws_topic, ws_data)
|
|
1580
|
+
if state:
|
|
1581
|
+
sleep(CLOUD_DEFAULT_CONNECTION_WAIT)
|
|
1582
|
+
|
|
1583
|
+
@override
|
|
1584
|
+
def register_to_cloud(self, token: str) -> None:
|
|
1585
|
+
ws_type = "SET"
|
|
1586
|
+
ws_topic = "protocols/cloud/cmd"
|
|
1587
|
+
ws_data = {"action": "register", "token": token, "server": ""}
|
|
1588
|
+
|
|
1589
|
+
self.logger.debug(
|
|
1590
|
+
f"Registering to cloud with token {token} on device {self.host}"
|
|
1591
|
+
)
|
|
1592
|
+
ws_api.send_request(self, ws_type, ws_topic, ws_data)
|
|
1593
|
+
sleep(CLOUD_ACTION_COMMUNICATION_DELAY)
|
|
1594
|
+
|
|
1595
|
+
@override
|
|
1596
|
+
def set_on_premise(self, url: str) -> None:
|
|
1597
|
+
ws_type = "SET"
|
|
1598
|
+
ws_topic = "protocols/cloud/cmd"
|
|
1599
|
+
ws_data = {"action": "setOnPremis", "token": "", "server": url}
|
|
1600
|
+
|
|
1601
|
+
self.logger.debug(
|
|
1602
|
+
f"Setting on-premise cloud server to {url} on device {self.host}"
|
|
1603
|
+
)
|
|
1604
|
+
ws_api.send_request(self, ws_type, ws_topic, ws_data)
|
|
1605
|
+
sleep(CLOUD_ACTION_COMMUNICATION_DELAY)
|
|
1606
|
+
|
|
1607
|
+
@override
|
|
1608
|
+
def netio_push_api_push_now(self) -> None:
|
|
1609
|
+
raise NotImplementedError(
|
|
1610
|
+
"netio_push_api_push_now is not implemented on 5.0.0+ devices"
|
|
1611
|
+
)
|
|
1612
|
+
|
|
1613
|
+
def _cleanup(self) -> None:
|
|
1614
|
+
try:
|
|
1615
|
+
if self._ka_thread:
|
|
1616
|
+
self._ka_thread.cancel()
|
|
1617
|
+
self._ka_thread.join()
|
|
1618
|
+
self.logger.debug("Disabled keep-alive thread.")
|
|
1619
|
+
except AttributeError:
|
|
1620
|
+
pass # No need to clean up a non existant thread
|
|
1621
|
+
# TODO: Logout to be sure here even though destroying the connection should be sufficient
|
|
1622
|
+
self.ws = None
|
|
1623
|
+
|
|
1624
|
+
@override
|
|
1625
|
+
def get_input_list(self) -> dict[str, Any]:
|
|
1626
|
+
ws_type = "SUBSCRIBE"
|
|
1627
|
+
ws_topic = "inputs/list"
|
|
1628
|
+
|
|
1629
|
+
self.logger.debug(f"Fetching input list from device {self.host}")
|
|
1630
|
+
response = ws_api.send_request(self, ws_type, ws_topic)["data"]
|
|
1631
|
+
return response
|
|
1632
|
+
|
|
1633
|
+
@override
|
|
1634
|
+
def get_input_data(self, input_id: int) -> dict[str, Any]:
|
|
1635
|
+
self.logger.debug(f"Fetching input {input_id} data from device {self.host}")
|
|
1636
|
+
input_list = self.get_input_list()
|
|
1637
|
+
if input_id > len(input_list["items"]):
|
|
1638
|
+
raise InvalidSocketIndex(
|
|
1639
|
+
f"The device doesn't support {input_id} inputs. The input range is <0;{len(input_list['items'])}>"
|
|
1640
|
+
)
|
|
1641
|
+
else:
|
|
1642
|
+
return input_list["items"][input_id - 1]
|
|
1643
|
+
|
|
1644
|
+
@override
|
|
1645
|
+
def get_system_datetime(self) -> dict[str, Any]:
|
|
1646
|
+
ws_type = "SUBSCRIBE"
|
|
1647
|
+
ws_topic = "system/datetime"
|
|
1648
|
+
|
|
1649
|
+
self.logger.debug(f"Fetching system date/time on device {self.host}")
|
|
1650
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
1651
|
+
return response.get("data", {})
|
|
1652
|
+
|
|
1653
|
+
# TODO: Timezone documentation
|
|
1654
|
+
@override
|
|
1655
|
+
def set_system_datetime(
|
|
1656
|
+
self,
|
|
1657
|
+
ntp_enabled: bool | None = None,
|
|
1658
|
+
ntp_server: str | None = None,
|
|
1659
|
+
timezone: str | None = None,
|
|
1660
|
+
time: int | None = None,
|
|
1661
|
+
) -> None:
|
|
1662
|
+
ws_type = "SET"
|
|
1663
|
+
ws_topic = "system/datetime"
|
|
1664
|
+
|
|
1665
|
+
self.logger.debug(f"Setting system date/time on device {self.host}")
|
|
1666
|
+
current = self._require_config(self.get_system_datetime(), "date/time")
|
|
1667
|
+
ws_data: dict[str, Any] = {
|
|
1668
|
+
"ntpEnabled": _value_or_current(ntp_enabled, current, "ntpEnabled"),
|
|
1669
|
+
"ntpServer": _value_or_current(ntp_server, current, "ntpServer"),
|
|
1670
|
+
"timezone": _value_or_current(timezone, current, "timezone"),
|
|
1671
|
+
"time": _value_or_current(time, current, "time"),
|
|
1672
|
+
}
|
|
1673
|
+
_ = ws_api.send_request(self, ws_type, ws_topic, ws_data)
|
|
1674
|
+
|
|
1675
|
+
@override
|
|
1676
|
+
def system_reset(self) -> None:
|
|
1677
|
+
ws_type = "SET"
|
|
1678
|
+
ws_topic = "system/reset"
|
|
1679
|
+
ws_data = {"reset": True}
|
|
1680
|
+
|
|
1681
|
+
self.logger.debug(f"Triggering system reset (reboot) on device {self.host}")
|
|
1682
|
+
ws_api.send_request(self, ws_type, ws_topic, ws_data)
|
|
1683
|
+
sleep(DEVICE_RESET_GRACE_PERIOD)
|
|
1684
|
+
|
|
1685
|
+
@override
|
|
1686
|
+
def get_ethernet_settings(self) -> dict[str, Any]:
|
|
1687
|
+
ws_type = "SUBSCRIBE"
|
|
1688
|
+
ws_topic = "network/ethernet/config"
|
|
1689
|
+
|
|
1690
|
+
self.logger.debug(f"Fetching Ethernet settings on device {self.host}")
|
|
1691
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
1692
|
+
return response.get("data", {})
|
|
1693
|
+
|
|
1694
|
+
@override
|
|
1695
|
+
def set_ethernet_settings(
|
|
1696
|
+
self,
|
|
1697
|
+
network_mode: str | None = None,
|
|
1698
|
+
ip: str | None = None,
|
|
1699
|
+
netmask: str | None = None,
|
|
1700
|
+
gateway: str | None = None,
|
|
1701
|
+
dns: str | None = None,
|
|
1702
|
+
) -> None:
|
|
1703
|
+
ws_type = "SET"
|
|
1704
|
+
ws_topic = "network/ethernet/config"
|
|
1705
|
+
|
|
1706
|
+
self.logger.debug(f"Setting Ethernet configuration on device {self.host}")
|
|
1707
|
+
current = self._require_config(self.get_ethernet_settings(), "Ethernet")
|
|
1708
|
+
if (
|
|
1709
|
+
network_mode == "static"
|
|
1710
|
+
and current["networkMode"] != "static"
|
|
1711
|
+
and None in (ip, netmask, gateway)
|
|
1712
|
+
):
|
|
1713
|
+
raise InvalidParameterValueError(
|
|
1714
|
+
"ip, netmask and gateway are required when switching Ethernet "
|
|
1715
|
+
"from DHCP to a static address"
|
|
1716
|
+
)
|
|
1717
|
+
ws_data: dict[str, Any] = {
|
|
1718
|
+
"mac": current["mac"],
|
|
1719
|
+
"networkMode": _value_or_current(network_mode, current, "networkMode"),
|
|
1720
|
+
"ip": _value_or_current(ip, current, "ip"),
|
|
1721
|
+
"netmask": _value_or_current(netmask, current, "netmask"),
|
|
1722
|
+
"gateway": _value_or_current(gateway, current, "gateway"),
|
|
1723
|
+
"dns": _value_or_current(dns, current, "dns"),
|
|
1724
|
+
}
|
|
1725
|
+
_ = ws_api.send_request(self, ws_type, ws_topic, ws_data)
|
|
1726
|
+
|
|
1727
|
+
@override
|
|
1728
|
+
def get_ethernet_status(self) -> dict[str, Any]:
|
|
1729
|
+
ws_type = "SUBSCRIBE"
|
|
1730
|
+
ws_topic = "network/ethernet/status"
|
|
1731
|
+
|
|
1732
|
+
self.logger.debug(f"Fetching Ethernet status on device {self.host}")
|
|
1733
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
1734
|
+
return response.get("data", {})
|
|
1735
|
+
|
|
1736
|
+
@override
|
|
1737
|
+
def get_wifi_status(self) -> dict[str, Any]:
|
|
1738
|
+
ws_type = "SUBSCRIBE"
|
|
1739
|
+
ws_topic = "network/wifi/status"
|
|
1740
|
+
|
|
1741
|
+
self.logger.debug(f"Fetching Wi-Fi status on device {self.host}")
|
|
1742
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
1743
|
+
return response.get("data", {})
|
|
1744
|
+
|
|
1745
|
+
@override
|
|
1746
|
+
def get_server_settings(self) -> dict[str, Any]:
|
|
1747
|
+
ws_type = "SUBSCRIBE"
|
|
1748
|
+
ws_topic = "network/servers/config"
|
|
1749
|
+
|
|
1750
|
+
self.logger.debug(f"Fetching web server settings on device {self.host}")
|
|
1751
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
1752
|
+
return response.get("data", {})
|
|
1753
|
+
|
|
1754
|
+
@override
|
|
1755
|
+
def set_server_settings(
|
|
1756
|
+
self,
|
|
1757
|
+
http_enable: bool | None = None,
|
|
1758
|
+
http_port: int | None = None,
|
|
1759
|
+
https_enable: bool | None = None,
|
|
1760
|
+
https_port: int | None = None,
|
|
1761
|
+
) -> None:
|
|
1762
|
+
ws_type = "SET"
|
|
1763
|
+
ws_topic = "network/servers/config"
|
|
1764
|
+
# TODO: make sure to change the https settings if it's toggled to https and vice versa
|
|
1765
|
+
|
|
1766
|
+
self.logger.debug(f"Setting web server settings on device {self.host}")
|
|
1767
|
+
current = self._require_config(self.get_server_settings(), "web server")
|
|
1768
|
+
ws_data: dict[str, Any] = {
|
|
1769
|
+
"httpEnable": _value_or_current(http_enable, current, "httpEnable"),
|
|
1770
|
+
"httpPort": _value_or_current(http_port, current, "httpPort"),
|
|
1771
|
+
"httpsEnable": _value_or_current(https_enable, current, "httpsEnable"),
|
|
1772
|
+
"httpsPort": _value_or_current(https_port, current, "httpsPort"),
|
|
1773
|
+
}
|
|
1774
|
+
_ = ws_api.send_request(self, ws_type, ws_topic, ws_data)
|
|
1775
|
+
|
|
1776
|
+
@override
|
|
1777
|
+
def get_global_measurement(self) -> dict[str, Any]:
|
|
1778
|
+
ws_type = "SUBSCRIBE"
|
|
1779
|
+
ws_topic = "outputs/globalmeasure"
|
|
1780
|
+
|
|
1781
|
+
self.logger.debug(f"Fetching global measurements on device {self.host}")
|
|
1782
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
1783
|
+
return response.get("data", {}).get("measure", {})
|
|
1784
|
+
|
|
1785
|
+
@override
|
|
1786
|
+
def get_outputs_list(self) -> list[dict[str, Any]]:
|
|
1787
|
+
ws_type = "SUBSCRIBE"
|
|
1788
|
+
ws_topic = "outputs/list"
|
|
1789
|
+
|
|
1790
|
+
self.logger.debug(f"Fetching outputs overview list on device {self.host}")
|
|
1791
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
1792
|
+
return response.get("data", {}).get("items", [])
|
|
1793
|
+
|
|
1794
|
+
@override
|
|
1795
|
+
def get_firmware_updates_list(self) -> list[dict[str, Any]]:
|
|
1796
|
+
ws_type = "SET"
|
|
1797
|
+
ws_topic = "system/firmwarelist"
|
|
1798
|
+
ws_data = {"getFirmwareList": True, "updateFw": ""}
|
|
1799
|
+
|
|
1800
|
+
self.logger.debug(f"Fetching available firmware updates on device {self.host}")
|
|
1801
|
+
ws_api.send_request(self, ws_type, ws_topic, ws_data)
|
|
1802
|
+
|
|
1803
|
+
ws_type = "SUBSCRIBE"
|
|
1804
|
+
ws_topic = "fwupdate/list"
|
|
1805
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
1806
|
+
return response.get("data", {}).get("items", [])
|
|
1807
|
+
|
|
1808
|
+
@override
|
|
1809
|
+
def get_system_lock(self) -> dict[str, Any]:
|
|
1810
|
+
ws_type = "SUBSCRIBE"
|
|
1811
|
+
ws_topic = "system/lock"
|
|
1812
|
+
# TODO: Use for cloud communication verification timing
|
|
1813
|
+
|
|
1814
|
+
self.logger.debug(f"Fetching system lock state on device {self.host}")
|
|
1815
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
1816
|
+
return response.get("data", {})
|
|
1817
|
+
|
|
1818
|
+
@override
|
|
1819
|
+
def get_system_notifications(self) -> list[dict[str, Any]]:
|
|
1820
|
+
ws_type = "SUBSCRIBE"
|
|
1821
|
+
ws_topic = "system/notifications"
|
|
1822
|
+
|
|
1823
|
+
self.logger.debug(f"Fetching system notifications on device {self.host}")
|
|
1824
|
+
response = ws_api.send_request(self, ws_type, ws_topic)
|
|
1825
|
+
data = response.get("data", [])
|
|
1826
|
+
return data if isinstance(data, list) else []
|