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,1516 @@
1
+ """
2
+ The implementation of the NETIODevice class for ESP firmware based devices.
3
+ """
4
+
5
+ import atexit
6
+ import json
7
+ import logging
8
+ import sys
9
+ import threading
10
+ from time import sleep
11
+
12
+ if sys.version_info >= (3, 12):
13
+ from typing import IO, Any, AnyStr, Dict, List, Tuple, override
14
+ else:
15
+ from typing import IO, Any, AnyStr, Dict, List, Tuple
16
+
17
+ from typing_extensions import override
18
+ from xml.etree import ElementTree as ET
19
+
20
+ import requests
21
+
22
+ from ..exceptions import (
23
+ CommunicationError,
24
+ ElementNotFound,
25
+ FeatureNotSupported,
26
+ InvalidParameterValueError,
27
+ InvalidSocketIndex,
28
+ ProtocolNotEnabled,
29
+ )
30
+ from ..netio_device import NETIODevice
31
+ from ..NetioManager import NetioManager
32
+ from . import esp_api
33
+
34
+
35
+ class ESPDevice(NETIODevice):
36
+ """
37
+ A base class for ESPDevices based on the 3.x.x firmware.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ host: str,
43
+ username: str,
44
+ password: str,
45
+ sn_number: str,
46
+ hostname: str,
47
+ keep_alive: bool,
48
+ netio_manager: NetioManager | None,
49
+ use_https: bool = False,
50
+ ) -> None:
51
+ super().__init__(
52
+ host,
53
+ username,
54
+ password,
55
+ sn_number,
56
+ hostname,
57
+ keep_alive,
58
+ netio_manager,
59
+ use_https,
60
+ )
61
+ self.logger = logging.getLogger(__name__)
62
+ self.session_id = self.login(username, password)
63
+ self.supported_features = self.get_features()
64
+ self.output_count: int = self.supported_features["outputCount"]
65
+ self.user_permissions = self.get_current_user()["privileges"]
66
+ if keep_alive:
67
+ self._ka_thread = threading.Timer(120, self._keep_alive)
68
+ self._ka_thread.daemon = True
69
+ self._ka_thread.start()
70
+ atexit.register(self._cleanup)
71
+
72
+ # region Session
73
+
74
+ def login(self, username: str, password: str, logout: bool = False) -> str:
75
+ if logout:
76
+ self.logger.debug("Logging out of the current session.")
77
+ self.logout()
78
+
79
+ response = esp_api.send_request(
80
+ self, "login", {"username": username, "password": password}, timeout=10
81
+ )
82
+ if "data" in response.json():
83
+ self.session_id = response.json()["data"]["sessionId"]
84
+ self.logger.debug(
85
+ f"Logged in to device {self.host} with session ID {self.session_id}"
86
+ )
87
+ else:
88
+ raise CommunicationError("Cannot login to device")
89
+ return self.session_id
90
+
91
+ def logout(self) -> None:
92
+ if self.session_id == "":
93
+ self.logger.debug(
94
+ f"Device {self.host} is not logged in, ignoring logout request."
95
+ )
96
+ pass
97
+ esp_api.send_request(self, "logout", {})
98
+ self.session_id = ""
99
+ self.logger.debug(f"Logged out of device {self.host}")
100
+
101
+ def ping(self) -> bool:
102
+ response = esp_api.send_request(self, "ping", {})
103
+ status = "OK" if response.status_code == 200 else response.json()
104
+ self.logger.debug(
105
+ f"Pinging device {self.host}, response: {status}. This should be empty if ok."
106
+ )
107
+ if response.status_code == 200:
108
+ return True
109
+ else:
110
+ return False
111
+
112
+ def _keep_alive(self) -> None:
113
+ self.logger.debug(f"Sending keep-alive to device {self.host}")
114
+ self._ka_thread = threading.Timer(120, self._keep_alive)
115
+ self._ka_thread.daemon = True
116
+ self._ka_thread.start()
117
+ self.ping()
118
+
119
+ # endregion
120
+
121
+ # region Device Information
122
+
123
+ def get_version(self) -> str:
124
+ response = esp_api.send_request(self, "getVersion", {}).json()["data"][
125
+ "version"
126
+ ]
127
+ response = response.split(" ")[0]
128
+ self.logger.debug(f"Received device version: {response}")
129
+ return response
130
+
131
+ def get_version_detailed(self) -> str:
132
+ response = esp_api.send_request(self, "getVersion").json()["data"]
133
+ self.logger.debug(f"Received device version information: {response}")
134
+ return response
135
+
136
+ def get_features(self) -> dict[str, Any]:
137
+ response = esp_api.send_request(self, "getFeatures").json()["data"]
138
+ self.logger.debug(f"Received device supported features: {response}")
139
+ return response
140
+
141
+ # endregion
142
+
143
+ # region Sockets
144
+ # region Socket Control
145
+
146
+ def get_output_state(self, output_id: int) -> bool:
147
+ self._check_socket_index(output_id)
148
+ return self.get_output_states()[output_id - 1][1]
149
+
150
+ def set_output(self, output_id: int, state: bool) -> None:
151
+ if "can_control_outputs" not in self.user_permissions:
152
+ raise PermissionError(
153
+ "You don't have permission to control outputs on this device."
154
+ )
155
+ self._check_socket_index(output_id)
156
+ esp_api.send_request(
157
+ self, "setOutputState", {"output": output_id, "value": state}
158
+ )
159
+ self.logger.debug(
160
+ f"Setting output {output_id} on device {self.host} to {state}."
161
+ )
162
+
163
+ def set_outputs_unified(self, state: bool) -> None:
164
+ for output in range(1, self.output_count + 1):
165
+ self.set_output(output, state)
166
+ sleep(0.1)
167
+
168
+ def rename_output(self, output_id: int, output_name: str) -> None:
169
+ if "can_alter_outputs" not in self.user_permissions:
170
+ raise PermissionError(
171
+ "You don't have permission to alter outputs on this device."
172
+ )
173
+ self._check_socket_index(output_id)
174
+ response = esp_api.send_request(self, "getOutputDetailList")
175
+ json_data = response.json()["data"][output_id - 1]
176
+ interrupt_delay = json_data["interruptDelay"]
177
+ default = json_data["default"]
178
+ pui = json_data["powerUpInterval"]
179
+ request_data = {
180
+ "id": output_id,
181
+ "name": output_name,
182
+ "resetDelay": interrupt_delay,
183
+ "default": default,
184
+ "powerUpInterval": pui,
185
+ }
186
+ self.logger.debug(
187
+ f"Renaming output {output_id} to {output_name} on device {self.host}"
188
+ )
189
+ esp_api.send_request(self, "setOutputBase", request_data)
190
+
191
+ def reset_output(self, output_id: int) -> None:
192
+ if "can_control_outputs" not in self.user_permissions:
193
+ raise PermissionError(
194
+ "You don't have permission to control outputs on this device."
195
+ )
196
+ self._check_socket_index(output_id)
197
+ esp_api.send_request(self, "resetOutputState", {"output": output_id})
198
+ self.logger.debug(f"Resetting output {output_id} on device {self.host}.")
199
+
200
+ def set_output_schedule(
201
+ self, output_id: int, schedule_id: int, enabled: bool | None = None
202
+ ) -> None:
203
+ if enabled is None:
204
+ enabled = True
205
+ if "can_control_outputs" not in self.user_permissions:
206
+ raise PermissionError(
207
+ "You don't have permission to control outputs on this device."
208
+ )
209
+ self._check_socket_index(output_id)
210
+ request_data = {
211
+ "id": output_id,
212
+ "scheduleId": f"{schedule_id}",
213
+ "enable": enabled,
214
+ }
215
+ esp_api.send_request(self, "setOutputSchedule", request_data)
216
+ self.logger.debug(
217
+ f"Setting schedule {schedule_id} on output {output_id}, netio_device: {self.host}."
218
+ )
219
+
220
+ def get_output_schedule(self, output_id: int) -> dict[str, Any]:
221
+ output_data = self.get_output_data(output_id)
222
+ schedule = output_data["schedule"]
223
+ return schedule
224
+
225
+ def get_output_schedule_id(self, output_id: int) -> int:
226
+ output_schedule = self.get_output_schedule(output_id)
227
+ return output_schedule["id"]
228
+
229
+ def set_output_schedule_state(self, output_id: int, schedule_enabled: bool) -> None:
230
+ self.logger.debug(
231
+ f"Setting schedule state {schedule_enabled} on output {output_id} to {schedule_enabled}, "
232
+ f"device {self.host}."
233
+ )
234
+ current_output_schedule = self.get_output_schedule_id(output_id)
235
+ self.set_output_schedule(
236
+ current_output_schedule, current_output_schedule, schedule_enabled
237
+ )
238
+
239
+ def set_output_schedule_by_name(
240
+ self, output_id: int, schedule_name: str, enabled: bool | None = None
241
+ ) -> None:
242
+ schedule_id = self.get_schedule_id(schedule_name)
243
+ self.set_output_schedule(output_id, schedule_id, enabled)
244
+
245
+ # endregion
246
+
247
+ # region Socket Information
248
+
249
+ def get_output_data(self, output_id: int) -> dict[str, Any]:
250
+ self._check_socket_index(output_id)
251
+ output_info = self.get_outputs_data()[output_id - 1]
252
+ return output_info
253
+
254
+ def get_outputs_data(self) -> list[dict[str, Any]]:
255
+ output_data = esp_api.send_request(self, "getOutputDetailList").json()["data"]
256
+ self.logger.debug(
257
+ f"Received output list form device {self.host}: {output_data}"
258
+ )
259
+ return output_data
260
+
261
+ def get_output_states(self) -> list[tuple[int, bool]]:
262
+ outputs = self.get_outputs_data()
263
+ value_list = [(output["outputId"], output["on"]) for output in outputs]
264
+ self.logger.debug(
265
+ f"Received socket states from device {self.host}: {value_list}"
266
+ )
267
+ return value_list
268
+
269
+ # endregion
270
+
271
+ # endregion
272
+
273
+ # region Device Configuration
274
+ # region Network
275
+
276
+ def get_wifi_settings(self) -> dict[str, Any]:
277
+ if self.supported_features["wifi"] == "no":
278
+ raise FeatureNotSupported("Wi-Fi is not supported on this device.")
279
+ response = esp_api.send_request(self, "getWifiSettings")
280
+ self.logger.debug(f"Wi-Fi settings on {self.host}: {response.json()['data']}")
281
+ return response.json()["data"]
282
+
283
+ def set_wifi_settings(self, ssid: str, password: str) -> None:
284
+ if self.supported_features["wifi"] == "no":
285
+ raise FeatureNotSupported("Wi-Fi is not supported on this device.")
286
+ request_data = {"ssid": ssid, "password": password}
287
+ response = esp_api.send_request(self, "setWifiSettings", request_data)
288
+ if response.status_code == 200:
289
+ self.logger.debug(f"Successfully connected to {ssid} on device {self.host}")
290
+ else:
291
+ self.logger.warning(f"Unable to connect to {ssid} on device {self.host}")
292
+
293
+ def set_wifi_static_address(
294
+ self,
295
+ address: str | None = None,
296
+ net_mask: str | None = None,
297
+ gateway: str | None = None,
298
+ dns_server: str | None = None,
299
+ hostname: str | None = None,
300
+ ) -> None:
301
+ self._require_arguments(
302
+ "set_wifi_static_address",
303
+ address=address,
304
+ net_mask=net_mask,
305
+ gateway=gateway,
306
+ dns_server=dns_server,
307
+ hostname=hostname,
308
+ )
309
+ if self.supported_features["wifi"] == "no":
310
+ raise FeatureNotSupported("Wi-Fi is not supported on this device.")
311
+ mac_address = str(self.get_wifi_settings()["mac"])
312
+ request_data = {
313
+ "networkMode": "manual",
314
+ "mac": mac_address,
315
+ "status": "Connected",
316
+ "ipAddress": address,
317
+ "netMask": net_mask,
318
+ "gateway": gateway,
319
+ "dnsServer": dns_server,
320
+ "hostname": hostname,
321
+ }
322
+ response = esp_api.send_request(self, "setNetworkWifi", request_data)
323
+ self.logger.debug(
324
+ f"Setting static address on device {self.host} to {address}, full info: {request_data}"
325
+ )
326
+ if response.status_code == 200:
327
+ self.logger.debug(f"Successfully set static address on device {self.host}")
328
+ else:
329
+ self.logger.debug(f"Unable to set static address on device {self.host}")
330
+
331
+ # endregion
332
+
333
+ def import_config(self, file, **kwargs: Any) -> None:
334
+ if "can_alter_settings" not in self.user_permissions:
335
+ raise PermissionError(
336
+ "You don't have permission to alter settings on this device."
337
+ )
338
+ if self._ka_thread:
339
+ self._ka_thread.cancel()
340
+ self._ka_thread.join()
341
+ data = json.load(file)
342
+ encoded_data = json.dumps(data).encode("utf-8")
343
+ esp_api.send_file(self, "/upload/config", encoded_data)
344
+ self.logger.info(
345
+ f"Imported configuration from {file.name}, device {self.host} is restarting..."
346
+ )
347
+ sleep(kwargs.get("sleep_time", 15))
348
+ username = kwargs.get("username", self.username)
349
+ password = kwargs.get("password", self.password)
350
+
351
+ if kwargs.get("login", True):
352
+ self.login(username, password)
353
+ if self._ka_thread:
354
+ self._keep_alive()
355
+
356
+ def export_config(self, save_file: str | None = None) -> dict[str, Any]:
357
+ response = esp_api.get_file(self, "/files/config.json")
358
+ self.logger.debug(
359
+ f"Exported configuration from device {self.host}, response: {response.json()}"
360
+ )
361
+ if save_file is not None:
362
+ with open(save_file, "w") as f:
363
+ f.write(response.text)
364
+ self.logger.debug(f"Saved configuration to {save_file}")
365
+ return response.json()
366
+
367
+ def update_firmware(self, file) -> NETIODevice:
368
+ if "can_alter_settings" not in self.user_permissions:
369
+ raise PermissionError(
370
+ "You don't have permission to alter settings on this device."
371
+ )
372
+ if self._ka_thread:
373
+ self._ka_thread.cancel()
374
+ self._ka_thread.join()
375
+
376
+ pre_reconnect_wait = 20
377
+ if self.supported_features["wifi"] == "yes":
378
+ wifi_settings = self.get_wifi_settings()
379
+ if (
380
+ wifi_settings["mode"] == "client"
381
+ and wifi_settings["client"]["status"] == "Connected"
382
+ ):
383
+ pre_reconnect_wait = 50
384
+
385
+ if esp_api.check_connectivity(self) > (pre_reconnect_wait / 10.0):
386
+ pre_reconnect_wait = pre_reconnect_wait * 2
387
+
388
+ _ = esp_api.send_request(self, "prepFwUpgrade")
389
+ _ = esp_api.send_file(self, "/upload/firmware", file)
390
+ try:
391
+ _ = esp_api.send_request(self, "startUpgrade", close=True)
392
+ except CommunicationError:
393
+ self.logger.warn(
394
+ f"Device {self.host} couldn't verify firmware update process beginning, this should be harmless if the device connects, waiting for connection."
395
+ )
396
+
397
+ self.logger.debug(
398
+ f"Uploaded firmware {file.name}, device {self.host} might be unresponsive for a while."
399
+ )
400
+ sleep(pre_reconnect_wait)
401
+
402
+ device_response_time = esp_api.check_connectivity(self)
403
+ retry_limit = 3 if device_response_time == -1 else 0
404
+ for _ in range(0, retry_limit):
405
+ device_response_time = esp_api.check_connectivity(self)
406
+
407
+ if device_response_time == -1:
408
+ raise CommunicationError(
409
+ "Device couldn't establish connection after firmware update."
410
+ )
411
+ updated_instance = self.netio_manager.update_device(self)
412
+
413
+ return updated_instance
414
+
415
+ def get_system_info(self) -> dict[str, Any]:
416
+ action = "getSystemInfo"
417
+ self.logger.debug(f"Getting system info for device {self.host}")
418
+ response = esp_api.send_request(self, action)
419
+ return response.json()["data"]
420
+
421
+ def get_uptime(self) -> int:
422
+ system_info = self.get_system_info()
423
+ self.logger.debug(f"Uptime for device {self.host}")
424
+ return system_info["uptime"]
425
+
426
+ def reset_power_consumption_counters(self) -> None:
427
+ action = "resetOutputConsumption"
428
+ self.logger.debug(
429
+ f"Resetting power consumption counters for device {self.host}"
430
+ )
431
+ esp_api.send_request(self, action)
432
+
433
+ def reset_output_consumption_counter(self, output_id: int) -> None:
434
+ # TODO: Implement reset_output_consumption_counter for legacy ESP devices
435
+ raise NotImplementedError(
436
+ "reset_output_consumption_counter is not implemented for legacy ESP devices"
437
+ )
438
+
439
+ def set_system_settings(
440
+ self,
441
+ device_name: str | None = None,
442
+ port: int | None = None,
443
+ periodic_restart: bool | None = None,
444
+ restart_period: int | None = None,
445
+ ) -> None:
446
+ device_system_info = esp_api.send_request(self, "getSystemInfo")
447
+ info_json_data = device_system_info.json()["data"]
448
+ device_name = (
449
+ device_name if device_name is not None else info_json_data["deviceName"]
450
+ )
451
+ port = port if port is not None else info_json_data["port"]
452
+ pr_enable = (
453
+ periodic_restart
454
+ if periodic_restart is not None
455
+ else info_json_data["periodicRestart"]["enable"]
456
+ )
457
+ pr_period = (
458
+ restart_period
459
+ if restart_period is not None
460
+ else info_json_data["periodicRestart"]["period"]
461
+ )
462
+ request_data = {
463
+ "deviceName": device_name,
464
+ "port": port,
465
+ "periodicRestart": {"enable": pr_enable, "period": pr_period},
466
+ }
467
+ self.logger.debug(
468
+ f"Setting system settings on device {device_name}, host {self.host} to {request_data}"
469
+ )
470
+ esp_api.send_request(self, "setSystemConfig", request_data)
471
+
472
+ def rename_device(self, device_name: str) -> None:
473
+ response = esp_api.send_request(self, "getSystemInfo")
474
+ json_data = response.json()["data"]
475
+ port = json_data["port"]
476
+ pr_enable = json_data["periodicRestart"]["enable"]
477
+ pr_period = json_data["periodicRestart"]["period"]
478
+ request_data = {
479
+ "deviceName": device_name,
480
+ "port": port,
481
+ "periodicRestart": {"enable": pr_enable, "period": pr_period},
482
+ }
483
+ self.logger.debug(f"Renaming device to {device_name} on url {self.host}")
484
+ esp_api.send_request(self, "setSystemConfig", request_data)
485
+
486
+ def set_periodic_restart(
487
+ self, enable: bool | None = None, restart_period: int | None = None
488
+ ) -> None:
489
+ self.set_system_settings(periodic_restart=enable, restart_period=restart_period)
490
+
491
+ def locate(self) -> None:
492
+ self.logger.debug(f"Blinking LED on device {self.host}")
493
+ esp_api.send_request(self, "locate")
494
+
495
+ # endregion
496
+
497
+ # region User Management
498
+
499
+ def get_current_user(self) -> dict[str, Any]:
500
+ response = esp_api.send_request(self, "getCurrentUser")
501
+ self.logger.debug(
502
+ f"Received current user information: {response.json()['data']} from device {self.host}"
503
+ )
504
+ return response.json()["data"]
505
+
506
+ def get_user_privileges(self, username: str) -> list[str]:
507
+ device_users = self.get_users()
508
+ self.logger.debug(
509
+ f"Checking for user privileges of user {username} on device {self.host}"
510
+ )
511
+ for user in device_users:
512
+ if user["username"] == username:
513
+ return user["permissions"]
514
+ self.logger.info(
515
+ f"Couldn't find the specified user {username} in the user list."
516
+ )
517
+ raise ElementNotFound
518
+
519
+ def get_users(self) -> dict[str, Any]:
520
+ action = "getUserList"
521
+ return esp_api.send_request(self, action).json()["data"]
522
+
523
+ def change_password(self, new_password: str) -> None:
524
+ self.change_user_password(new_password, self.username, self.password)
525
+
526
+ def change_user_password(
527
+ self,
528
+ username: str,
529
+ old_password: str,
530
+ new_password: str,
531
+ ) -> None:
532
+ current_user = esp_api.send_request(self, "getCurrentUser").json()
533
+ if "can_alter_users" not in current_user["data"]["privileges"]:
534
+ raise PermissionError(
535
+ "You don't have permission to manage users on this device."
536
+ )
537
+ request_data = {
538
+ "username": username,
539
+ "password": {"old": old_password, "new": new_password},
540
+ }
541
+ self.logger.debug(
542
+ f"Changing password of user {username} to {new_password} on device {self.host}"
543
+ )
544
+ esp_api.send_request(self, "setUser", request_data)
545
+ if username == current_user["data"]["username"]:
546
+ self.password = new_password
547
+
548
+ def create_user(
549
+ self, username: str, password: str, privileges: list[str] | None = None
550
+ ) -> None:
551
+ if "can_alter_users" not in self.user_permissions:
552
+ raise PermissionError(
553
+ "You don't have permission to manage users on this device."
554
+ )
555
+ possible_priviledges = (
556
+ "can_login",
557
+ "can_alter_users",
558
+ "can_alter_settings",
559
+ "can_use_tunnels",
560
+ "can_browse_logs",
561
+ "can_alter_outputs",
562
+ "can_control_outputs",
563
+ "can_view_settings",
564
+ "can_alter_rules",
565
+ )
566
+ if privileges is None:
567
+ privileges = ["can_login"]
568
+ for privilege in privileges:
569
+ if privilege not in possible_priviledges:
570
+ raise ValueError(
571
+ f"List of priviledges contains an invalid value: {privileges}"
572
+ )
573
+ action = "addUser"
574
+ request_data = {
575
+ "username": username,
576
+ "password": password,
577
+ "permissions": privileges,
578
+ }
579
+ self.logger.debug(f"Creating user {username} on device {self.host}")
580
+ esp_api.send_request(self, action, request_data)
581
+
582
+ def remove_user(self, username: str) -> None:
583
+ requst_data = {"username": username}
584
+ action = "deleteUser"
585
+ self.logger.debug(f"Removing user {username} on device {self.host}")
586
+ esp_api.send_request(self, action, requst_data)
587
+
588
+ # endregion
589
+
590
+ # region Protocols
591
+
592
+ def get_active_protocols(self) -> list[int]:
593
+ response = esp_api.send_request(self, "getActiveProtocols")
594
+ self.logger.debug(
595
+ f"Received active protocols: {response.json()['data']['protocols']} from device {self.host}"
596
+ )
597
+ return response.json()["data"]["protocols"]
598
+
599
+ def get_supported_protocols(self) -> list[int]:
600
+ response = esp_api.send_request(self, "getSupportedProtocols")
601
+ protocols = response.json()["data"]["protocols"]
602
+ self.logger.debug(
603
+ f"Received supported protocols: {protocols} from device {self.host}"
604
+ )
605
+ return protocols
606
+
607
+ # region NETIO Cloud
608
+
609
+ def set_cloud_state(self, state: bool) -> None:
610
+ string_state = "true" if state else "false"
611
+
612
+ response = esp_api.send_request(
613
+ self, "setProtocol", {"id": 111, "action": "set", "enable": string_state}
614
+ )
615
+ self.logger.debug(
616
+ f"Setting cloud state of device {self.host} to {state}, response: {response.json()}"
617
+ )
618
+
619
+ def get_cloud_state(self) -> dict[str, Any]:
620
+ response = esp_api.send_request(
621
+ self, "getProtocol", {"id": 111, "action": "get"}
622
+ )
623
+ self.logger.debug(
624
+ f"Received cloud state: {response.json()['data']} from device {self.host}"
625
+ )
626
+ return response.json()
627
+
628
+ def set_on_premise(self, url: str) -> None:
629
+ response = esp_api.send_request(
630
+ self, "setProtocol", {"id": 111, "action": "setOnPremis", "server": url}
631
+ )
632
+ self.logger.debug(
633
+ f"Setting on-premise server of device {self.host} to {url}, response: {response.json()}"
634
+ )
635
+
636
+ def register_to_cloud(self, token: str) -> None:
637
+ response = esp_api.send_request(
638
+ self, "setProtocol", {"id": 111, "action": "register", "token": token}
639
+ )
640
+ self.logger.debug(
641
+ f"Registering device {self.host} to cloud using token {token}, response: {response.json()}"
642
+ )
643
+
644
+ # endregion
645
+
646
+ # region URLAPI
647
+
648
+ def set_urlapi_state(
649
+ self,
650
+ protocol_enabled: bool | None = None,
651
+ write_enable: bool | None = None,
652
+ write_password: str | None = None,
653
+ ) -> None:
654
+ self._require_arguments(
655
+ "set_urlapi_state",
656
+ protocol_enabled=protocol_enabled,
657
+ write_enable=write_enable,
658
+ write_password=write_password,
659
+ )
660
+ json_data = {
661
+ "enable": protocol_enabled,
662
+ "write": {"enable": write_enable, "password": write_password},
663
+ "id": 105,
664
+ }
665
+ response = esp_api.send_request(self, "setProtocol", data=json_data)
666
+ self.logger.debug(
667
+ f"Setting URL API state on device {self.host} to {protocol_enabled}, response: {response.json()}"
668
+ )
669
+
670
+ # endregion
671
+
672
+ # region Modbus
673
+
674
+ def get_modbus_state(self) -> dict[str, Any]:
675
+ response = esp_api.send_request(
676
+ self, "getProtocol", {"id": 107, "action": "getStatus"}
677
+ )
678
+ self.logger.debug(
679
+ f"Received Modbus M2M state: {response.json()['data']} from device {self.host}"
680
+ )
681
+ return response.json()["data"]
682
+
683
+ def set_modbus_state(
684
+ self,
685
+ protocol_enabled: bool | None = None,
686
+ port: int | None = None,
687
+ ip_filter_enabled: bool | None = None,
688
+ ip_from: str | None = None,
689
+ ip_to: str | None = None,
690
+ ) -> None:
691
+ self._require_arguments("set_modbus_state", protocol_enabled=protocol_enabled)
692
+ port = 502 if port is None else port
693
+ ip_filter_enabled = False if ip_filter_enabled is None else ip_filter_enabled
694
+ ip_from = "" if ip_from is None else ip_from
695
+ ip_to = "" if ip_to is None else ip_to
696
+ modbus_data = self.get_modbus_state()
697
+ json_data = {"enable": protocol_enabled, "port": port, "id": 107}
698
+ if ip_filter_enabled:
699
+ if ip_from == "":
700
+ ip_from = modbus_data["ipFilter"]["ipFrom"]
701
+ if ip_to == "":
702
+ ip_to = modbus_data["ipFilter"]["ipTo"]
703
+ json_data["ipFilter"] = {
704
+ "enable": ip_filter_enabled,
705
+ "ipFrom": ip_from,
706
+ "ipTo": ip_to,
707
+ } # noqa
708
+ else:
709
+ json_data["ipFilter"] = {"enable": ip_filter_enabled}
710
+ response = esp_api.send_request(self, "setProtocol", data=json_data)
711
+ self.logger.debug(
712
+ f"Setting Modbus M2M state on device {self.host} to {protocol_enabled}, response: {response.json()}"
713
+ )
714
+
715
+ # endregion
716
+
717
+ # region JSON
718
+
719
+ def get_json_api_state(self) -> dict[str, Any]:
720
+ response = esp_api.send_request(
721
+ self, "getProtocol", {"id": 104, "action": "getStatus"}
722
+ )
723
+ self.logger.debug(
724
+ f"Received JSON API state: {response.json()['data']} from device {self.host}"
725
+ )
726
+ return response.json()["data"]
727
+
728
+ def set_json_api_state(
729
+ self,
730
+ protocol_enabled: bool | None = None,
731
+ read_enable: bool | None = None,
732
+ write_enable: bool | None = None,
733
+ read_auth: tuple[str, str] | None = None,
734
+ write_auth: tuple[str, str] | None = None,
735
+ ) -> None:
736
+ self._require_arguments("set_json_api_state", protocol_enabled=protocol_enabled)
737
+ # TODO: Make the parameters optional, so only a specific setting can be changed.
738
+ current_config = self.get_json_api_state()
739
+
740
+ if read_enable is None:
741
+ read_enable = current_config["read"]["enable"]
742
+ if write_enable is None:
743
+ write_enable = current_config["write"]["enable"]
744
+ if read_auth is None:
745
+ read_auth = (
746
+ current_config["read"]["username"],
747
+ current_config["read"]["password"],
748
+ )
749
+ if write_auth is None:
750
+ write_auth = (
751
+ current_config["write"]["username"],
752
+ current_config["write"]["password"],
753
+ )
754
+
755
+ request_data = {
756
+ "enable": protocol_enabled,
757
+ "read": {
758
+ "enable": read_enable,
759
+ "username": read_auth[0],
760
+ "password": read_auth[1],
761
+ },
762
+ "write": {
763
+ "enable": write_enable,
764
+ "username": write_auth[0],
765
+ "password": write_auth[1],
766
+ },
767
+ "id": 104,
768
+ }
769
+
770
+ response = esp_api.send_request(self, "setProtocol", request_data)
771
+ self.logger.debug(
772
+ f"Setting JSON API state on device {self.host} to {protocol_enabled}, full info: {request_data}"
773
+ )
774
+ if response.status_code == 200:
775
+ ap = self.get_active_protocols()
776
+ if (protocol_enabled and 104 in ap) or (
777
+ not protocol_enabled and 104 not in ap
778
+ ):
779
+ self.logger.debug(
780
+ f"Successfully set JSON API state on device {self.host}"
781
+ )
782
+ else:
783
+ self.logger.debug(f"Unable to set JSON API state on device {self.host}")
784
+
785
+ def get_json(self, json_auth: tuple[str, str]) -> dict[str, Any]:
786
+ if 104 not in self.get_active_protocols():
787
+ raise ProtocolNotEnabled("JSON API is not enabled on the device.")
788
+ try:
789
+ response = requests.get(
790
+ "http://" + self.host + "/netio.json", auth=json_auth, timeout=10
791
+ )
792
+ if response.status_code == 200:
793
+ self.logger.debug(
794
+ f"Received JSON information from device {self.host}: {response.json()}"
795
+ )
796
+ return response.json()
797
+ else:
798
+ raise requests.ConnectionError
799
+ except (requests.ConnectionError, requests.ReadTimeout) as e:
800
+ self.logger.critical(f"Unable to reach host {self.host}")
801
+ raise e
802
+
803
+ # endregion
804
+
805
+ def get_telnet_api_state(self) -> dict[str, Any]:
806
+ action = "getProtocol"
807
+ response = esp_api.send_request(
808
+ self, action, {"id": 106, "action": "getStatus"}
809
+ )
810
+ self.logger.debug(
811
+ f"Received telnet API state: {response.json()['data']} from device {self.host}"
812
+ )
813
+ return response.json()["data"]
814
+
815
+ def set_telnet_api_state(
816
+ self,
817
+ protocol_enabled: bool | None = None,
818
+ port: int | None = None,
819
+ read_enabled: bool | None = None,
820
+ read_auth: tuple[str, str] | None = None,
821
+ write_enabled: bool | None = None,
822
+ write_auth: tuple[str, str] | None = None,
823
+ ) -> None:
824
+ self._require_arguments(
825
+ "set_telnet_api_state", protocol_enabled=protocol_enabled
826
+ )
827
+ action = "setProtocol"
828
+ current_state = self.get_telnet_api_state()
829
+ request_data = {
830
+ "enable": protocol_enabled,
831
+ "port": port if port else current_state["port"],
832
+ "read": {
833
+ "enable": read_enabled
834
+ if read_enabled
835
+ else current_state["read"]["enable"],
836
+ "username": read_auth[0]
837
+ if read_auth
838
+ else current_state["read"]["username"],
839
+ "password": read_auth[1]
840
+ if read_auth
841
+ else current_state["read"]["password"],
842
+ },
843
+ "write": {
844
+ "enable": write_enabled
845
+ if write_enabled
846
+ else current_state["write"]["enable"],
847
+ "username": write_auth[0]
848
+ if write_auth
849
+ else current_state["write"]["username"],
850
+ "password": write_auth[1]
851
+ if write_auth
852
+ else current_state["write"]["password"],
853
+ },
854
+ "id": 106,
855
+ }
856
+ self.logger.debug(
857
+ f"Setting telnet API state on device {self.host} to new settings: {request_data}"
858
+ )
859
+ response = esp_api.send_request(self, action, request_data)
860
+ if response.status_code == 200:
861
+ ap = self.get_active_protocols()
862
+ if (protocol_enabled and 106 in ap) or (
863
+ not protocol_enabled and 106 not in ap
864
+ ):
865
+ self.logger.debug(
866
+ f"Successfully set new telnet state on device {self.host}"
867
+ )
868
+ else:
869
+ self.logger.debug(
870
+ f"Unable to set telnet state on device {self.host}, check debug log."
871
+ )
872
+
873
+ def get_netio_push_api_state(self) -> dict[str, Any]:
874
+ action = "getProtocol"
875
+ response = esp_api.send_request(
876
+ self, action, {"id": 109, "action": "getStatus"}
877
+ )
878
+ self.logger.debug(
879
+ f"Received Netio Push API state: {response.json()['data']} from device {self.host}"
880
+ )
881
+ return response.json()["data"]
882
+
883
+ def set_netio_push_api_state(
884
+ self,
885
+ protocol_enabled: bool | None = None,
886
+ url: str | None = None,
887
+ push_protocol: str | None = None,
888
+ delta: int | None = None,
889
+ period: int | None = None,
890
+ ) -> None:
891
+ self._require_arguments(
892
+ "set_netio_push_api_state", protocol_enabled=protocol_enabled
893
+ )
894
+ action = "setProtocol"
895
+ current_state = self.get_netio_push_api_state()
896
+ request_data = {
897
+ "enable": protocol_enabled,
898
+ "url": url if url else current_state["url"],
899
+ "pushProtocol": push_protocol
900
+ if push_protocol
901
+ else current_state["pushProtocol"],
902
+ "delta": delta if delta else current_state["delta"],
903
+ "period": period if period else current_state["period"],
904
+ "value": "current",
905
+ "id": 109,
906
+ }
907
+ self.logger.debug(
908
+ f"Setting Netio Push API state on device {self.host} to settings: {request_data}"
909
+ )
910
+ response = esp_api.send_request(self, action, request_data)
911
+ if response.status_code == 200:
912
+ ap = self.get_active_protocols()
913
+ if (protocol_enabled and 109 in ap) or (
914
+ not protocol_enabled and 109 not in ap
915
+ ):
916
+ self.logger.debug(
917
+ f"Successfully set new Netio Push API state on device {self.host}"
918
+ )
919
+ else:
920
+ self.logger.debug(
921
+ f"Unable to set the Netio Push API state on device {self.host}, check debug log."
922
+ )
923
+
924
+ def netio_push_api_push_now(self) -> None:
925
+ action = "pushNow"
926
+ ap = self.get_active_protocols()
927
+ if 109 not in ap:
928
+ self.logger.info(
929
+ f"Tried to push on device with Push protocol disabled: {self.host}"
930
+ )
931
+ raise ProtocolNotEnabled(
932
+ f"Netio Push API is not enabled on the device. Current protocols: {ap}"
933
+ )
934
+ self.logger.debug(f"Pushing Netio Push API now on device {self.host}")
935
+ esp_api.send_request(self, action)
936
+
937
+ def get_snmp_api_state(self) -> dict[str, Any]:
938
+ action = "getProtocol"
939
+ response = esp_api.send_request(
940
+ self, action, {"id": 110, "action": "getStatus"}
941
+ )
942
+ self.logger.debug(
943
+ f"Received SNMP API state: {response.json()['data']} from device {self.host}"
944
+ )
945
+ return response.json()["data"]
946
+
947
+ def _set_snmp_api_state(
948
+ self,
949
+ protocol_enabled: bool,
950
+ version: str,
951
+ location: str | None = None,
952
+ community_read: str | None = None,
953
+ community_write: str | None = None,
954
+ security_name: str | None = None,
955
+ security_level: str | None = None,
956
+ auth_protocol: str | None = None,
957
+ auth_key: str | None = None,
958
+ priv_protocol: str | None = None,
959
+ priv_key: str | None = None,
960
+ ) -> None:
961
+ action = "setProtocol"
962
+ current_state = self.get_snmp_api_state()
963
+ if version == "v1,2c":
964
+ request_data = {
965
+ "enable": protocol_enabled,
966
+ "id": 110,
967
+ "version": "v1-2",
968
+ "location": location if location else current_state["location"],
969
+ "communityRead": community_read
970
+ if community_read
971
+ else current_state["communityRead"],
972
+ "communityWrite": community_write
973
+ if community_write
974
+ else current_state["communityWrite"],
975
+ }
976
+ _ = esp_api.send_request(self, action, request_data)
977
+ elif version == "v3":
978
+ request_data = {
979
+ "enable": protocol_enabled,
980
+ "id": 110,
981
+ "version": "v3",
982
+ "location": location if location else current_state["location"],
983
+ "communityRead": community_read
984
+ if community_read
985
+ else current_state["communityRead"],
986
+ "communityWrite": community_write
987
+ if community_write
988
+ else current_state["communityWrite"],
989
+ }
990
+ security_level = (
991
+ security_level
992
+ if security_level
993
+ else current_state["snmpV3Users"][0]["securityLevel"]
994
+ )
995
+ user_object = {
996
+ "username": security_name
997
+ if security_name
998
+ else current_state["snmpV3Users"][0]["username"],
999
+ "accessLevel": "rw",
1000
+ "securityLevel": security_level,
1001
+ }
1002
+ if security_level == "authNoPriv" or security_level == "authPriv":
1003
+ print(current_state["snmpV3Users"])
1004
+ user_object["authAlgo"] = (
1005
+ auth_protocol
1006
+ if auth_protocol
1007
+ else current_state["snmpV3Users"][0]["authAlgo"]
1008
+ )
1009
+ user_object["authKey"] = (
1010
+ auth_key if auth_key else current_state["snmpV3Users"][0]["authKey"]
1011
+ )
1012
+ if security_level == "authPriv":
1013
+ user_object["encryptAlgo"] = (
1014
+ priv_protocol
1015
+ if priv_protocol
1016
+ else current_state["snmpV3Users"][0]["encryptAlgo"]
1017
+ )
1018
+ user_object["encryptKey"] = (
1019
+ priv_key
1020
+ if priv_key
1021
+ else current_state["snmpV3Users"][0]["encryptKey"]
1022
+ )
1023
+ request_data["snmpV3Users"] = [user_object]
1024
+ _ = esp_api.send_request(self, action, request_data)
1025
+ else:
1026
+ raise InvalidParameterValueError
1027
+
1028
+ def set_snmp_v1_2_api_state(
1029
+ self,
1030
+ protocol_enabled: bool | None = None,
1031
+ location: str | None = None,
1032
+ community_read: str | None = None,
1033
+ community_write: str | None = None,
1034
+ ) -> None:
1035
+ self._require_arguments(
1036
+ "set_snmp_v1_2_api_state", protocol_enabled=protocol_enabled
1037
+ )
1038
+ self.logger.debug(f"Setting SNMP v1,2c API state on device {self.host}")
1039
+ if not protocol_enabled:
1040
+ self.logger.debug(
1041
+ "The protocol is being disabled, the device will restart."
1042
+ )
1043
+ self._set_snmp_api_state(
1044
+ protocol_enabled, "v1,2c", location, community_read, community_write
1045
+ )
1046
+
1047
+ def set_snmp_v3_api_state(
1048
+ self,
1049
+ protocol_enabled: bool | None = None,
1050
+ location: str | None = None,
1051
+ security_name: str | None = None,
1052
+ security_level: str | None = None,
1053
+ auth_protocol: str | None = None,
1054
+ auth_key: str | None = None,
1055
+ priv_protocol: str | None = None,
1056
+ priv_key: str | None = None,
1057
+ ) -> None:
1058
+ self._require_arguments(
1059
+ "set_snmp_v3_api_state", protocol_enabled=protocol_enabled
1060
+ )
1061
+ self.logger.debug(f"Setting SNMP v3 API state on device {self.host}")
1062
+ if not protocol_enabled:
1063
+ self.logger.debug(
1064
+ "The protocol is being disabled, the device will restart."
1065
+ )
1066
+ self._set_snmp_api_state(
1067
+ protocol_enabled,
1068
+ "v3",
1069
+ location,
1070
+ None,
1071
+ None,
1072
+ security_name,
1073
+ security_level,
1074
+ auth_protocol,
1075
+ auth_key,
1076
+ priv_protocol,
1077
+ priv_key,
1078
+ )
1079
+
1080
+ # region XML
1081
+
1082
+ def set_xml_api_state(
1083
+ self,
1084
+ protocol_enabled: bool | None = None,
1085
+ read_enable: bool | None = None,
1086
+ write_enable: bool | None = None,
1087
+ read_auth: tuple[str, str] | None = None,
1088
+ write_auth: tuple[str, str] | None = None,
1089
+ ) -> None:
1090
+ self._require_arguments(
1091
+ "set_xml_api_state",
1092
+ protocol_enabled=protocol_enabled,
1093
+ read_enable=read_enable,
1094
+ write_enable=write_enable,
1095
+ read_auth=read_auth,
1096
+ write_auth=write_auth,
1097
+ )
1098
+ # TODO: Make the parameters optional, so only a specific setting can be changed.
1099
+ request_data = {
1100
+ "enable": protocol_enabled,
1101
+ "read": {
1102
+ "enable": read_enable,
1103
+ "username": read_auth[0],
1104
+ "password": read_auth[1],
1105
+ },
1106
+ "write": {
1107
+ "enable": write_enable,
1108
+ "username": write_auth[0],
1109
+ "password": write_auth[1],
1110
+ },
1111
+ "id": 103,
1112
+ }
1113
+
1114
+ response = esp_api.send_request(self, "setProtocol", request_data)
1115
+ self.logger.debug(
1116
+ f"Setting XML API state on device {self.host} to {protocol_enabled}, full info: {request_data}"
1117
+ )
1118
+ if response.status_code == 200:
1119
+ ap = self.get_active_protocols()
1120
+ if 103 in ap:
1121
+ self.logger.debug(
1122
+ f"Successfully set XML API state on device {self.host}"
1123
+ )
1124
+ else:
1125
+ self.logger.debug(f"Unable to set XML API state on device {self.host}")
1126
+
1127
+ def get_xml(self, xml_auth: tuple[str, str]) -> ET.Element:
1128
+ if 103 not in self.get_active_protocols():
1129
+ raise ProtocolNotEnabled("XML API is not enabled on the device.")
1130
+ try:
1131
+ response = requests.get(
1132
+ "http://" + self.host + "/netio.xml", auth=xml_auth, timeout=10
1133
+ )
1134
+ if response.status_code == 200:
1135
+ self.logger.debug(
1136
+ f"Received XML information from device {self.host}: {response.content}"
1137
+ )
1138
+ return ET.fromstring(response.content)
1139
+ else:
1140
+ self.logger.critical(f"Unable to reach host {self.host}")
1141
+ raise CommunicationError(f"Return code: {response.status_code}")
1142
+ except requests.ConnectionError as e:
1143
+ raise e
1144
+
1145
+ # endregion
1146
+
1147
+ # endregion
1148
+
1149
+ def get_rules(self) -> list[dict[str, Any]]:
1150
+ action = "getRules"
1151
+ self.logger.debug(f"Getting a rule list from device {self.host}")
1152
+ response = esp_api.send_request(self, action, {})
1153
+ rule_list = response.json()["data"]
1154
+ return rule_list
1155
+
1156
+ def get_enabled_rules(self) -> list[dict[str, Any]]:
1157
+ rule_list = self.get_rules()
1158
+ enabled_rules = []
1159
+ self.logger.debug(f"Filtering for enabled rules from device {self.host}")
1160
+ for rule in rule_list:
1161
+ if rule["enabled"]:
1162
+ enabled_rules.append(rule)
1163
+ return enabled_rules
1164
+
1165
+ def get_disabled_rules(self) -> list[dict[str, Any]]:
1166
+ rule_list = self.get_rules()
1167
+ disabled_rules = []
1168
+ self.logger.debug(f"Filtering for disabled rules from device {self.host}")
1169
+ for rule in rule_list:
1170
+ if not rule["enabled"]:
1171
+ disabled_rules.append(rule)
1172
+ return disabled_rules
1173
+
1174
+ def get_rule_by_name(self, rule_name: str) -> dict[str, Any]:
1175
+ rule_list = self.get_rules()
1176
+ self.logger.debug(f"Filtering for rule {rule_name} from device {self.host}")
1177
+ for rule in rule_list:
1178
+ if rule["name"] == rule_name:
1179
+ return rule
1180
+ self.logger.info(f"Unable to find rule {rule_name} on device {self.host}")
1181
+ raise ElementNotFound
1182
+
1183
+ def get_watchdogs(self) -> list[dict[str, Any]]:
1184
+ action = "getWatchdogs"
1185
+ self.logger.debug(f"Getting a watchdog list from device {self.host}")
1186
+ response = esp_api.send_request(self, action, {})
1187
+ watchdog_list = response.json()["data"]
1188
+ return watchdog_list
1189
+
1190
+ def get_enabled_watchdogs(self) -> list[dict[str, Any]]:
1191
+ watchdogs = self.get_watchdogs()
1192
+ enabled_watchdogs = []
1193
+ self.logger.debug(f"Filtering for enabled watchdogs from device {self.host}")
1194
+ for watchdog in watchdogs:
1195
+ if watchdog["enabled"]:
1196
+ enabled_watchdogs.append(watchdog)
1197
+ return enabled_watchdogs
1198
+
1199
+ def get_disabled_watchdogs(self) -> list[dict[str, Any]]:
1200
+ watchdogs = self.get_watchdogs()
1201
+ disabled_watchdogs = []
1202
+ self.logger.debug(f"Filtering for disabled watchdogs from device {self.host}")
1203
+ for watchdog in watchdogs:
1204
+ if not watchdog["enabled"]:
1205
+ disabled_watchdogs.append(watchdog)
1206
+ return disabled_watchdogs
1207
+
1208
+ def get_watchdog_by_name(self, watchdog_name: str) -> dict[str, Any]:
1209
+ watchdogs = self.get_watchdogs()
1210
+ self.logger.debug(
1211
+ f"Filtering for watchdog {watchdog_name} on device {self.host}"
1212
+ )
1213
+ for watchdog in watchdogs:
1214
+ if watchdog["name"] == watchdog_name:
1215
+ return watchdog
1216
+ self.logger.info(
1217
+ f"Unable to find watchdog {watchdog_name} on device {self.host}"
1218
+ )
1219
+ raise ElementNotFound
1220
+
1221
+ def get_schedules(self) -> list[dict[str, Any]]:
1222
+ action = "getScheduleList"
1223
+ self.logger.debug(f"Getting a schedule list from device {self.host}")
1224
+ response = esp_api.send_request(self, action, {})
1225
+ schedule_list = response.json()["data"]
1226
+ return schedule_list
1227
+
1228
+ def get_schedule_by_name(self, schedule_name: str) -> dict[str, Any]:
1229
+ scheduels = self.get_schedules()
1230
+ self.logger.debug(
1231
+ f"Filtering for schedule {schedule_name} on device {self.host}"
1232
+ )
1233
+ for schedule in scheduels:
1234
+ if schedule["name"] == schedule_name:
1235
+ return schedule
1236
+ self.logger.info(
1237
+ f"Unable to find schedule {schedule_name} on device {self.host}"
1238
+ )
1239
+ raise ElementNotFound
1240
+
1241
+ def get_schedule_id(self, schedule_name: str) -> int:
1242
+ schedule = self.get_schedule_by_name(schedule_name)
1243
+ return schedule["id"]
1244
+
1245
+ def get_schedule_names(self) -> list[str]:
1246
+ schedules = self.get_schedules()
1247
+ schedule_names = []
1248
+ self.logger.debug(f"Filtering for schedule names on device {self.host}")
1249
+ for schedule in schedules:
1250
+ schedule_names.append(schedule["name"])
1251
+ return schedule_names
1252
+
1253
+ def get_active_schedules(self) -> list[dict[str, Any]]:
1254
+ schedules = self.get_schedules()
1255
+ active_schedules = []
1256
+ self.logger.debug(f"Filtering for active schedules on device {self.host}")
1257
+ for schedule in schedules:
1258
+ if schedule["active"]:
1259
+ active_schedules.append(schedule)
1260
+ return active_schedules
1261
+
1262
+ def create_schedule(self, name: str, intervals: list[dict[str, Any]]) -> None:
1263
+ raise NotImplementedError(
1264
+ "create_schedule is only available for devices 5.x.x and up, please update your device."
1265
+ )
1266
+
1267
+ def delete_schedule(self, schedule_id: int) -> None:
1268
+ action = "deleteSchedule"
1269
+ self.logger.debug(f"Deleting schedule {schedule_id} from device {self.host}")
1270
+ try:
1271
+ esp_api.send_request(self, action, {"id": schedule_id})
1272
+ except CommunicationError as e:
1273
+ if "Invalid parameter" in str(e):
1274
+ raise ElementNotFound
1275
+
1276
+ def delete_schedule_by_name(self, schedule_name: str) -> None:
1277
+ schedule_id = self.get_schedule_id(schedule_name)
1278
+ self.delete_schedule(schedule_id)
1279
+
1280
+ def get_system_log(self) -> list[dict[str, Any]]:
1281
+ action = "loadSystemLog"
1282
+ self.logger.debug(f"Getting system log from device {self.host}")
1283
+ response = esp_api.send_request(self, action, {})
1284
+ return response.json()["data"]
1285
+
1286
+ def clear_system_log(self) -> None:
1287
+ action = "clearUserLog"
1288
+ self.logger.debug(f"Clearing system log from device {self.host}")
1289
+ _ = esp_api.send_request(self, action, {})
1290
+
1291
+ def get_pabs(self) -> list[dict[str, Any]]:
1292
+ action = "getPABList"
1293
+ self.logger.debug(f"Getting PABs from device {self.host}")
1294
+ response = esp_api.send_request(self, action, {})
1295
+ pab_list = response.json()["data"]
1296
+ return pab_list
1297
+
1298
+ def get_enabled_pabs(self) -> list[dict[str, Any]]:
1299
+ pab_list = self.get_pabs()
1300
+ enabled_pabs = []
1301
+ self.logger.debug(f"Filtering for enabled PABs from device {self.host}")
1302
+ for pab in pab_list:
1303
+ if pab["enabled"]:
1304
+ enabled_pabs.append(pab)
1305
+ return enabled_pabs
1306
+
1307
+ def get_disabled_pabs(self) -> list[dict[str, Any]]:
1308
+ pab_list = self.get_pabs()
1309
+ disabled_pabs = []
1310
+ self.logger.debug(f"Filtering for disabled PABs from device {self.host}")
1311
+ for pab in pab_list:
1312
+ if not pab["enabled"]:
1313
+ disabled_pabs.append(pab)
1314
+ return disabled_pabs
1315
+
1316
+ def get_pab_by_name(self, pab_name: str) -> dict[str, Any]:
1317
+ pabs = self.get_pabs()
1318
+ self.logger.debug(f"Filtering for PAB {pab_name} on device {self.host}")
1319
+ for pab in pabs:
1320
+ if pab["name"] == pab_name:
1321
+ return pab
1322
+ raise ElementNotFound
1323
+
1324
+ def delete_pab_by_name(self, pab_name: str) -> None:
1325
+ action = "deletePAB"
1326
+ request_data = {"name": pab_name}
1327
+ self.logger.debug(f"Deleting PAB {pab_name} from device {self.host}")
1328
+ esp_api.send_request(self, action, request_data)
1329
+
1330
+ # region Private Functions
1331
+ def _check_socket_index(self, socket_index: int) -> None:
1332
+ if socket_index < 1 or socket_index > self.output_count:
1333
+ raise InvalidSocketIndex(
1334
+ f"You tried to access socket with id {socket_index},"
1335
+ f" the device supports <1;{self.output_count}>."
1336
+ )
1337
+
1338
+ def _require_arguments(self, method_name: str, **arguments: Any) -> None:
1339
+ # Pre-5.0.0 can't keep these values from the device, so they stay required.
1340
+ missing = [name for name, value in arguments.items() if value is None]
1341
+ if missing:
1342
+ raise InvalidParameterValueError(
1343
+ f"{method_name} requires {', '.join(missing)} "
1344
+ "on firmware older than 5.0.0"
1345
+ )
1346
+
1347
+ def _cleanup(self) -> None:
1348
+ try:
1349
+ if self._ka_thread:
1350
+ self._ka_thread.cancel()
1351
+ self._ka_thread.join()
1352
+ self.logger.debug("Disabled keep-alive thread.")
1353
+ except AttributeError:
1354
+ pass # No need to clean up a non existant thread
1355
+ if self.session_id != "" or self.session_id != "TODO AUTH":
1356
+ self.logout()
1357
+
1358
+ # endregion
1359
+
1360
+ def get_mqttflex_state(self) -> dict[str, Any]:
1361
+ raise FeatureNotSupported(
1362
+ "MQTT with certificates is only supported on firmware 5.0.0 and newer."
1363
+ )
1364
+
1365
+ def set_mqttflex_state(
1366
+ self, state: bool | None = None, config: dict | None = None
1367
+ ) -> None:
1368
+ raise FeatureNotSupported(
1369
+ "MQTT with certificates is only supported on firmware 5.0.0 and newer."
1370
+ )
1371
+
1372
+ def upload_mqtt_ca_certificate(self, ca: str) -> None:
1373
+ raise FeatureNotSupported(
1374
+ "MQTT with certificates is only supported on firmware 5.0.0 and newer."
1375
+ )
1376
+
1377
+ def upload_mqtt_client_certificate(self, cert: str) -> None:
1378
+ raise FeatureNotSupported(
1379
+ "MQTT with certificates is only supported on firmware 5.0.0 and newer."
1380
+ )
1381
+
1382
+ def upload_mqtt_client_key(self, key: str) -> None:
1383
+ raise FeatureNotSupported(
1384
+ "MQTT with certificates is only supported on firmware 5.0.0 and newer."
1385
+ )
1386
+
1387
+ def upload_https_certificate(self, certfile: str) -> None:
1388
+ raise FeatureNotSupported(
1389
+ "HTTPS with certificates is only supported on firmware 5.0.0 and newer."
1390
+ )
1391
+
1392
+ def upload_https_private_key(self, keyfile: str) -> None:
1393
+ raise FeatureNotSupported(
1394
+ "HTTPS with certificates is only supported on firmware 5.0.0 and newer."
1395
+ )
1396
+
1397
+ @override
1398
+ def get_version_revision(self) -> str:
1399
+ response = esp_api.send_request(self, "getVersion").json()["data"]["revision"]
1400
+ self.logger.debug(f"Received device version information: {response}")
1401
+ return response
1402
+
1403
+ @override
1404
+ def get_input_list(self) -> dict[str, Any]:
1405
+ raise NotImplementedError(
1406
+ "Input commands are only available for devices 5.x.x and up, please update your device."
1407
+ )
1408
+
1409
+ @override
1410
+ def get_input_data(self, input_id: int) -> dict[str, Any]:
1411
+ raise NotImplementedError(
1412
+ "Input commands are only available for devices 5.x.x and up, please update your device."
1413
+ )
1414
+
1415
+ @override
1416
+ def get_system_datetime(self) -> dict[str, Any]:
1417
+ raise NotImplementedError(
1418
+ "get_system_datetime is only available for devices 5.x.x and up, please update your device."
1419
+ )
1420
+
1421
+ @override
1422
+ def set_system_datetime(
1423
+ self,
1424
+ ntp_enabled: bool | None = None,
1425
+ ntp_server: str | None = None,
1426
+ timezone: str | None = None,
1427
+ time: int | None = None,
1428
+ ) -> None:
1429
+ raise NotImplementedError(
1430
+ "set_system_datetime is only available for devices 5.x.x and up, please update your device."
1431
+ )
1432
+
1433
+ @override
1434
+ def system_reset(self) -> None:
1435
+ raise NotImplementedError(
1436
+ "system_reset is only available for devices 5.x.x and up, please update your device."
1437
+ )
1438
+
1439
+ @override
1440
+ def get_ethernet_settings(self) -> dict[str, Any]:
1441
+ raise NotImplementedError(
1442
+ "get_ethernet_settings is only available for devices 5.x.x and up, please update your device."
1443
+ )
1444
+
1445
+ @override
1446
+ def set_ethernet_settings(
1447
+ self,
1448
+ network_mode: str | None = None,
1449
+ ip: str | None = None,
1450
+ netmask: str | None = None,
1451
+ gateway: str | None = None,
1452
+ dns: str | None = None,
1453
+ ) -> None:
1454
+ raise NotImplementedError(
1455
+ "set_ethernet_settings is only available for devices 5.x.x and up, please update your device."
1456
+ )
1457
+
1458
+ @override
1459
+ def get_ethernet_status(self) -> dict[str, Any]:
1460
+ raise NotImplementedError(
1461
+ "get_ethernet_status is only available for devices 5.x.x and up, please update your device."
1462
+ )
1463
+
1464
+ @override
1465
+ def get_wifi_status(self) -> dict[str, Any]:
1466
+ raise NotImplementedError(
1467
+ "get_wifi_status is only available for devices 5.x.x and up, please update your device."
1468
+ )
1469
+
1470
+ @override
1471
+ def get_server_settings(self) -> dict[str, Any]:
1472
+ raise NotImplementedError(
1473
+ "get_server_settings is only available for devices 5.x.x and up, please update your device."
1474
+ )
1475
+
1476
+ @override
1477
+ def set_server_settings(
1478
+ self,
1479
+ http_enable: bool | None = None,
1480
+ http_port: int | None = None,
1481
+ https_enable: bool | None = None,
1482
+ https_port: int | None = None,
1483
+ ) -> None:
1484
+ raise NotImplementedError(
1485
+ "set_server_settings is only available for devices 5.x.x and up, please update your device."
1486
+ )
1487
+
1488
+ @override
1489
+ def get_global_measurement(self) -> dict[str, Any]:
1490
+ raise NotImplementedError(
1491
+ "get_global_measurement is only available for devices 5.x.x and up, please update your device."
1492
+ )
1493
+
1494
+ @override
1495
+ def get_outputs_list(self) -> list[dict[str, Any]]:
1496
+ raise NotImplementedError(
1497
+ "get_outputs_list is only available for devices 5.x.x and up, please update your device."
1498
+ )
1499
+
1500
+ @override
1501
+ def get_firmware_updates_list(self) -> list[dict[str, Any]]:
1502
+ raise NotImplementedError(
1503
+ "get_firmware_updates_list is only available for devices 5.x.x and up, please update your device."
1504
+ )
1505
+
1506
+ @override
1507
+ def get_system_lock(self) -> dict[str, Any]:
1508
+ raise NotImplementedError(
1509
+ "get_system_lock is only available for devices 5.x.x and up, please update your device."
1510
+ )
1511
+
1512
+ @override
1513
+ def get_system_notifications(self) -> list[dict[str, Any]]:
1514
+ raise NotImplementedError(
1515
+ "get_system_notifications is only available for devices 5.x.x and up, please update your device."
1516
+ )