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.
@@ -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 []