tinytoolslib 0.2.5__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.
- tinytoolslib/__init__.py +1 -0
- tinytoolslib/__version__.py +1 -0
- tinytoolslib/constants.py +14 -0
- tinytoolslib/discovery.py +167 -0
- tinytoolslib/exceptions.py +49 -0
- tinytoolslib/flash.py +316 -0
- tinytoolslib/models.py +1674 -0
- tinytoolslib/parsers.py +86 -0
- tinytoolslib/requests.py +268 -0
- tinytoolslib-0.2.5.dist-info/METADATA +76 -0
- tinytoolslib-0.2.5.dist-info/RECORD +13 -0
- tinytoolslib-0.2.5.dist-info/WHEEL +5 -0
- tinytoolslib-0.2.5.dist-info/top_level.txt +1 -0
tinytoolslib/models.py
ADDED
|
@@ -0,0 +1,1674 @@
|
|
|
1
|
+
from dataclasses import dataclass, field, asdict
|
|
2
|
+
import re
|
|
3
|
+
from typing import Tuple, Union, Dict, List, ClassVar, Any
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
from tinytoolslib.parsers import (
|
|
7
|
+
int_inverted,
|
|
8
|
+
parse_version,
|
|
9
|
+
up_to_int,
|
|
10
|
+
float_div10,
|
|
11
|
+
float_div100,
|
|
12
|
+
float_div1000,
|
|
13
|
+
strint_to_int_list,
|
|
14
|
+
name_list,
|
|
15
|
+
list_map,
|
|
16
|
+
)
|
|
17
|
+
from tinytoolslib.requests import get, async_get
|
|
18
|
+
from aiohttp import ClientSession
|
|
19
|
+
from tinytoolslib.constants import (
|
|
20
|
+
FAMILY_DCDC,
|
|
21
|
+
FAMILY_LK,
|
|
22
|
+
FAMILY_PS,
|
|
23
|
+
FAMILY_TCPDU,
|
|
24
|
+
FW_URL_TEMPLATE,
|
|
25
|
+
)
|
|
26
|
+
from tinytoolslib.exceptions import (
|
|
27
|
+
TinyToolsRequestConnectionError,
|
|
28
|
+
TinyToolsRequestError,
|
|
29
|
+
TinyToolsRequestInternalServerError,
|
|
30
|
+
TinyToolsRequestNotFound,
|
|
31
|
+
TinyToolsRequestSSLError,
|
|
32
|
+
TinyToolsRequestTimeout,
|
|
33
|
+
TinyToolsUnsupported,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class DeviceInfo:
|
|
39
|
+
model: str
|
|
40
|
+
family: str
|
|
41
|
+
fw_tag: Union[str, None] = None
|
|
42
|
+
fw_url: Union[str, None] = field(init=False, default=None)
|
|
43
|
+
fw_changelog: Union[str, None] = None
|
|
44
|
+
extras: Dict[str, Any] = field(default_factory=dict)
|
|
45
|
+
|
|
46
|
+
def __post_init__(self):
|
|
47
|
+
if self.fw_tag:
|
|
48
|
+
self.fw_url = FW_URL_TEMPLATE.format(self.fw_tag)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class DeviceModel:
|
|
53
|
+
"""Base class for tinycontrol devices with common methods."""
|
|
54
|
+
|
|
55
|
+
info: ClassVar[Union[DeviceInfo, None]] = None
|
|
56
|
+
mapping: ClassVar[Dict[str, Dict]] = {}
|
|
57
|
+
parsers: ClassVar[List[str]] = []
|
|
58
|
+
|
|
59
|
+
host: str
|
|
60
|
+
schema: str = "http"
|
|
61
|
+
port: int = 80
|
|
62
|
+
username: str = ""
|
|
63
|
+
password: str = ""
|
|
64
|
+
hardware_version: str = ""
|
|
65
|
+
software_version: str = ""
|
|
66
|
+
session: Union[ClientSession, None] = None
|
|
67
|
+
|
|
68
|
+
_context: Dict[str, Dict] = field(init=False, default_factory=dict)
|
|
69
|
+
_close_session: bool = False
|
|
70
|
+
|
|
71
|
+
def __post_init__(self):
|
|
72
|
+
if self.schema == "https" and self.port != 443:
|
|
73
|
+
warnings.warn(
|
|
74
|
+
"Devices (LK3.X, LK4.X, tcPDU) always use port 443 for https. "
|
|
75
|
+
f"You are about to use {self.port}."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def check_version(
|
|
80
|
+
cls, hardware_version: Union[str, None], software_version: str
|
|
81
|
+
) -> bool:
|
|
82
|
+
"""Verifies if versions matches this Device model."""
|
|
83
|
+
raise NotImplementedError
|
|
84
|
+
|
|
85
|
+
def _get(
|
|
86
|
+
self,
|
|
87
|
+
data: Dict[str, Any],
|
|
88
|
+
path: str,
|
|
89
|
+
skip_keys: Union[List[str], None] = None,
|
|
90
|
+
remove_mapped_keys: bool = False,
|
|
91
|
+
) -> Dict[str, Any]:
|
|
92
|
+
"""Process get data (dict) with mapping."""
|
|
93
|
+
if data is not None:
|
|
94
|
+
updates = {}
|
|
95
|
+
for key, value in data.items():
|
|
96
|
+
if skip_keys and key in skip_keys:
|
|
97
|
+
continue
|
|
98
|
+
mapper = self.mapping.get(key)
|
|
99
|
+
if mapper is not None:
|
|
100
|
+
mapped = mapper["format"](value)
|
|
101
|
+
if isinstance(mapper["name"], list):
|
|
102
|
+
for name, val in zip(mapper["name"], mapped):
|
|
103
|
+
updates[name] = val
|
|
104
|
+
else:
|
|
105
|
+
updates[mapper["name"]] = mapped
|
|
106
|
+
data.update(updates)
|
|
107
|
+
if remove_mapped_keys:
|
|
108
|
+
# Remove keys that were parsed/mapped
|
|
109
|
+
for key, val in self.mapping.items():
|
|
110
|
+
if isinstance(val["name"], str) and key != val["name"]:
|
|
111
|
+
data.pop(key, None)
|
|
112
|
+
# Run extra parsers, that need to work on whole response.
|
|
113
|
+
for parser in self.parsers:
|
|
114
|
+
parser_func = getattr(self, parser)
|
|
115
|
+
parser_func(data, path)
|
|
116
|
+
return data
|
|
117
|
+
|
|
118
|
+
def get(
|
|
119
|
+
self,
|
|
120
|
+
path: str,
|
|
121
|
+
skip_keys: Union[List[str], None] = None,
|
|
122
|
+
remove_mapped_keys: bool = False,
|
|
123
|
+
**kwargs,
|
|
124
|
+
) -> Dict[str, Any]:
|
|
125
|
+
"""Get data, process parsed part with mapping."""
|
|
126
|
+
response = get(
|
|
127
|
+
self.host,
|
|
128
|
+
path,
|
|
129
|
+
schema=self.schema,
|
|
130
|
+
port=self.port,
|
|
131
|
+
username=self.username,
|
|
132
|
+
password=self.password,
|
|
133
|
+
**kwargs,
|
|
134
|
+
)
|
|
135
|
+
return self._get(response.get("parsed"), path, skip_keys, remove_mapped_keys)
|
|
136
|
+
|
|
137
|
+
def set_out(self, index, value=None):
|
|
138
|
+
"""Set output state to value or toggle if value is None."""
|
|
139
|
+
if hasattr(self, "_set_out") and callable(self._set_out):
|
|
140
|
+
return self.get(self._set_out(index, value))
|
|
141
|
+
raise TinyToolsUnsupported(
|
|
142
|
+
"{} does not support controlling OUTs".format(self.__class__.__name__)
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def set_pwm(self, index, value):
|
|
146
|
+
"""Set pwm state to value or toggle if value is None."""
|
|
147
|
+
if hasattr(self, "_set_pwm") and callable(self._set_pwm):
|
|
148
|
+
return self.get(self._set_pwm(index, value))
|
|
149
|
+
raise TinyToolsUnsupported(
|
|
150
|
+
"{} does not support controlling PWMs".format(self.__class__.__name__)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def set_pwm_duty(self, index, value):
|
|
154
|
+
"""Set pwm duty to value."""
|
|
155
|
+
if hasattr(self, "_set_pwm_duty") and callable(self._set_pwm_duty):
|
|
156
|
+
return self.get(self._set_pwm_duty(index, value))
|
|
157
|
+
raise TinyToolsUnsupported(
|
|
158
|
+
"{} does not support controlling PWM duty".format(self.__class__.__name__)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def set_pwm_freq(self, index, value):
|
|
162
|
+
"""Set pwm freq to value."""
|
|
163
|
+
if hasattr(self, "_set_pwm_freq") and callable(self._set_pwm_freq):
|
|
164
|
+
return self.get(self._set_pwm_freq(index, value))
|
|
165
|
+
raise TinyToolsUnsupported(
|
|
166
|
+
"{} does not support controlling PWM freq".format(self.__class__.__name__)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def set_var(self, index, value):
|
|
170
|
+
"""Set VAR/EVENT variable to value."""
|
|
171
|
+
if hasattr(self, "_set_var") and callable(self._set_var):
|
|
172
|
+
return self.get(self._set_var(index, value))
|
|
173
|
+
raise TinyToolsUnsupported(
|
|
174
|
+
"{} does not support controlling VARs".format(self.__class__.__name__)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def set_ds(self, index: int, value: str):
|
|
178
|
+
"""Set ID of DS on position to value."""
|
|
179
|
+
if hasattr(self, "_set_ds") and callable(self._set_ds):
|
|
180
|
+
return self.get(self._set_ds(index, value))
|
|
181
|
+
raise TinyToolsUnsupported(
|
|
182
|
+
"{} does not support setting DSs".format(self.__class__.__name__)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def get_all(self) -> Dict[str, Any]:
|
|
186
|
+
"""Get set of all sensor/readings."""
|
|
187
|
+
if hasattr(self, "_get_all") and callable(self._get_all):
|
|
188
|
+
data = {}
|
|
189
|
+
for url in self._get_all():
|
|
190
|
+
data.update(self.get(url))
|
|
191
|
+
return data
|
|
192
|
+
raise TinyToolsUnsupported(
|
|
193
|
+
"{} does not support get_all command".format(self.__class__.__name__)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def reset_to_defaults(self):
|
|
197
|
+
"""Reset settings to defaults."""
|
|
198
|
+
if hasattr(self, "_reset_to_defaults") and callable(self._reset_to_defaults):
|
|
199
|
+
return self.get(self._reset_to_defaults())
|
|
200
|
+
raise TinyToolsUnsupported(
|
|
201
|
+
"{} does not support reset to defaults".format(self.__class__.__name__)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def restart(self):
|
|
205
|
+
"""Restart device."""
|
|
206
|
+
if hasattr(self, "_restart") and callable(self._restart):
|
|
207
|
+
return self.get(self._restart())
|
|
208
|
+
raise TinyToolsUnsupported(
|
|
209
|
+
"{} does not support restart command".format(self.__class__.__name__)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# region Async variants
|
|
213
|
+
async def async_get(
|
|
214
|
+
self,
|
|
215
|
+
path: str,
|
|
216
|
+
skip_keys: Union[List[str], None] = None,
|
|
217
|
+
remove_mapped_keys: bool = False,
|
|
218
|
+
**kwargs,
|
|
219
|
+
):
|
|
220
|
+
"""Async version of get."""
|
|
221
|
+
if self.session is None:
|
|
222
|
+
self.session = ClientSession()
|
|
223
|
+
self._close_session = True
|
|
224
|
+
response = await async_get(
|
|
225
|
+
self.host,
|
|
226
|
+
path,
|
|
227
|
+
schema=self.schema,
|
|
228
|
+
port=self.port,
|
|
229
|
+
username=self.username,
|
|
230
|
+
password=self.password,
|
|
231
|
+
session=self.session,
|
|
232
|
+
**kwargs,
|
|
233
|
+
)
|
|
234
|
+
return self._get(response.get("parsed"), path, skip_keys, remove_mapped_keys)
|
|
235
|
+
|
|
236
|
+
async def async_set_out(self, index, value=None):
|
|
237
|
+
if hasattr(self, "_set_out") and callable(self._set_out):
|
|
238
|
+
return await self.async_get(self._set_out(index, value))
|
|
239
|
+
raise TinyToolsUnsupported(
|
|
240
|
+
"{} does not support controlling OUTs".format(self.__class__.__name__)
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
async def async_set_pwm(self, index, value):
|
|
244
|
+
if hasattr(self, "_set_pwm") and callable(self._set_pwm):
|
|
245
|
+
return await self.async_get(self._set_pwm(index, value))
|
|
246
|
+
raise TinyToolsUnsupported(
|
|
247
|
+
"{} does not support controlling PWMs".format(self.__class__.__name__)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
async def async_set_var(self, index, value):
|
|
251
|
+
if hasattr(self, "_set_var") and callable(self._set_var):
|
|
252
|
+
return await self.async_get(self._set_var(index, value))
|
|
253
|
+
raise TinyToolsUnsupported(
|
|
254
|
+
"{} does not support controlling VARs".format(self.__class__.__name__)
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
async def async_get_all(self) -> Dict[str, Any]:
|
|
258
|
+
if hasattr(self, "_get_all") and callable(self._get_all):
|
|
259
|
+
data = {}
|
|
260
|
+
for url in self._get_all():
|
|
261
|
+
data.update(await self.async_get(url))
|
|
262
|
+
return data
|
|
263
|
+
raise TinyToolsUnsupported(
|
|
264
|
+
"{} does not support get_all command".format(self.__class__.__name__)
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# region Session handling for asyncio
|
|
268
|
+
async def close(self) -> None:
|
|
269
|
+
"""Close open client session."""
|
|
270
|
+
await self.session.close()
|
|
271
|
+
|
|
272
|
+
async def __aenter__(self) -> "DeviceModel":
|
|
273
|
+
"""Async enter.
|
|
274
|
+
|
|
275
|
+
Returns
|
|
276
|
+
-------
|
|
277
|
+
The Device object.
|
|
278
|
+
"""
|
|
279
|
+
return self
|
|
280
|
+
|
|
281
|
+
async def __aexit__(self, *_exc_info: object) -> None:
|
|
282
|
+
"""Async exit.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
----
|
|
286
|
+
_exc_info: Exec type.
|
|
287
|
+
"""
|
|
288
|
+
await self.close()
|
|
289
|
+
|
|
290
|
+
# endregion
|
|
291
|
+
# endregion
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@dataclass
|
|
295
|
+
class LK_HW_20_PS(DeviceModel):
|
|
296
|
+
"""Methods for working with Power Socket on LK2.0.
|
|
297
|
+
|
|
298
|
+
Note: for outputs it uses unified values 0 - off, 1 - on.
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
info: ClassVar[Union[DeviceInfo, None]] = DeviceInfo(
|
|
302
|
+
"IP Power Socket v1 (LK2.0)", # 5G10A/6G10A
|
|
303
|
+
FAMILY_PS,
|
|
304
|
+
)
|
|
305
|
+
mapping: ClassVar[Dict[str, Dict]] = {
|
|
306
|
+
# --- st0.xml
|
|
307
|
+
"out0": {"name": "out0", "format": int_inverted},
|
|
308
|
+
"out1": {"name": "out1", "format": int_inverted},
|
|
309
|
+
"out2": {"name": "out2", "format": int_inverted},
|
|
310
|
+
"out3": {"name": "out3", "format": int_inverted},
|
|
311
|
+
"out4": {"name": "out4", "format": int_inverted},
|
|
312
|
+
"out5": {"name": "out5", "format": int_inverted},
|
|
313
|
+
"out6": {"name": "out_negation", "format": int_inverted},
|
|
314
|
+
"di0": {"name": "iDValue1", "format": up_to_int},
|
|
315
|
+
"di1": {"name": "iDValue2", "format": up_to_int},
|
|
316
|
+
"ia0": {"name": "boardTemp", "format": float_div10},
|
|
317
|
+
"ia1": {"name": "ds1", "format": float_div10},
|
|
318
|
+
"ia2": {"name": "ds2", "format": float_div10},
|
|
319
|
+
"ia3": {"name": "ds3", "format": float_div10},
|
|
320
|
+
"ia4": {"name": "ds4", "format": float_div10},
|
|
321
|
+
"ia5": {"name": "iAValue1", "format": float_div100}, # Voltage input
|
|
322
|
+
"ia6": {"name": "boardVoltage", "format": float_div10},
|
|
323
|
+
# Doubled keys for time
|
|
324
|
+
"sec0": {"name": "uptimeSeconds", "format": int},
|
|
325
|
+
"sec1": {"name": "uptimeMinutes", "format": int},
|
|
326
|
+
"sec2": {"name": "uptimeHours", "format": int},
|
|
327
|
+
"sec3": {"name": "uptimeDays", "format": int},
|
|
328
|
+
"sec4": {"name": "time", "format": int},
|
|
329
|
+
"t": {"name": "time", "format": int},
|
|
330
|
+
# --- st2.xml
|
|
331
|
+
"ver": {"name": "software_version", "format": str},
|
|
332
|
+
"hw": {"name": "hardware_version", "format": lambda x: "2." + x},
|
|
333
|
+
"na": {"name": "hostname", "format": str},
|
|
334
|
+
"r0": {"name": "out0_reset_time", "format": int},
|
|
335
|
+
"r1": {"name": "out1_reset_time", "format": int},
|
|
336
|
+
"r2": {"name": "out2_reset_time", "format": int},
|
|
337
|
+
"r3": {"name": "out3_reset_time", "format": int},
|
|
338
|
+
"r4": {"name": "out4_reset_time", "format": int},
|
|
339
|
+
"r5": {"name": "out5_reset_time", "format": int},
|
|
340
|
+
"r6": {"name": "out0_name", "format": str},
|
|
341
|
+
"r7": {"name": "out1_name", "format": str},
|
|
342
|
+
"r8": {"name": "out2_name", "format": str},
|
|
343
|
+
"r9": {"name": "out3_name", "format": str},
|
|
344
|
+
"r10": {"name": "out4_name", "format": str},
|
|
345
|
+
"r11": {"name": "out5_name", "format": str},
|
|
346
|
+
# Autoswitch times 6*on + 6*off (X*X*X*...)
|
|
347
|
+
"a": {"name": "autoswitch_times", "format": str},
|
|
348
|
+
# Autoswitch enabled (int with bin values 000000)
|
|
349
|
+
"as": {"name": "autoswitch_active", "format": str},
|
|
350
|
+
# Names divided with *
|
|
351
|
+
"d": {"name": "dsName1-4_iAName1_iDName1-2", "format": str},
|
|
352
|
+
# --- board.xml (configuration like network, remote access, email, etc.)
|
|
353
|
+
# a0, a1, a2 - auto send trap settings
|
|
354
|
+
# Email
|
|
355
|
+
"b0": {"name": "email_host", "format": str},
|
|
356
|
+
"b1": {"name": "email_port", "format": int},
|
|
357
|
+
"b2": {"name": "email_username", "format": str},
|
|
358
|
+
"b3": {"name": "email_password", "format": str},
|
|
359
|
+
"b4": {"name": "email_to", "format": str},
|
|
360
|
+
"b5": {"name": "email_sender", "format": str},
|
|
361
|
+
"b26": {"name": "email_subject", "format": str},
|
|
362
|
+
# Network
|
|
363
|
+
"b6": {"name": "mac", "format": str},
|
|
364
|
+
"b7": {"name": "hostname", "format": str},
|
|
365
|
+
"b27": {"name": "dhcp", "format": bool}, # 'true' or ''
|
|
366
|
+
"b8": {"name": "ip_address", "format": str},
|
|
367
|
+
"b9": {"name": "gateway", "format": str},
|
|
368
|
+
"b10": {"name": "netmask", "format": str},
|
|
369
|
+
"b11": {"name": "dns_primary", "format": str},
|
|
370
|
+
"b12": {"name": "dns_secondary", "format": str},
|
|
371
|
+
"b13": {"name": "http_port", "format": int},
|
|
372
|
+
# Access
|
|
373
|
+
"b14": {"name": "admin_username", "format": str},
|
|
374
|
+
"b15": {"name": "admin_password", "format": str},
|
|
375
|
+
"b29": {"name": "basic_auth", "format": bool}, # 'true' or ''
|
|
376
|
+
"b30": {"name": "user_username", "format": str},
|
|
377
|
+
"b31": {"name": "user_password", "format": str},
|
|
378
|
+
# NTP
|
|
379
|
+
"b16": {"name": "ntp_host", "format": str},
|
|
380
|
+
"b17": {"name": "ntp_port", "format": int},
|
|
381
|
+
"b18": {"name": "ntp_interval", "format": int},
|
|
382
|
+
"b19": {"name": "ntp_timezone", "format": int},
|
|
383
|
+
# SNMP
|
|
384
|
+
"b20": {"name": "snmp_public_community", "format": str},
|
|
385
|
+
"b21": {"name": "snmp_public_community2", "format": str},
|
|
386
|
+
"b22": {"name": "snmp_private_community", "format": str},
|
|
387
|
+
"b23": {"name": "snmp_private_community2", "format": str},
|
|
388
|
+
"b24": {"name": "snmp_trap_ip", "format": str},
|
|
389
|
+
"b25": {"name": "snmp_trap_community", "format": str},
|
|
390
|
+
"b28": {"name": "snmp_trap_active", "format": bool}, # 'true' or ''
|
|
391
|
+
# r0, r1, r2 - remote control
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
@classmethod
|
|
395
|
+
def check_version(
|
|
396
|
+
cls, hardware_version: Union[str, None], software_version: str
|
|
397
|
+
) -> bool:
|
|
398
|
+
return hardware_version == "2.0" and software_version in [
|
|
399
|
+
"6.00",
|
|
400
|
+
"6.09",
|
|
401
|
+
"6.10",
|
|
402
|
+
"6.12a",
|
|
403
|
+
"6.12",
|
|
404
|
+
]
|
|
405
|
+
|
|
406
|
+
def _get(
|
|
407
|
+
self,
|
|
408
|
+
data: Dict[str, Any],
|
|
409
|
+
path: str,
|
|
410
|
+
skip_keys: Union[List[str], None] = None,
|
|
411
|
+
remove_mapped_keys: bool = False,
|
|
412
|
+
) -> Dict[str, Any]:
|
|
413
|
+
if path == "/board.xml":
|
|
414
|
+
if skip_keys is None:
|
|
415
|
+
skip_keys = set()
|
|
416
|
+
# Ignore few variables for /board.xml as they overlap in /st2.xml and /board.xml,
|
|
417
|
+
# but with different meaning (out reset time instead of remote control).
|
|
418
|
+
skip_keys.update({"r0", "r1", "r2", "r3", "r4"})
|
|
419
|
+
return super()._get(data, path, skip_keys, remove_mapped_keys)
|
|
420
|
+
|
|
421
|
+
def _set_out(
|
|
422
|
+
self, index: Union[int, List[int]], value: Union[int, List[int], None]
|
|
423
|
+
) -> str:
|
|
424
|
+
"""Prepare command for setting outputs OUT.
|
|
425
|
+
|
|
426
|
+
Arguments:
|
|
427
|
+
index: 0-5 (single or list)
|
|
428
|
+
value: 0-1 (single or list)
|
|
429
|
+
"""
|
|
430
|
+
cmd = "/outs.cgi?"
|
|
431
|
+
if isinstance(index, list):
|
|
432
|
+
if value is None:
|
|
433
|
+
cmd += "out=" + "".join(map(str, index))
|
|
434
|
+
elif isinstance(value, list):
|
|
435
|
+
cmd += "&".join(
|
|
436
|
+
[
|
|
437
|
+
"out{}={}".format(ix, int_inverted(val))
|
|
438
|
+
for ix, val in zip(index, value)
|
|
439
|
+
]
|
|
440
|
+
)
|
|
441
|
+
else:
|
|
442
|
+
cmd += "&".join(
|
|
443
|
+
["out{}={}".format(ix, int_inverted(value)) for ix in index]
|
|
444
|
+
)
|
|
445
|
+
else:
|
|
446
|
+
if value is None:
|
|
447
|
+
cmd += "out={}".format(index)
|
|
448
|
+
else:
|
|
449
|
+
cmd += "out{}={}".format(index, int_inverted(value))
|
|
450
|
+
# Fix for HW2.0 - HW2.0 uses NON inverted for out5=Y,
|
|
451
|
+
# so Y=1 to turn on and Y=0 to turn off.
|
|
452
|
+
if self.hardware_version == "2.0":
|
|
453
|
+
if "out5=0" in cmd:
|
|
454
|
+
cmd = cmd.replace("out5=0", "out5=1")
|
|
455
|
+
elif "out5=1" in cmd:
|
|
456
|
+
cmd = cmd.replace("out5=1", "out5=0")
|
|
457
|
+
return cmd
|
|
458
|
+
|
|
459
|
+
def _get_all(self) -> List[str]:
|
|
460
|
+
"""Prepare list of URLs to fetch data from."""
|
|
461
|
+
return ["/st0.xml", "/board.xml", "/st2.xml"]
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
@dataclass
|
|
465
|
+
class LK_HW_20(LK_HW_20_PS):
|
|
466
|
+
"""Methods for working with LK2.0.
|
|
467
|
+
|
|
468
|
+
Note: for outputs it uses unified values 0 - off, 1 - on.
|
|
469
|
+
"""
|
|
470
|
+
|
|
471
|
+
info: ClassVar[Union[DeviceInfo, None]] = DeviceInfo(
|
|
472
|
+
"LK HW 2.0",
|
|
473
|
+
FAMILY_LK,
|
|
474
|
+
"lc20",
|
|
475
|
+
"https://tinycontrol.pl/en/archives/lan-controller-20/#firmware",
|
|
476
|
+
)
|
|
477
|
+
mapping: ClassVar[Dict[str, Dict]] = {
|
|
478
|
+
# Overwrite PS mapping (there will be extra b30, b31)
|
|
479
|
+
**LK_HW_20_PS.mapping,
|
|
480
|
+
# --- st0.xml
|
|
481
|
+
"di2": {"name": "iDValue3", "format": up_to_int},
|
|
482
|
+
"di3": {"name": "iDValue4", "format": up_to_int},
|
|
483
|
+
"ia0": {"name": "boardTemp", "format": float_div10},
|
|
484
|
+
"ia1": {"name": "boardVoltage", "format": float_div10},
|
|
485
|
+
"ia2": {"name": "iAValue1", "format": float_div100},
|
|
486
|
+
"ia3": {"name": "iAValue2", "format": float_div100},
|
|
487
|
+
"ia4": {"name": "iAValue3", "format": float_div10},
|
|
488
|
+
"ia5": {"name": "iAValue4", "format": float_div100},
|
|
489
|
+
"ia6": {"name": "iAValue5", "format": float_div10},
|
|
490
|
+
"ia7": {"name": "ds1", "format": float_div10},
|
|
491
|
+
"ia8": {"name": "ds2", "format": float_div10},
|
|
492
|
+
"ia9": {"name": "ds3", "format": float_div10},
|
|
493
|
+
"ia10": {"name": "ds4", "format": float_div10},
|
|
494
|
+
"ia11": {"name": "ds5", "format": float_div10},
|
|
495
|
+
"ia12": {"name": "ds6", "format": float_div10},
|
|
496
|
+
"ia13": {"name": "dth22 temp", "format": float_div10},
|
|
497
|
+
"ia14": {"name": "dth22 hum", "format": float_div10},
|
|
498
|
+
"ia15": {"name": "power1", "format": float_div1000}, # power1 is iA4*iA5
|
|
499
|
+
"ia16": {"name": "energy1", "format": float_div1000},
|
|
500
|
+
"ia17": {
|
|
501
|
+
"name": "power2",
|
|
502
|
+
"format": float_div1000,
|
|
503
|
+
}, # power2 is iD4 impulse counter
|
|
504
|
+
"ia18": {"name": "inp4d_ia18", "format": float_div1000}, # No idea what is it
|
|
505
|
+
"ia19": {"name": "diff1", "format": float_div10},
|
|
506
|
+
"freq": {"name": "pwmFrequency0", "format": int},
|
|
507
|
+
"duty": {"name": "pwmDuty0", "format": float_div10},
|
|
508
|
+
"pwm": {"name": "pwm0", "format": int},
|
|
509
|
+
# --- st2.xml
|
|
510
|
+
# Calibrations
|
|
511
|
+
"k0": {"name": "cal_board_temp", "format": float_div10},
|
|
512
|
+
"k1": {"name": "cal_board_voltage", "format": float_div10},
|
|
513
|
+
"k2": {"name": "cal_iA1", "format": float_div100},
|
|
514
|
+
"k3": {"name": "cal_iA2", "format": float_div100},
|
|
515
|
+
"k4": {"name": "cal_iA3", "format": float_div10},
|
|
516
|
+
"k5": {"name": "cal_iA4", "format": float_div100},
|
|
517
|
+
"k6": {"name": "cal_iA5", "format": float_div10},
|
|
518
|
+
"k7": {"name": "cal_iA1_sensor", "format": float_div10},
|
|
519
|
+
"k8": {"name": "cal_iA4_sensor", "format": float_div10},
|
|
520
|
+
"k9": {"name": "cal_iA5_sensor", "format": float_div10},
|
|
521
|
+
"k10": {"name": "diff1_part1", "format": int},
|
|
522
|
+
"k11": {"name": "diff1_part2", "format": int},
|
|
523
|
+
# Names divided with *
|
|
524
|
+
"d": {"name": "dsName1-6_iDName1-4", "format": str},
|
|
525
|
+
"dz": {"name": "power2_iD4_divisor", "format": int},
|
|
526
|
+
"mm": {"name": "power2_iD4_unit", "format": str},
|
|
527
|
+
"mh": {"name": "power2_iD4_divisor2", "format": int},
|
|
528
|
+
# Negation of iD (int with bin 0000)
|
|
529
|
+
"db": {"name": "iD_negation", "format": str},
|
|
530
|
+
# --- board.xml
|
|
531
|
+
"ds": {"name": "ds_read_id", "format": str},
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
@classmethod
|
|
535
|
+
def check_version(
|
|
536
|
+
cls, hardware_version: Union[str, None], software_version: str
|
|
537
|
+
) -> bool:
|
|
538
|
+
return hardware_version == "2.0" and software_version not in [
|
|
539
|
+
"6.00",
|
|
540
|
+
"6.09",
|
|
541
|
+
"6.10",
|
|
542
|
+
"6.12a",
|
|
543
|
+
"6.12",
|
|
544
|
+
]
|
|
545
|
+
|
|
546
|
+
def _set_pwm(
|
|
547
|
+
self, index: Union[int, List[int]], value: Union[int, List[int]]
|
|
548
|
+
) -> str:
|
|
549
|
+
"""Prepare command for setting PWM.
|
|
550
|
+
|
|
551
|
+
Arguments:
|
|
552
|
+
index: 0-3 (single or list)
|
|
553
|
+
value: 0-1 (single or list)
|
|
554
|
+
NOTE: 1-3 can be only set, not read
|
|
555
|
+
"""
|
|
556
|
+
cmd = "/ind.cgi?"
|
|
557
|
+
if isinstance(index, list):
|
|
558
|
+
if isinstance(value, list):
|
|
559
|
+
cmd += "&".join(
|
|
560
|
+
["pwm{}={}".format(ix, val) for ix, val in zip(index, value)]
|
|
561
|
+
)
|
|
562
|
+
else:
|
|
563
|
+
cmd += "&".join(["pwm{}={}".format(ix, value) for ix in index])
|
|
564
|
+
else:
|
|
565
|
+
cmd += "pwm{}={}".format(index, value)
|
|
566
|
+
cmd = cmd.replace("pwm0", "pwm")
|
|
567
|
+
return cmd
|
|
568
|
+
|
|
569
|
+
def _set_pwm_duty(
|
|
570
|
+
self, index: Union[int, List[int]], value: Union[int, List[int]]
|
|
571
|
+
) -> str:
|
|
572
|
+
"""Prepare command for setting PWM duty.
|
|
573
|
+
|
|
574
|
+
Arguments:
|
|
575
|
+
index: 0-3 (single or list)
|
|
576
|
+
value: 0-100 (single or list)
|
|
577
|
+
NOTE: 1-3 can be only set, not read
|
|
578
|
+
"""
|
|
579
|
+
cmd = "/ind.cgi?"
|
|
580
|
+
if isinstance(index, list):
|
|
581
|
+
if isinstance(value, list):
|
|
582
|
+
cmd += "&".join(
|
|
583
|
+
[
|
|
584
|
+
"pwmd{}={}".format(ix, int(val * 10))
|
|
585
|
+
for ix, val in zip(index, value)
|
|
586
|
+
]
|
|
587
|
+
)
|
|
588
|
+
else:
|
|
589
|
+
cmd += "&".join(
|
|
590
|
+
["pwmd{}={}".format(ix, int(value * 10)) for ix in index]
|
|
591
|
+
)
|
|
592
|
+
else:
|
|
593
|
+
cmd += "pwmd{}={}".format(index, int(value * 10))
|
|
594
|
+
cmd = cmd.replace("pwmd0", "pwmd")
|
|
595
|
+
return cmd
|
|
596
|
+
|
|
597
|
+
def _set_pwm_freq(self, index: Any, value: int) -> str:
|
|
598
|
+
"""Prepare command for setting PWM freq.
|
|
599
|
+
|
|
600
|
+
Arguments:
|
|
601
|
+
index: not used (only one frequency)
|
|
602
|
+
value: 2_600-4_000_000
|
|
603
|
+
"""
|
|
604
|
+
return "/ind.cgi?pwmf={}".format(value)
|
|
605
|
+
|
|
606
|
+
def _set_ds(self, index: int, value: Any = None) -> str:
|
|
607
|
+
"""Set ID of DS on position to value.
|
|
608
|
+
|
|
609
|
+
Arguments:
|
|
610
|
+
index - 1-6
|
|
611
|
+
value - not used for LK2.X
|
|
612
|
+
"""
|
|
613
|
+
cmd = "/ind.cgi?ds={}".format(index)
|
|
614
|
+
return cmd
|
|
615
|
+
|
|
616
|
+
def get_ds_id(self) -> str:
|
|
617
|
+
"""Get ID of detected DS."""
|
|
618
|
+
self.get("/ind.cgi?ds=0")
|
|
619
|
+
return self.get("/board.xml").get("ds_read_id")
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
@dataclass
|
|
623
|
+
class LK_HW_25(LK_HW_20):
|
|
624
|
+
"""Methods for working with LK2.5.
|
|
625
|
+
|
|
626
|
+
Note: for outputs it uses unified values 0 - off, 1 - on.
|
|
627
|
+
"""
|
|
628
|
+
|
|
629
|
+
info: ClassVar[Union[DeviceInfo, None]] = DeviceInfo(
|
|
630
|
+
"LK HW 2.5",
|
|
631
|
+
FAMILY_LK,
|
|
632
|
+
"lc25",
|
|
633
|
+
"https://tinycontrol.pl/en/lan-controller-25/firmware-docs/#firmware",
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
@classmethod
|
|
637
|
+
def check_version(
|
|
638
|
+
cls, hardware_version: Union[str, None], software_version: str
|
|
639
|
+
) -> bool:
|
|
640
|
+
return hardware_version == "2.5" and software_version != "6.15"
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
@dataclass
|
|
644
|
+
class LK_HW_25_PS(LK_HW_20_PS):
|
|
645
|
+
info: ClassVar[Union[DeviceInfo, None]] = DeviceInfo(
|
|
646
|
+
"IP Power Socket v2 (LK2.5)",
|
|
647
|
+
FAMILY_PS,
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
@classmethod
|
|
651
|
+
def check_version(
|
|
652
|
+
cls, hardware_version: Union[str, None], software_version: str
|
|
653
|
+
) -> bool:
|
|
654
|
+
return hardware_version == "2.5" and software_version == "6.15"
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
@dataclass
|
|
658
|
+
class LK_HW_30(DeviceModel):
|
|
659
|
+
"""Methods for working with LK3.0.
|
|
660
|
+
|
|
661
|
+
Note that INPD/digital for HW 3.5+ SW 1.49+ includes negation
|
|
662
|
+
right in response from LK. Previous SW and HW 3.0 do NOT.
|
|
663
|
+
"""
|
|
664
|
+
|
|
665
|
+
info: ClassVar[Union[DeviceInfo, None]] = DeviceInfo(
|
|
666
|
+
"LK HW 3.0",
|
|
667
|
+
FAMILY_LK,
|
|
668
|
+
"lc30",
|
|
669
|
+
"https://tinycontrol.pl/en/archives/lan-controller-30/#firmware",
|
|
670
|
+
{"number_of_outputs": 6},
|
|
671
|
+
)
|
|
672
|
+
mapping: ClassVar[Dict[str, Dict]] = {
|
|
673
|
+
# OUTs - further parsed with _parse_outs
|
|
674
|
+
"out0": {"name": "out0", "format": int},
|
|
675
|
+
"out1": {"name": "out1", "format": int},
|
|
676
|
+
"out2": {"name": "out2", "format": int},
|
|
677
|
+
"out3": {"name": "out3", "format": int},
|
|
678
|
+
"out4": {"name": "out4", "format": int},
|
|
679
|
+
"out5": {"name": "out5", "format": int},
|
|
680
|
+
"out": {"name": name_list("out", 6, 0), "format": strint_to_int_list(6)},
|
|
681
|
+
# PWM
|
|
682
|
+
"pwm": {"name": name_list("pwm", 4, 0), "format": strint_to_int_list(4)},
|
|
683
|
+
"pwmd0": {"name": "pwmDuty0", "format": int},
|
|
684
|
+
"pwmd1": {"name": "pwmDuty1", "format": int},
|
|
685
|
+
"pwmd2": {"name": "pwmDuty2", "format": int},
|
|
686
|
+
"pwmd3": {"name": "pwmDuty3", "format": int},
|
|
687
|
+
"pwmf0": {"name": "pwmFrequency0", "format": int},
|
|
688
|
+
"pwmf1": {"name": "pwmFrequency13", "format": int},
|
|
689
|
+
# EVENT
|
|
690
|
+
"eventVariables": {
|
|
691
|
+
"name": name_list("event", 8),
|
|
692
|
+
"format": strint_to_int_list(8),
|
|
693
|
+
},
|
|
694
|
+
# analog
|
|
695
|
+
"inpp1": {"name": "iAValue1", "format": float_div100},
|
|
696
|
+
"inpp2": {"name": "iAValue2", "format": float_div100},
|
|
697
|
+
"inpp3": {"name": "iAValue3", "format": float_div100},
|
|
698
|
+
"inpp4": {"name": "iAValue4", "format": float_div100},
|
|
699
|
+
"inpp5": {"name": "iAValue5", "format": float_div100},
|
|
700
|
+
"inpp6": {"name": "iAValue6", "format": float_div100},
|
|
701
|
+
# ds
|
|
702
|
+
"ds1": {"name": "ds1", "format": float_div10},
|
|
703
|
+
"ds2": {"name": "ds2", "format": float_div10},
|
|
704
|
+
"ds3": {"name": "ds3", "format": float_div10},
|
|
705
|
+
"ds4": {"name": "ds4", "format": float_div10},
|
|
706
|
+
"ds5": {"name": "ds5", "format": float_div10},
|
|
707
|
+
"ds6": {"name": "ds6", "format": float_div10},
|
|
708
|
+
"ds7": {"name": "ds7", "format": float_div10},
|
|
709
|
+
"ds8": {"name": "ds8", "format": float_div10},
|
|
710
|
+
# i2c
|
|
711
|
+
"dthTemp": {"name": "i2cTemp", "format": float_div10},
|
|
712
|
+
"dthHum": {"name": "i2cHum", "format": float_div10},
|
|
713
|
+
"bm280p": {"name": "i2cPressure", "format": float_div100},
|
|
714
|
+
"dewPoint": {"name": "dewPoint", "format": float_div10},
|
|
715
|
+
# pm1-10
|
|
716
|
+
"pm1": {"name": "pm1.0", "format": float_div10},
|
|
717
|
+
"pm2": {"name": "pm2.5", "format": float_div10},
|
|
718
|
+
"pm4": {"name": "pm4.0", "format": float_div10},
|
|
719
|
+
"pm10": {"name": "pm10.0", "format": float_div10},
|
|
720
|
+
# co2
|
|
721
|
+
"co2": {"name": "co2", "format": int},
|
|
722
|
+
# m1-30 - parsed in _parse_custom_readings
|
|
723
|
+
# diffs w/ diffConfig - they have special parsing _parse_diffs
|
|
724
|
+
# digital
|
|
725
|
+
"ind": {"name": name_list("iDValue", 4), "format": strint_to_int_list(4)},
|
|
726
|
+
"inpdnn": {"name": name_list("iDNegation", 4), "format": strint_to_int_list(4)},
|
|
727
|
+
# power and energy
|
|
728
|
+
"power1": {"name": "power1", "format": float_div1000},
|
|
729
|
+
"power2": {"name": "power2", "format": float_div1000},
|
|
730
|
+
"power3": {"name": "power3", "format": float_div1000},
|
|
731
|
+
"power4": {"name": "power4", "format": float_div1000},
|
|
732
|
+
"power5": {"name": "power5", "format": float_div1000},
|
|
733
|
+
"power6": {"name": "power6", "format": float_div1000},
|
|
734
|
+
"energy1": {"name": "energy1", "format": float_div1000},
|
|
735
|
+
"energy2": {"name": "energy2", "format": float_div1000},
|
|
736
|
+
"energy3": {"name": "energy3", "format": float_div1000},
|
|
737
|
+
"energy4": {"name": "energy4", "format": float_div1000},
|
|
738
|
+
"energy5": {"name": "energy5", "format": float_div1000},
|
|
739
|
+
"energy6": {"name": "energy6", "format": float_div1000},
|
|
740
|
+
# serial port sensors?
|
|
741
|
+
"distanceSensor": {"name": "distanceSensor", "format": float},
|
|
742
|
+
"ozon": {"name": "ozon", "format": int}, # the same value as co2
|
|
743
|
+
# "rhewa": {"name": 'rhewa', "format": str},
|
|
744
|
+
# "barcode": {"name": 'barcode', "format": str},
|
|
745
|
+
# general stuff
|
|
746
|
+
"hw": {"name": "hardware_version", "format": str},
|
|
747
|
+
"sw": {"name": "software_version", "format": str},
|
|
748
|
+
"ip4": {"name": "mac", "format": str},
|
|
749
|
+
"vin": {"name": "boardVoltage", "format": float_div100},
|
|
750
|
+
"tem": {"name": "boardTemp", "format": float_div100},
|
|
751
|
+
"time": {"name": "time", "format": int},
|
|
752
|
+
"sname": {"name": "hostname", "format": str},
|
|
753
|
+
# others
|
|
754
|
+
"inpd1tim": {"name": "digital1_impulse_timer", "format": int},
|
|
755
|
+
"inpd3tim": {"name": "digital3_impulse_timer", "format": int},
|
|
756
|
+
# DS reading
|
|
757
|
+
"dsid": {"name": "ds_read_id", "format": str},
|
|
758
|
+
"d0": {"name": "duralux0", "format": float_div10},
|
|
759
|
+
"d1": {"name": "duralux1", "format": float_div10},
|
|
760
|
+
"d2": {"name": "duralux2", "format": float_div10},
|
|
761
|
+
"d3": {"name": "duralux3", "format": float_div10},
|
|
762
|
+
"d4": {"name": "duralux4", "format": float_div100},
|
|
763
|
+
"d5": {"name": "duralux5", "format": int},
|
|
764
|
+
"d6": {"name": "duralux6", "format": int},
|
|
765
|
+
"d7": {"name": "duralux7", "format": float_div10},
|
|
766
|
+
"d8": {"name": "duralux8", "format": int},
|
|
767
|
+
}
|
|
768
|
+
parsers: ClassVar[List[str]] = [
|
|
769
|
+
"_parse_outs",
|
|
770
|
+
"_parse_diffs",
|
|
771
|
+
"_parse_custom_readings",
|
|
772
|
+
]
|
|
773
|
+
|
|
774
|
+
@classmethod
|
|
775
|
+
def check_version(
|
|
776
|
+
cls, hardware_version: Union[str, None], software_version: str
|
|
777
|
+
) -> bool:
|
|
778
|
+
return hardware_version == "3.0"
|
|
779
|
+
|
|
780
|
+
# region Parser methods that modifies data in _get()
|
|
781
|
+
def _parse_diff(self, value, source, diffsel, depth=3):
|
|
782
|
+
"""Parse value of diff according to source sensor.
|
|
783
|
+
|
|
784
|
+
Valid for HW3.0 and HW 3.5+ up to SW 1.47a
|
|
785
|
+
"""
|
|
786
|
+
if depth == 0:
|
|
787
|
+
return 0
|
|
788
|
+
if source < 6 or source == 24:
|
|
789
|
+
return value / 100
|
|
790
|
+
elif source < 14 or source == 22 or source == 23:
|
|
791
|
+
return value / 10
|
|
792
|
+
elif source >= 25 and source <= 27:
|
|
793
|
+
return self._parse_diff(value, diffsel[source - 25], diffsel, depth - 1)
|
|
794
|
+
else:
|
|
795
|
+
return value / 1000
|
|
796
|
+
|
|
797
|
+
def _parse_diffs(self, data: Dict[str, Any], path: str) -> None:
|
|
798
|
+
"""Parse diffs according to sw/hw versions if set."""
|
|
799
|
+
if "diff1" not in data:
|
|
800
|
+
return
|
|
801
|
+
if self.hardware_version != "3.0" and self.software_version >= "1.49":
|
|
802
|
+
# HW3.5+ SW 1.49+ - Version with 6 diffs
|
|
803
|
+
for index in range(1, 7):
|
|
804
|
+
key = "diff{}".format(index)
|
|
805
|
+
data[key] = float_div1000(data[key])
|
|
806
|
+
elif "diffsel" in data:
|
|
807
|
+
# HW 3.0 or HW3.5+ SW <1.49 - Version with 3 diffs.
|
|
808
|
+
# Requires diffsel to parse, so currently it will only work for all.json.
|
|
809
|
+
diffsel = list(map(int, data.get("diffsel").split("*")))
|
|
810
|
+
if diffsel[0] != 25 and diffsel[1] != 26 and diffsel[2] != 27:
|
|
811
|
+
# No pointing itself 25-27 is diff1-3
|
|
812
|
+
for index in range(1, 4):
|
|
813
|
+
key = "diff{}".format(index)
|
|
814
|
+
data[key] = self._parse_diff(
|
|
815
|
+
float(data[key]), diffsel[index - 1], diffsel
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
def _parse_custom_readings(self, data: Dict[str, Any], path: str) -> None:
|
|
819
|
+
"""Parse readings mappings (Modbus/1 Wire) and HW 3.0 SDMs."""
|
|
820
|
+
if not (self.hardware_version and self.software_version):
|
|
821
|
+
# No data
|
|
822
|
+
return
|
|
823
|
+
if self.hardware_version == "3.0" or (
|
|
824
|
+
self.hardware_version != "3.0" and self.software_version <= "1.31"
|
|
825
|
+
):
|
|
826
|
+
# LK HW 3.0 or HW 3.5+ SW <=1.31 - Dict method for sdm1-sdm14, rest is unknown
|
|
827
|
+
if "sdm1" not in data:
|
|
828
|
+
return
|
|
829
|
+
for index in range(1, 15):
|
|
830
|
+
data["mValue{}".format(index)] = float_div100(data["sdm{}".format(index)])
|
|
831
|
+
for index in range(15, 31):
|
|
832
|
+
data["mValue{}".format(index)] = 0
|
|
833
|
+
elif self.hardware_version != "3.0" and self.software_version < "1.33":
|
|
834
|
+
# HW 3.5+ SW 1.32+ - List method for sdm1-sdm29
|
|
835
|
+
if "modbusSensor" not in data or "sdm1" not in data:
|
|
836
|
+
return
|
|
837
|
+
modbus_sensor = int(data.get("modbusSensor", 0))
|
|
838
|
+
# Prepare list of 30 values
|
|
839
|
+
tmp = list_map(int)(data.get("sdm")) + [0]
|
|
840
|
+
if modbus_sensor == 5:
|
|
841
|
+
tmp[6] = tmp[6] / 10
|
|
842
|
+
tmp[7] = tmp[7] / 100
|
|
843
|
+
tmp[8] = tmp[8] / 10
|
|
844
|
+
tmp[9] = tmp[9] / 100
|
|
845
|
+
tmp[10] = tmp[10] / 100
|
|
846
|
+
tmp[11] = tmp[11] / 100
|
|
847
|
+
tmp[12] = tmp[12] / 100
|
|
848
|
+
tmp[13] = tmp[13] / 100
|
|
849
|
+
tmp[14] = tmp[14] / 100
|
|
850
|
+
tmp[15] = tmp[15] / 10
|
|
851
|
+
tmp[16] = tmp[16] / 100
|
|
852
|
+
tmp[17] = tmp[17] / 10
|
|
853
|
+
tmp[18] = tmp[18] / 100
|
|
854
|
+
tmp[19] = tmp[19] / 10
|
|
855
|
+
tmp[20] = tmp[20] / 100
|
|
856
|
+
tmp[25] = tmp[25] / 100
|
|
857
|
+
elif modbus_sensor == 4:
|
|
858
|
+
for index in range(0, 9): # 0-8
|
|
859
|
+
tmp[index] = tmp[index] / 100
|
|
860
|
+
for index in range(13, 29): # 13-28
|
|
861
|
+
tmp[index] = tmp[index] / 100
|
|
862
|
+
else:
|
|
863
|
+
for index in range(0, 29): # 0-28
|
|
864
|
+
tmp[index] = tmp[index] / 100
|
|
865
|
+
# Update data with changes above
|
|
866
|
+
for index in range(0, 30):
|
|
867
|
+
data["mValue{}".format(index + 1)] = tmp[index]
|
|
868
|
+
elif self.hardware_version != "3.0" and self.software_version < "1.50":
|
|
869
|
+
# HW 3.5+ SW 1.33+ (3 modbus slots)
|
|
870
|
+
# modbusMapping points to positions in modbusReadings
|
|
871
|
+
if "modbusMapping" not in data or "modbusReadings" not in data:
|
|
872
|
+
return
|
|
873
|
+
modbus_mapping = [
|
|
874
|
+
(int(item[0]), int(item[1:]))
|
|
875
|
+
for item in data.get("modbusMapping").split("*")
|
|
876
|
+
]
|
|
877
|
+
modbus_values = data.get("modbusReadings")
|
|
878
|
+
tmp = []
|
|
879
|
+
for index, item in enumerate(modbus_mapping):
|
|
880
|
+
if item[0] == 0:
|
|
881
|
+
tmp.append(0)
|
|
882
|
+
else:
|
|
883
|
+
# Slots are 1-3, so `-1`
|
|
884
|
+
tmp.append(float(modbus_values[item[0] - 1][item[1]]))
|
|
885
|
+
# Update data with changes above
|
|
886
|
+
for index in range(0, 30):
|
|
887
|
+
data["mValue{}".format(index + 1)] = tmp[index]
|
|
888
|
+
elif self.hardware_version != "3.0" and self.software_version >= "1.50":
|
|
889
|
+
# HW 3.5+ SW 1.50+ - direct access to m1-30
|
|
890
|
+
if "customReadings" not in data:
|
|
891
|
+
return
|
|
892
|
+
for index, item in enumerate(data.get("customReadings")):
|
|
893
|
+
data["mValue{}".format(index + 1)] = float(item)
|
|
894
|
+
|
|
895
|
+
def _parse_outs(self, data: Dict[str, Any], path: str) -> None:
|
|
896
|
+
"""Parse outputs OUT including negation."""
|
|
897
|
+
if "outnn" in data:
|
|
898
|
+
self._context["out_negation"] = int(data.get("outnn"))
|
|
899
|
+
if "out0" in data:
|
|
900
|
+
out_negation = self._context.get("out_negation")
|
|
901
|
+
for name in name_list("out", self.info.extras["number_of_outputs"], 0):
|
|
902
|
+
data[name] = (
|
|
903
|
+
int_inverted(data[name]) if out_negation else int(data[name])
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
# endregion
|
|
907
|
+
|
|
908
|
+
def _set_out(
|
|
909
|
+
self, index: Union[int, List[int]], value: Union[int, List[int], None]
|
|
910
|
+
) -> str:
|
|
911
|
+
"""Prepare command for setting outputs OUT.
|
|
912
|
+
|
|
913
|
+
Arguments:
|
|
914
|
+
index: 0-5 (single or list)
|
|
915
|
+
value: 0-1 (single or list)
|
|
916
|
+
NOTE: When out is negated it will negate passed value,
|
|
917
|
+
so value=1 will actually set 0 and value=0 set 1.
|
|
918
|
+
"""
|
|
919
|
+
cmd = "/outs.cgi?"
|
|
920
|
+
if value is not None and self._context.get("out_negation"):
|
|
921
|
+
if isinstance(value, list):
|
|
922
|
+
value = [int_inverted(val) for val in value]
|
|
923
|
+
else:
|
|
924
|
+
value = int_inverted(value)
|
|
925
|
+
if isinstance(index, list):
|
|
926
|
+
if value is None:
|
|
927
|
+
cmd += "&".join(["out=out{}".format(ix) for ix in index])
|
|
928
|
+
elif isinstance(value, list):
|
|
929
|
+
cmd += "&".join(
|
|
930
|
+
["out{}={}".format(ix, val) for ix, val in zip(index, value)]
|
|
931
|
+
)
|
|
932
|
+
else:
|
|
933
|
+
cmd += "&".join(["out{}={}".format(ix, value) for ix in index])
|
|
934
|
+
else:
|
|
935
|
+
if value is None:
|
|
936
|
+
cmd += "out=out{}".format(index)
|
|
937
|
+
else:
|
|
938
|
+
cmd += "out{}={}".format(index, value)
|
|
939
|
+
return cmd
|
|
940
|
+
|
|
941
|
+
def _set_pwm(
|
|
942
|
+
self, index: Union[int, List[int]], value: Union[int, List[int], None]
|
|
943
|
+
) -> str:
|
|
944
|
+
"""Prepare command for setting PWM.
|
|
945
|
+
|
|
946
|
+
Arguments:
|
|
947
|
+
index: 0-3 (single or list)
|
|
948
|
+
value: 0-1 (single or list)
|
|
949
|
+
"""
|
|
950
|
+
cmd = "/outs.cgi?"
|
|
951
|
+
if isinstance(index, list):
|
|
952
|
+
if value is None:
|
|
953
|
+
cmd += "&".join(["pwm=pwm{}".format(ix) for ix in index])
|
|
954
|
+
elif isinstance(value, list):
|
|
955
|
+
cmd += "&".join(
|
|
956
|
+
["pwm{}={}".format(ix, val) for ix, val in zip(index, value)]
|
|
957
|
+
)
|
|
958
|
+
else:
|
|
959
|
+
cmd += "&".join(["pwm{}={}".format(ix, value) for ix in index])
|
|
960
|
+
else:
|
|
961
|
+
if value is None:
|
|
962
|
+
cmd += "pwm=pwm{}".format(index)
|
|
963
|
+
else:
|
|
964
|
+
cmd += "pwm{}={}".format(index, value)
|
|
965
|
+
return cmd
|
|
966
|
+
|
|
967
|
+
def _set_pwm_duty(
|
|
968
|
+
self, index: Union[int, List[int]], value: Union[int, List[int]]
|
|
969
|
+
) -> str:
|
|
970
|
+
"""Set pwm duty to value.
|
|
971
|
+
|
|
972
|
+
Arguments:
|
|
973
|
+
index: 0-3 (single or list)
|
|
974
|
+
value: 0-100 (single or list)
|
|
975
|
+
"""
|
|
976
|
+
cmd = "/stm.cgi?"
|
|
977
|
+
if isinstance(index, list):
|
|
978
|
+
if isinstance(value, list):
|
|
979
|
+
cmd += "&".join(
|
|
980
|
+
["pwmd={}{}".format(ix, int(val)) for ix, val in zip(index, value)]
|
|
981
|
+
)
|
|
982
|
+
else:
|
|
983
|
+
cmd += "&".join(["pwmd={}{}".format(ix, int(value)) for ix in index])
|
|
984
|
+
else:
|
|
985
|
+
cmd += "pwmd={}{}".format(index, int(value))
|
|
986
|
+
return cmd
|
|
987
|
+
|
|
988
|
+
def _set_pwm_freq(
|
|
989
|
+
self, index: Union[int, List[int]], value: Union[int, List[int]]
|
|
990
|
+
) -> str:
|
|
991
|
+
"""Set pwm freq to value.
|
|
992
|
+
|
|
993
|
+
Arguments:
|
|
994
|
+
index: 0-1 (single or list; 0 - pwm0, 1 - pwm1-3 shared)
|
|
995
|
+
value: 1-1_000_000
|
|
996
|
+
"""
|
|
997
|
+
cmd = "/stm.cgi?"
|
|
998
|
+
if isinstance(index, list):
|
|
999
|
+
if isinstance(value, list):
|
|
1000
|
+
cmd += "&".join(
|
|
1001
|
+
["pwmf={}{}".format(ix, int(val)) for ix, val in zip(index, value)]
|
|
1002
|
+
)
|
|
1003
|
+
else:
|
|
1004
|
+
cmd += "&".join(["pwmf={}{}".format(ix, int(value)) for ix in index])
|
|
1005
|
+
else:
|
|
1006
|
+
cmd += "pwmf={}{}".format(index, int(value))
|
|
1007
|
+
return cmd
|
|
1008
|
+
|
|
1009
|
+
def _set_ds(
|
|
1010
|
+
self, index: Union[int, List[int]], value: Union[str, List[str]]
|
|
1011
|
+
) -> str:
|
|
1012
|
+
"""Set ID of DS on position to value.
|
|
1013
|
+
|
|
1014
|
+
Arguments:
|
|
1015
|
+
index: 1-8
|
|
1016
|
+
value: DS ID
|
|
1017
|
+
"""
|
|
1018
|
+
cmd = "/stm.cgi?"
|
|
1019
|
+
if isinstance(index, list):
|
|
1020
|
+
if isinstance(value, list):
|
|
1021
|
+
cmd += "&".join(
|
|
1022
|
+
["dswrite={}:{}".format(ix, val) for ix, val in zip(index, value)]
|
|
1023
|
+
)
|
|
1024
|
+
else:
|
|
1025
|
+
cmd += "&".join(["dswrite={}:{}".format(ix, value) for ix in index])
|
|
1026
|
+
else:
|
|
1027
|
+
cmd += "dswrite={}:{}".format(index, value)
|
|
1028
|
+
return cmd
|
|
1029
|
+
|
|
1030
|
+
def get_ds_id(self) -> str:
|
|
1031
|
+
"""Get ID of detected DS."""
|
|
1032
|
+
self.get("/stm.cgi?dswrite=0")
|
|
1033
|
+
return self.get("/json/dsi2c.json").get("ds_read_id")
|
|
1034
|
+
|
|
1035
|
+
def _get_all(self) -> List[str]:
|
|
1036
|
+
"""Prepare list of URLs to fetch data from."""
|
|
1037
|
+
urls = ["/json/all.json", "/json/pwmpid.json"]
|
|
1038
|
+
if self.hardware_version >= "3.5" and "1.50" > self.software_version >= "1.22b":
|
|
1039
|
+
urls.append("/json/events_per.json")
|
|
1040
|
+
return urls
|
|
1041
|
+
|
|
1042
|
+
def _reset_to_defaults(self):
|
|
1043
|
+
"""Reset settings to defaults."""
|
|
1044
|
+
return "/stm.cgi?eeprom_reset=1"
|
|
1045
|
+
|
|
1046
|
+
def _restart(self):
|
|
1047
|
+
"""Restart device."""
|
|
1048
|
+
return "/stm.cgi?upgrade=lkstart3"
|
|
1049
|
+
|
|
1050
|
+
def set_analog_input(
|
|
1051
|
+
self,
|
|
1052
|
+
index: Union[int, List[int]],
|
|
1053
|
+
sensor: Union[int, List[int]],
|
|
1054
|
+
calibration: Union[int, List[int], None] = None,
|
|
1055
|
+
multiplier: Union[float, List[float], None] = None,
|
|
1056
|
+
) -> Dict[str, Any]:
|
|
1057
|
+
"""Set sensor, calibration and multiplier for analog input.
|
|
1058
|
+
|
|
1059
|
+
Arguments:
|
|
1060
|
+
index: 1-6 (analog input index)
|
|
1061
|
+
sensor: 0-20, sensor to set (range varies depending on index)
|
|
1062
|
+
calibration: -32768 - 32767 (calibration offset)
|
|
1063
|
+
multiplier: 0.01 - 327.67 (before sending it will be int(X*100)
|
|
1064
|
+
"""
|
|
1065
|
+
cmd = "/inpa.cgi?"
|
|
1066
|
+
if isinstance(index, list):
|
|
1067
|
+
if isinstance(sensor, list):
|
|
1068
|
+
cmd += "&".join(
|
|
1069
|
+
[
|
|
1070
|
+
"sensor={}{}".format(ix - 1, val)
|
|
1071
|
+
for ix, val in zip(index, sensor)
|
|
1072
|
+
]
|
|
1073
|
+
)
|
|
1074
|
+
else:
|
|
1075
|
+
cmd += "&".join(["sensor={}{}".format(ix - 1, sensor) for ix in index])
|
|
1076
|
+
if isinstance(calibration, list):
|
|
1077
|
+
cmd += "&".join(
|
|
1078
|
+
[
|
|
1079
|
+
"calibration={}{}".format(ix - 1, val)
|
|
1080
|
+
for ix, val in zip(index, calibration)
|
|
1081
|
+
]
|
|
1082
|
+
)
|
|
1083
|
+
elif calibration is not None:
|
|
1084
|
+
cmd += "&".join(
|
|
1085
|
+
["calibration={}{}".format(ix - 1, calibration) for ix in index]
|
|
1086
|
+
)
|
|
1087
|
+
if isinstance(multiplier, list):
|
|
1088
|
+
cmd += "&".join(
|
|
1089
|
+
[
|
|
1090
|
+
"multiplier={}{}".format(ix - 1, int(val * 100))
|
|
1091
|
+
for ix, val in zip(index, multiplier)
|
|
1092
|
+
]
|
|
1093
|
+
)
|
|
1094
|
+
elif multiplier is not None:
|
|
1095
|
+
cmd += "&".join(
|
|
1096
|
+
[
|
|
1097
|
+
"multiplier={}{}".format(ix - 1, int(multiplier * 100))
|
|
1098
|
+
for ix in index
|
|
1099
|
+
]
|
|
1100
|
+
)
|
|
1101
|
+
else:
|
|
1102
|
+
cmd += "sensor={}{}".format(index - 1, sensor)
|
|
1103
|
+
if calibration is not None:
|
|
1104
|
+
cmd += "&calibration={}{}".format(index - 1, calibration)
|
|
1105
|
+
if multiplier is not None:
|
|
1106
|
+
cmd += "&multiplier={}{}".format(index - 1, int(multiplier * 100))
|
|
1107
|
+
return self.get(cmd)
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
@dataclass
|
|
1111
|
+
class LK_HW_35(LK_HW_30):
|
|
1112
|
+
"""Methods for working with LK3.5."""
|
|
1113
|
+
|
|
1114
|
+
info: ClassVar[Union[DeviceInfo, None]] = DeviceInfo(
|
|
1115
|
+
"LK HW 3.5", # Covers HW 3.5, 3.6, 3.7, 3.8
|
|
1116
|
+
FAMILY_LK,
|
|
1117
|
+
"lc35",
|
|
1118
|
+
"https://tinycontrol.pl/en/lan-controller-35/firmware/#firmware",
|
|
1119
|
+
{"number_of_outputs": 6},
|
|
1120
|
+
)
|
|
1121
|
+
|
|
1122
|
+
@classmethod
|
|
1123
|
+
def check_version(
|
|
1124
|
+
cls, hardware_version: Union[str, None], software_version: str
|
|
1125
|
+
) -> bool:
|
|
1126
|
+
return (
|
|
1127
|
+
"3.5" <= hardware_version < "4.0"
|
|
1128
|
+
and not software_version.endswith("ps")
|
|
1129
|
+
and not software_version.endswith("dcdc")
|
|
1130
|
+
)
|
|
1131
|
+
|
|
1132
|
+
def _set_var(
|
|
1133
|
+
self, index: Union[int, List[int]], value: Union[int, List[int]]
|
|
1134
|
+
) -> str:
|
|
1135
|
+
"""Prepare command for setting VAR/EVENT variables.
|
|
1136
|
+
|
|
1137
|
+
Arguments:
|
|
1138
|
+
index: 1-8 (single or list)
|
|
1139
|
+
value: 0-1 (single or list)
|
|
1140
|
+
"""
|
|
1141
|
+
cmd = "/outs.cgi?"
|
|
1142
|
+
if isinstance(index, list):
|
|
1143
|
+
if isinstance(value, list):
|
|
1144
|
+
cmd += "&".join(
|
|
1145
|
+
["vout{}={}".format(ix - 1, val) for ix, val in zip(index, value)]
|
|
1146
|
+
)
|
|
1147
|
+
else:
|
|
1148
|
+
cmd += "&".join(["vout{}={}".format(ix - 1, value) for ix in index])
|
|
1149
|
+
else:
|
|
1150
|
+
cmd += "vout{}={}".format(index - 1, value)
|
|
1151
|
+
return cmd
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
@dataclass
|
|
1155
|
+
class LK_HW_35_PS(LK_HW_35):
|
|
1156
|
+
"""Methods for working with IP Power Socket v2 (LK3.5)."""
|
|
1157
|
+
|
|
1158
|
+
info: ClassVar[Union[DeviceInfo, None]] = DeviceInfo(
|
|
1159
|
+
"IP Power Socket v2 (LK3.5)", # 5G10A/6G10A
|
|
1160
|
+
FAMILY_PS,
|
|
1161
|
+
)
|
|
1162
|
+
|
|
1163
|
+
@classmethod
|
|
1164
|
+
def check_version(
|
|
1165
|
+
cls, hardware_version: Union[str, None], software_version: str
|
|
1166
|
+
) -> bool:
|
|
1167
|
+
return "3.5" <= hardware_version < "4.0" and software_version.endswith("ps")
|
|
1168
|
+
|
|
1169
|
+
|
|
1170
|
+
@dataclass
|
|
1171
|
+
class LK_HW_35_DCDC(LK_HW_35):
|
|
1172
|
+
"""Methods for working with LK3.5."""
|
|
1173
|
+
|
|
1174
|
+
info: ClassVar[Union[DeviceInfo, None]] = DeviceInfo(
|
|
1175
|
+
"Converter DC/DC",
|
|
1176
|
+
FAMILY_DCDC,
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
@classmethod
|
|
1180
|
+
def check_version(
|
|
1181
|
+
cls, hardware_version: Union[str, None], software_version: str
|
|
1182
|
+
) -> bool:
|
|
1183
|
+
return "3.5" <= hardware_version < "4.0" and software_version.endswith("dcdc")
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
@dataclass
|
|
1187
|
+
class LK_HW_40(DeviceModel):
|
|
1188
|
+
"""Device model for LK4.
|
|
1189
|
+
|
|
1190
|
+
For information about possible requests see:
|
|
1191
|
+
https://docs.tinycontrol.pl/en/lk4/api/commands/
|
|
1192
|
+
"""
|
|
1193
|
+
|
|
1194
|
+
info: ClassVar[Union[DeviceInfo, None]] = DeviceInfo(
|
|
1195
|
+
"LK HW 4.0",
|
|
1196
|
+
FAMILY_LK,
|
|
1197
|
+
"lc40",
|
|
1198
|
+
"https://tinycontrol.pl/en/lk4/downloads/#firmware",
|
|
1199
|
+
{"number_of_outputs": 6},
|
|
1200
|
+
)
|
|
1201
|
+
mapping: ClassVar[Dict[str, Dict]] = {
|
|
1202
|
+
"netMac": {"name": "mac", "format": str},
|
|
1203
|
+
"softwareVersion": {"name": "software_version", "format": str},
|
|
1204
|
+
"hardwareVersion": {"name": "hardware_version", "format": str},
|
|
1205
|
+
}
|
|
1206
|
+
parsers: ClassVar[List[str]] = ["_parse_outs"]
|
|
1207
|
+
|
|
1208
|
+
@classmethod
|
|
1209
|
+
def check_version(
|
|
1210
|
+
cls, hardware_version: Union[str, None], software_version: str
|
|
1211
|
+
) -> bool:
|
|
1212
|
+
return hardware_version == "4.0" and not software_version.endswith("mini")
|
|
1213
|
+
|
|
1214
|
+
def _parse_outs(self, data: Dict[str, Any], path: str) -> None:
|
|
1215
|
+
"""Parse outputs OUT including negation."""
|
|
1216
|
+
if "outNegation" in data:
|
|
1217
|
+
self._context["out_negation"] = data.get("outNegation")
|
|
1218
|
+
if "out1" in data:
|
|
1219
|
+
out_negation = self._context.get("out_negation")
|
|
1220
|
+
for name in name_list("out", self.info.extras["number_of_outputs"]):
|
|
1221
|
+
data[name] = (
|
|
1222
|
+
int_inverted(data[name]) if out_negation else int(data[name])
|
|
1223
|
+
)
|
|
1224
|
+
|
|
1225
|
+
def _set_out(
|
|
1226
|
+
self, index: Union[int, List[int]], value: Union[int, List[int], None]
|
|
1227
|
+
) -> str:
|
|
1228
|
+
"""Prepare command for setting outputs OUT.
|
|
1229
|
+
|
|
1230
|
+
Arguments:
|
|
1231
|
+
index: 1-n (n - self.info.extras['number_of_outputs']; single or list)
|
|
1232
|
+
value: 0-1 (single or list)
|
|
1233
|
+
NOTE: When out is negated it will negate passed value,
|
|
1234
|
+
so value=1 will actually set 0 and value=0 set 1.
|
|
1235
|
+
"""
|
|
1236
|
+
cmd = "/api/v1/save/?"
|
|
1237
|
+
if value is not None and self._context.get("out_negation"):
|
|
1238
|
+
if isinstance(value, list):
|
|
1239
|
+
value = [int_inverted(val) for val in value]
|
|
1240
|
+
else:
|
|
1241
|
+
value = int_inverted(value)
|
|
1242
|
+
if isinstance(index, list):
|
|
1243
|
+
if value is None:
|
|
1244
|
+
cmd += "&".join(["out=out{}".format(ix) for ix in index])
|
|
1245
|
+
elif isinstance(value, list):
|
|
1246
|
+
cmd += "&".join(
|
|
1247
|
+
["out{}={}".format(ix, val) for ix, val in zip(index, value)]
|
|
1248
|
+
)
|
|
1249
|
+
else:
|
|
1250
|
+
cmd += "&".join(["out{}={}".format(ix, value) for ix in index])
|
|
1251
|
+
else:
|
|
1252
|
+
if value is None:
|
|
1253
|
+
cmd += "out=out{}".format(index)
|
|
1254
|
+
else:
|
|
1255
|
+
cmd += "out{}={}".format(index, value)
|
|
1256
|
+
return cmd
|
|
1257
|
+
|
|
1258
|
+
def _set_pwm(
|
|
1259
|
+
self, index: Union[int, List[int]], value: Union[int, List[int], None]
|
|
1260
|
+
) -> str:
|
|
1261
|
+
"""Prepare command for setting PWM.
|
|
1262
|
+
|
|
1263
|
+
Arguments:
|
|
1264
|
+
index: 1-3 (single or list)
|
|
1265
|
+
value: 0-1 (single or list)
|
|
1266
|
+
"""
|
|
1267
|
+
cmd = "/api/v1/save/?"
|
|
1268
|
+
if isinstance(index, list):
|
|
1269
|
+
if value is None:
|
|
1270
|
+
cmd += "&".join(["pwm=pwm{}".format(ix) for ix in index])
|
|
1271
|
+
elif isinstance(value, list):
|
|
1272
|
+
cmd += "&".join(
|
|
1273
|
+
["pwm{}={}".format(ix, val) for ix, val in zip(index, value)]
|
|
1274
|
+
)
|
|
1275
|
+
else:
|
|
1276
|
+
cmd += "&".join(["pwm{}={}".format(ix, value) for ix in index])
|
|
1277
|
+
else:
|
|
1278
|
+
if value is None:
|
|
1279
|
+
cmd += "pwm=pwm{}".format(index)
|
|
1280
|
+
else:
|
|
1281
|
+
cmd += "pwm{}={}".format(index, value)
|
|
1282
|
+
return cmd
|
|
1283
|
+
|
|
1284
|
+
def _set_pwm_duty(
|
|
1285
|
+
self, index: Union[int, List[int]], value: Union[int, List[int]]
|
|
1286
|
+
) -> str:
|
|
1287
|
+
"""Prepare command for setting PWM duty.
|
|
1288
|
+
|
|
1289
|
+
Arguments:
|
|
1290
|
+
index: 1-3 (single or list)
|
|
1291
|
+
value: 0-100 (single or list)
|
|
1292
|
+
"""
|
|
1293
|
+
cmd = "/api/v1/save/?"
|
|
1294
|
+
if isinstance(index, list):
|
|
1295
|
+
if isinstance(value, list):
|
|
1296
|
+
cmd += "&".join(
|
|
1297
|
+
[
|
|
1298
|
+
"pwmDuty{}={}".format(ix, int(val))
|
|
1299
|
+
for ix, val in zip(index, value)
|
|
1300
|
+
]
|
|
1301
|
+
)
|
|
1302
|
+
else:
|
|
1303
|
+
cmd += "&".join(["pwmDuty{}={}".format(ix, int(value)) for ix in index])
|
|
1304
|
+
else:
|
|
1305
|
+
cmd += "pwmDuty{}={}".format(index, int(value))
|
|
1306
|
+
return cmd
|
|
1307
|
+
|
|
1308
|
+
def _set_pwm_freq(self, index: Any, value: int) -> str:
|
|
1309
|
+
"""Prepare command for setting PWM freq.
|
|
1310
|
+
|
|
1311
|
+
Arguments:
|
|
1312
|
+
index: not used (only one frequency)
|
|
1313
|
+
value: 1-1_000_000
|
|
1314
|
+
"""
|
|
1315
|
+
cmd = "/api/v1/save/?pwmFrequency={}".format(value)
|
|
1316
|
+
return cmd
|
|
1317
|
+
|
|
1318
|
+
def _set_var(
|
|
1319
|
+
self, index: Union[int, List[int]], value: Union[int, List[int]]
|
|
1320
|
+
) -> str:
|
|
1321
|
+
"""Prepare command for setting VAR/EVENT variables.
|
|
1322
|
+
|
|
1323
|
+
Arguments:
|
|
1324
|
+
index: 1-8 (single or list)
|
|
1325
|
+
value: 0-1 (single or list)
|
|
1326
|
+
"""
|
|
1327
|
+
cmd = "/api/v1/save/?"
|
|
1328
|
+
if isinstance(index, list):
|
|
1329
|
+
if isinstance(value, list):
|
|
1330
|
+
cmd += "&".join(
|
|
1331
|
+
["var{}={}".format(ix, val) for ix, val in zip(index, value)]
|
|
1332
|
+
)
|
|
1333
|
+
else:
|
|
1334
|
+
cmd += "&".join(["var{}={}".format(ix, value) for ix in index])
|
|
1335
|
+
else:
|
|
1336
|
+
cmd += "var{}={}".format(index, value)
|
|
1337
|
+
return cmd
|
|
1338
|
+
|
|
1339
|
+
def _set_ds(
|
|
1340
|
+
self, index: Union[int, List[int]], value: Union[str, List[str]]
|
|
1341
|
+
) -> str:
|
|
1342
|
+
"""Set ID of DS on position to value.
|
|
1343
|
+
|
|
1344
|
+
Arguments:
|
|
1345
|
+
index: 1-8
|
|
1346
|
+
value: DS ID
|
|
1347
|
+
"""
|
|
1348
|
+
cmd = "/api/v1/save/?"
|
|
1349
|
+
if isinstance(index, list):
|
|
1350
|
+
if isinstance(value, list):
|
|
1351
|
+
cmd += "&".join(
|
|
1352
|
+
["dsID{}={}".format(ix, val) for ix, val in zip(index, value)]
|
|
1353
|
+
)
|
|
1354
|
+
else:
|
|
1355
|
+
cmd += "&".join(["dsID{}={}".format(ix, value) for ix in index])
|
|
1356
|
+
else:
|
|
1357
|
+
cmd += "dsID{}={}".format(index, value)
|
|
1358
|
+
return cmd
|
|
1359
|
+
|
|
1360
|
+
def get_ds_id(self) -> str:
|
|
1361
|
+
"""Get ID of detected DS."""
|
|
1362
|
+
self.get("/api/v1/save/?dsReadID=0")
|
|
1363
|
+
return self.get("/api/v1/read/status/?dsValues").get("dsReadID")
|
|
1364
|
+
|
|
1365
|
+
def _get_all(self) -> List[str]:
|
|
1366
|
+
"""Prepare list of URLs to fetch data from."""
|
|
1367
|
+
return [
|
|
1368
|
+
"/api/v1/read/set/?generalConfig&outConfig&powerConfig&networkConfig",
|
|
1369
|
+
"/api/v1/read/status/?boardValues&statusValues&timeValues&outValues&pwmValues&iAValues&dsValues&i2cValues&otherSensorsValues&diffValues&iDValues&powerValues&mrValues&varValues",
|
|
1370
|
+
]
|
|
1371
|
+
|
|
1372
|
+
def _reset_to_defaults(self):
|
|
1373
|
+
"""Reset settings to defaults."""
|
|
1374
|
+
return "/api/v1/save/?eeprom_reset=1"
|
|
1375
|
+
|
|
1376
|
+
def _restart(self):
|
|
1377
|
+
"""Restart device."""
|
|
1378
|
+
return "/api/v1/save/?restart=1"
|
|
1379
|
+
|
|
1380
|
+
|
|
1381
|
+
@dataclass
|
|
1382
|
+
class TCPDU(LK_HW_40):
|
|
1383
|
+
"""Device model for tcPDU.
|
|
1384
|
+
|
|
1385
|
+
For information about possible requests see:
|
|
1386
|
+
https://docs.tinycontrol.pl/en/tcpdu/api/commands/
|
|
1387
|
+
"""
|
|
1388
|
+
|
|
1389
|
+
info: ClassVar[Union[DeviceInfo, None]] = DeviceInfo(
|
|
1390
|
+
"tcPDU",
|
|
1391
|
+
FAMILY_TCPDU,
|
|
1392
|
+
"tcpdu",
|
|
1393
|
+
"https://tinycontrol.pl/en/tcpdu/downloads/#firmware",
|
|
1394
|
+
{"number_of_outputs": 7},
|
|
1395
|
+
)
|
|
1396
|
+
|
|
1397
|
+
@classmethod
|
|
1398
|
+
def check_version(
|
|
1399
|
+
cls, hardware_version: Union[str, None], software_version: str
|
|
1400
|
+
) -> bool:
|
|
1401
|
+
return hardware_version in ["1.0", "1.1"] and software_version.endswith("tcPDU")
|
|
1402
|
+
|
|
1403
|
+
# Unset few commands - there are no PWM in tcPDU
|
|
1404
|
+
_set_pwm = None
|
|
1405
|
+
_set_pwm_duty = None
|
|
1406
|
+
_set_pwm_freq = None
|
|
1407
|
+
|
|
1408
|
+
def _get_all(self) -> List[str]:
|
|
1409
|
+
"""Prepare list of URLs to fetch data from.
|
|
1410
|
+
|
|
1411
|
+
Compared to LK4 does not include pwm, analog, pm, co2, mapped (modbus readings).
|
|
1412
|
+
"""
|
|
1413
|
+
return [
|
|
1414
|
+
"/api/v1/read/set/?generalConfig&outConfig&powerConfig&networkConfig",
|
|
1415
|
+
"/api/v1/read/status/?boardValues&statusValues&timeValues&outValues&dsValues&i2cValues&diffValues&iDValues&powerValues&varValues",
|
|
1416
|
+
]
|
|
1417
|
+
|
|
1418
|
+
|
|
1419
|
+
@dataclass
|
|
1420
|
+
class LK_HW_40_mini(LK_HW_40):
|
|
1421
|
+
"""Device model for LK4mini.
|
|
1422
|
+
|
|
1423
|
+
It's basically the same as LK4 minus outputs, PWM outputs, analog
|
|
1424
|
+
inputs, digital inputs, power and energy, serial port.
|
|
1425
|
+
"""
|
|
1426
|
+
|
|
1427
|
+
info: ClassVar[Union[DeviceInfo, None]] = DeviceInfo(
|
|
1428
|
+
"LK HW 4.0 mini",
|
|
1429
|
+
FAMILY_LK,
|
|
1430
|
+
"lc40mini",
|
|
1431
|
+
"https://tinycontrol.pl/en/lk4mini/downloads/#firmware",
|
|
1432
|
+
{"number_of_outputs": 0},
|
|
1433
|
+
)
|
|
1434
|
+
parsers: ClassVar[List[str]] = []
|
|
1435
|
+
|
|
1436
|
+
@classmethod
|
|
1437
|
+
def check_version(
|
|
1438
|
+
cls, hardware_version: Union[str, None], software_version: str
|
|
1439
|
+
) -> bool:
|
|
1440
|
+
return hardware_version == "4.0" and software_version.endswith("mini")
|
|
1441
|
+
|
|
1442
|
+
# Unset few commands - there are no OUT, PWM in LK4 mini
|
|
1443
|
+
_set_out = None
|
|
1444
|
+
_set_pwm = None
|
|
1445
|
+
_set_pwm_duty = None
|
|
1446
|
+
_set_pwm_freq = None
|
|
1447
|
+
|
|
1448
|
+
def _get_all(self) -> List[str]:
|
|
1449
|
+
return [
|
|
1450
|
+
"/api/v1/read/set/?generalConfig&networkConfig",
|
|
1451
|
+
"/api/v1/read/status/?boardValues&statusValues&timeValues&dsValues&i2cValues&otherSensorsValues&diffValues&mrValues&varValues"
|
|
1452
|
+
]
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
DEVICE_MODELS = [
|
|
1456
|
+
LK_HW_40_mini,
|
|
1457
|
+
TCPDU,
|
|
1458
|
+
LK_HW_40,
|
|
1459
|
+
LK_HW_35,
|
|
1460
|
+
LK_HW_35_PS,
|
|
1461
|
+
LK_HW_35_DCDC,
|
|
1462
|
+
LK_HW_30,
|
|
1463
|
+
LK_HW_25,
|
|
1464
|
+
LK_HW_25_PS,
|
|
1465
|
+
LK_HW_20,
|
|
1466
|
+
LK_HW_20_PS,
|
|
1467
|
+
]
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
def detect_version(version_text: str) -> Union[str, None]:
|
|
1471
|
+
"""Detect hw version based on sw version (from discovery).
|
|
1472
|
+
|
|
1473
|
+
Currently it is only useful for LK20/25.
|
|
1474
|
+
"""
|
|
1475
|
+
pattern = re.compile(r"^(\d\.\d+)(.*)$")
|
|
1476
|
+
match = pattern.search(version_text)
|
|
1477
|
+
hardware_version = ""
|
|
1478
|
+
if match is not None:
|
|
1479
|
+
parts = match.groups()
|
|
1480
|
+
software_version = float(parts[0])
|
|
1481
|
+
# First check 'frozen' versions like 2.0 or 3.0
|
|
1482
|
+
if software_version in [
|
|
1483
|
+
2.0,
|
|
1484
|
+
2.03,
|
|
1485
|
+
2.07,
|
|
1486
|
+
2.09,
|
|
1487
|
+
3.03,
|
|
1488
|
+
3.06,
|
|
1489
|
+
3.10,
|
|
1490
|
+
3.13,
|
|
1491
|
+
3.15,
|
|
1492
|
+
3.18,
|
|
1493
|
+
]:
|
|
1494
|
+
hardware_version = "2.0"
|
|
1495
|
+
elif software_version in [6.0, 6.09, 6.10, 6.12]: # power socket 2.0
|
|
1496
|
+
hardware_version = "2.0"
|
|
1497
|
+
elif software_version == 6.15: # power socket 2.5
|
|
1498
|
+
hardware_version = "2.5"
|
|
1499
|
+
elif software_version in [2.01, 3.01, 3.02]:
|
|
1500
|
+
hardware_version = "2.5"
|
|
1501
|
+
return hardware_version
|
|
1502
|
+
|
|
1503
|
+
|
|
1504
|
+
def get_device_info(
|
|
1505
|
+
hardware_version: str, software_version: str, asdict_: bool = False
|
|
1506
|
+
) -> Union[DeviceInfo, Dict[str, Any]]:
|
|
1507
|
+
"""Return device info based on given HW and SW."""
|
|
1508
|
+
device_info = None
|
|
1509
|
+
for device_model in DEVICE_MODELS:
|
|
1510
|
+
if device_model.check_version(hardware_version, software_version):
|
|
1511
|
+
device_info = device_model.info
|
|
1512
|
+
break
|
|
1513
|
+
if not device_info:
|
|
1514
|
+
device_info = DeviceInfo("?", "?")
|
|
1515
|
+
if asdict_:
|
|
1516
|
+
device_info = asdict(device_info)
|
|
1517
|
+
return device_info
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
def get_device(
|
|
1521
|
+
hardware_version: str,
|
|
1522
|
+
software_version: str,
|
|
1523
|
+
host: str,
|
|
1524
|
+
schema: str = "http",
|
|
1525
|
+
port: int = 80,
|
|
1526
|
+
username: str = "",
|
|
1527
|
+
password: str = "",
|
|
1528
|
+
session: Union[ClientSession, None] = None,
|
|
1529
|
+
) -> Union[DeviceModel, None]:
|
|
1530
|
+
"""Get Device instance."""
|
|
1531
|
+
for device_model in DEVICE_MODELS:
|
|
1532
|
+
if device_model.check_version(hardware_version, software_version):
|
|
1533
|
+
return device_model(
|
|
1534
|
+
host,
|
|
1535
|
+
schema,
|
|
1536
|
+
port,
|
|
1537
|
+
username,
|
|
1538
|
+
password,
|
|
1539
|
+
hardware_version,
|
|
1540
|
+
software_version,
|
|
1541
|
+
session=session,
|
|
1542
|
+
)
|
|
1543
|
+
return None
|
|
1544
|
+
|
|
1545
|
+
|
|
1546
|
+
def _get_version(
|
|
1547
|
+
response: Union[Any, None],
|
|
1548
|
+
host: str,
|
|
1549
|
+
port: str,
|
|
1550
|
+
username: str,
|
|
1551
|
+
password: str,
|
|
1552
|
+
with_info: bool,
|
|
1553
|
+
with_device: bool,
|
|
1554
|
+
session: Union[ClientSession, None],
|
|
1555
|
+
) -> Union[Dict[str, Any], None]:
|
|
1556
|
+
"""Analyze responses to get version info."""
|
|
1557
|
+
if response is None:
|
|
1558
|
+
return response
|
|
1559
|
+
version_info = parse_version(response["parsed"])
|
|
1560
|
+
version_info["network_info"] = {
|
|
1561
|
+
"host": host,
|
|
1562
|
+
"schema": str(response["_response"].url).split(":", 1)[0],
|
|
1563
|
+
"port": port,
|
|
1564
|
+
"username": username,
|
|
1565
|
+
"password": password,
|
|
1566
|
+
}
|
|
1567
|
+
if with_info:
|
|
1568
|
+
version_info.update(
|
|
1569
|
+
get_device_info(
|
|
1570
|
+
version_info["hardware_version"],
|
|
1571
|
+
version_info["software_version"],
|
|
1572
|
+
asdict_=True,
|
|
1573
|
+
)
|
|
1574
|
+
)
|
|
1575
|
+
if with_device:
|
|
1576
|
+
version_info["device_model"] = get_device(
|
|
1577
|
+
version_info["hardware_version"],
|
|
1578
|
+
version_info["software_version"],
|
|
1579
|
+
**version_info["network_info"],
|
|
1580
|
+
session=session,
|
|
1581
|
+
)
|
|
1582
|
+
return version_info
|
|
1583
|
+
|
|
1584
|
+
|
|
1585
|
+
def _get_version_initial(
|
|
1586
|
+
exc: Exception, schema: str, port: int, silent: bool
|
|
1587
|
+
) -> Tuple[str, str, int]:
|
|
1588
|
+
"""Handle first response in get_version - prepare data for second request."""
|
|
1589
|
+
schema, path = "http", ""
|
|
1590
|
+
try:
|
|
1591
|
+
raise exc
|
|
1592
|
+
except TinyToolsRequestNotFound:
|
|
1593
|
+
# Likely LK3.X
|
|
1594
|
+
path = "/xml/stat.xml"
|
|
1595
|
+
except TinyToolsRequestInternalServerError:
|
|
1596
|
+
# Likely LK4/LK4mini/tcPDU
|
|
1597
|
+
path = "/api/v1/read/set/?generalConfig"
|
|
1598
|
+
except TinyToolsRequestSSLError:
|
|
1599
|
+
# When LK3 has https enabled it will redirect http and with ssl verification we land here.
|
|
1600
|
+
port = 443
|
|
1601
|
+
path = "/xml/stat.xml"
|
|
1602
|
+
schema = "https"
|
|
1603
|
+
except (TinyToolsRequestConnectionError, TinyToolsRequestTimeout):
|
|
1604
|
+
# Seems that newer SW LK4/tcPDU with https get here, but still include timeout
|
|
1605
|
+
port = 443
|
|
1606
|
+
path = "/api/v1/read/set/?generalConfig"
|
|
1607
|
+
schema = "https"
|
|
1608
|
+
except TinyToolsRequestError:
|
|
1609
|
+
if not silent:
|
|
1610
|
+
raise
|
|
1611
|
+
return path, schema, port
|
|
1612
|
+
|
|
1613
|
+
|
|
1614
|
+
def get_version(
|
|
1615
|
+
host: str,
|
|
1616
|
+
port: int = 80,
|
|
1617
|
+
username: str = "",
|
|
1618
|
+
password: str = "",
|
|
1619
|
+
with_info: bool = True,
|
|
1620
|
+
with_device: bool = False,
|
|
1621
|
+
silent: bool = True,
|
|
1622
|
+
) -> Union[Dict[str, Any], None]:
|
|
1623
|
+
"""Query device for version and optionally device model.
|
|
1624
|
+
|
|
1625
|
+
NOTE: Might not work properly with https and port different than 443,
|
|
1626
|
+
eg. proxy, as LK always uses 443 for https.
|
|
1627
|
+
TODO: Might add schema parameter so it would work to handle proxy.
|
|
1628
|
+
"""
|
|
1629
|
+
response = None
|
|
1630
|
+
try:
|
|
1631
|
+
response = get(host, "/st2.xml", "http", port, username, password)
|
|
1632
|
+
except TinyToolsRequestError as exc:
|
|
1633
|
+
path, schema, port = _get_version_initial(exc, "http", port, silent)
|
|
1634
|
+
if path:
|
|
1635
|
+
response = get(host, path, schema, port, username, password, silent=silent)
|
|
1636
|
+
return _get_version(
|
|
1637
|
+
response, host, port, username, password, with_info, with_device, None
|
|
1638
|
+
)
|
|
1639
|
+
|
|
1640
|
+
|
|
1641
|
+
async def async_get_version(
|
|
1642
|
+
host: str,
|
|
1643
|
+
port: int = 80,
|
|
1644
|
+
username: str = "",
|
|
1645
|
+
password: str = "",
|
|
1646
|
+
with_info: bool = True,
|
|
1647
|
+
with_device: bool = False,
|
|
1648
|
+
silent: bool = True,
|
|
1649
|
+
session: Union[ClientSession, None] = None,
|
|
1650
|
+
) -> Union[Dict[str, Any], None]:
|
|
1651
|
+
response = None
|
|
1652
|
+
try:
|
|
1653
|
+
response = await async_get(
|
|
1654
|
+
host, "/st2.xml", "http", port, username, password, session=session
|
|
1655
|
+
)
|
|
1656
|
+
except TinyToolsRequestError as exc:
|
|
1657
|
+
path, schema, port = _get_version_initial(exc, "http", port, silent)
|
|
1658
|
+
if path:
|
|
1659
|
+
response = await async_get(
|
|
1660
|
+
host,
|
|
1661
|
+
path,
|
|
1662
|
+
schema,
|
|
1663
|
+
port,
|
|
1664
|
+
username,
|
|
1665
|
+
password,
|
|
1666
|
+
silent=silent,
|
|
1667
|
+
session=session,
|
|
1668
|
+
)
|
|
1669
|
+
return _get_version(
|
|
1670
|
+
response, host, port, username, password, with_info, with_device, session
|
|
1671
|
+
)
|
|
1672
|
+
|
|
1673
|
+
|
|
1674
|
+
async_get_version.__doc__ = get_version.__doc__
|