tinytoolslib 0.4.0__tar.gz → 0.5.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinytoolslib
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Set of tools for use with Tinycontrol devices like LK2.X, LK3.X, LK4.X or tcPDU.
5
5
  Author-email: Bartek Barszczewski <tinycontrol.software@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tinytoolslib"
7
- version = "0.4.0"
7
+ version = "0.5.0"
8
8
  authors = [
9
9
  { name="Bartek Barszczewski", email="tinycontrol.software@gmail.com" },
10
10
  ]
@@ -0,0 +1 @@
1
+ __version__ = '0.5.0'
@@ -1,5 +1,6 @@
1
1
  """Device definitions along with methods for communicating with them."""
2
2
 
3
+ import operator
3
4
  import re
4
5
  import warnings
5
6
  from abc import ABC, abstractmethod
@@ -8,27 +9,18 @@ from typing import Any, ClassVar, Dict, List, Tuple, Union
8
9
 
9
10
  from aiohttp import ClientSession
10
11
 
11
- from tinytoolslib.constants import FW_URL_TEMPLATE, DeviceFamily, FWUpdateMethod
12
- from tinytoolslib.exceptions import (
13
- TinyToolsRequestConnectionError,
14
- TinyToolsRequestError,
15
- TinyToolsRequestInternalServerError,
16
- TinyToolsRequestNotFound,
17
- TinyToolsRequestSSLError,
18
- TinyToolsRequestTimeout,
19
- TinyToolsUnsupported,
20
- )
21
- from tinytoolslib.parsers import (
22
- float_div10,
23
- float_div100,
24
- float_div1000,
25
- int_inverted,
26
- list_map,
27
- name_list,
28
- parse_version,
29
- strint_to_int_list,
30
- up_to_int,
31
- )
12
+ from tinytoolslib.constants import (FW_URL_TEMPLATE, DeviceFamily,
13
+ FWUpdateMethod)
14
+ from tinytoolslib.exceptions import (TinyToolsRequestConnectionError,
15
+ TinyToolsRequestError,
16
+ TinyToolsRequestInternalServerError,
17
+ TinyToolsRequestNotFound,
18
+ TinyToolsRequestSSLError,
19
+ TinyToolsRequestTimeout,
20
+ TinyToolsUnsupported)
21
+ from tinytoolslib.parsers import (float_div10, float_div100, float_div1000,
22
+ int_inverted, list_map, name_list,
23
+ parse_version, strint_to_int_list, up_to_int)
32
24
  from tinytoolslib.requests import async_get, get
33
25
 
34
26
 
@@ -108,7 +100,11 @@ class DeviceModel(ABC):
108
100
  if remove_mapped_keys:
109
101
  # Remove keys that were parsed/mapped
110
102
  for key, val in self.mapping.items():
111
- if isinstance(val["name"], str) and key != val["name"]:
103
+ if (
104
+ isinstance(val["name"], str)
105
+ and key != val["name"]
106
+ and (skip_keys is None or key not in skip_keys)
107
+ ):
112
108
  data.pop(key, None)
113
109
  # Run extra parsers, that need to work on whole response.
114
110
  for parser in self.parsers:
@@ -183,12 +179,12 @@ class DeviceModel(ABC):
183
179
  f"{self.__class__.__name__} does not support setting DSs"
184
180
  )
185
181
 
186
- def get_all(self) -> Dict[str, Any]:
182
+ def get_all(self, remove_mapped_keys: bool = False) -> Dict[str, Any]:
187
183
  """Get set of all sensor/readings."""
188
184
  if callable(getattr(self, "_get_all", None)):
189
185
  data = {}
190
186
  for url in self._get_all():
191
- data.update(self.get(url))
187
+ data.update(self.get(url, remove_mapped_keys=remove_mapped_keys))
192
188
  return data
193
189
  raise TinyToolsUnsupported(
194
190
  f"{self.__class__.__name__} does not support get_all command"
@@ -258,12 +254,12 @@ class DeviceModel(ABC):
258
254
  f"{self.__class__.__name__} does not support controlling VARs"
259
255
  )
260
256
 
261
- async def async_get_all(self) -> Dict[str, Any]:
257
+ async def async_get_all(self, remove_mapped_keys: bool = False) -> Dict[str, Any]:
262
258
  """Async get_all."""
263
259
  if callable(getattr(self, "_get_all", None)):
264
260
  data = {}
265
261
  for url in self._get_all():
266
- data.update(await self.async_get(url))
262
+ data.update(await self.async_get(url, remove_mapped_keys=remove_mapped_keys))
267
263
  return data
268
264
  raise TinyToolsUnsupported(
269
265
  f"{self.__class__.__name__} does not support get_all command"
@@ -296,6 +292,82 @@ class DeviceModel(ABC):
296
292
  # endregion
297
293
 
298
294
 
295
+ def set_cmd_helper(
296
+ caller: DeviceModel,
297
+ cmd_prefix: str,
298
+ cmd_param: str,
299
+ index: Union[int, List[int]],
300
+ value: Union[int, List[int], None],
301
+ negation=False,
302
+ toggle_available=True,
303
+ cmd_set_format="{cmd_param}{index}={value}",
304
+ cmd_toggle_format="{cmd_param}={cmd_param}{index}",
305
+ ):
306
+ """Helper for building GET query for setting, eg OUTs, PWMs, VARs.
307
+
308
+ It handles negation (or forced inversion of state, eg. when device
309
+ uses 0 for on and 1 for off), toggle commands (only chainable mode,
310
+ eg. out=out1&out=out2), and also mixed combination of index+value.
311
+
312
+ Example output (for single index and single value, set command):
313
+ `{cmd_prefix}{cmd_param}{index}={value}`
314
+ """
315
+ cmd = cmd_prefix
316
+ # Invert states for negation or forced inversion.
317
+ if negation and value is not None:
318
+ if isinstance(value, list):
319
+ value = [int_inverted(val) for val in value]
320
+ else:
321
+ value = int_inverted(value)
322
+ # Handle case when value is missing but toggle is not available.
323
+ if toggle_available is False and value is None:
324
+ raise TinyToolsUnsupported(
325
+ f"{caller.__class__.__name__} does not support toggle for command {cmd_param}"
326
+ )
327
+ # Handle combinations of index + value: int + int/None, [int] + int/[int]/None.
328
+ if isinstance(index, list):
329
+ if toggle_available and value is None:
330
+ cmd += "&".join(
331
+ [
332
+ cmd_toggle_format.format(cmd_param=cmd_param, index=ix)
333
+ for ix in index
334
+ ]
335
+ )
336
+ elif isinstance(value, list):
337
+ cmd += "&".join(
338
+ [
339
+ cmd_set_format.format(cmd_param=cmd_param, index=ix, value=val)
340
+ for ix, val in zip(index, value)
341
+ ]
342
+ )
343
+ else:
344
+ cmd += "&".join(
345
+ [
346
+ cmd_set_format.format(cmd_param=cmd_param, index=ix, value=value)
347
+ for ix in index
348
+ ]
349
+ )
350
+ else:
351
+ if toggle_available and value is None:
352
+ cmd += cmd_toggle_format.format(cmd_param=cmd_param, index=index)
353
+ else:
354
+ cmd += cmd_set_format.format(cmd_param=cmd_param, index=index, value=value)
355
+ return cmd
356
+
357
+
358
+ def apply_index_offset(
359
+ index: Union[int, List[int]], offset: int = 0, func=operator.add
360
+ ) -> Union[int, List[int]]:
361
+ """Apply an integer offset to a single index or list of indices.
362
+
363
+ Use offset=-1 to convert 1-based → 0-based, offset=+1 to convert 0-based → 1-based.
364
+ Might change how it processes by providing func, which will be given 2 params: index and offset.
365
+ """
366
+ if isinstance(index, list):
367
+ return [func(i, offset) for i in index]
368
+ return func(index, offset)
369
+
370
+
299
371
  @dataclass
300
372
  class LK_HW_20_PS(DeviceModel):
301
373
  """Methods for working with Power Socket on LK2.0.
@@ -307,6 +379,7 @@ class LK_HW_20_PS(DeviceModel):
307
379
  "IP Power Socket v1 (LK2.0)", # 5G10A/6G10A
308
380
  DeviceFamily.PS,
309
381
  fw_update_method=FWUpdateMethod.TFTP,
382
+ extras={"number_of_outputs": 6, "outputs_inverted": True}, # or 5 for SW 6.12a
310
383
  )
311
384
  mapping: ClassVar[Dict[str, Dict]] = {
312
385
  # --- st0.xml
@@ -396,6 +469,9 @@ class LK_HW_20_PS(DeviceModel):
396
469
  "b28": {"name": "snmp_trap_active", "format": bool}, # 'true' or ''
397
470
  # r0, r1, r2 - remote control
398
471
  }
472
+ parsers: ClassVar[List[str]] = [
473
+ "_parse_outs",
474
+ ]
399
475
 
400
476
  @classmethod
401
477
  def check_version(
@@ -409,6 +485,27 @@ class LK_HW_20_PS(DeviceModel):
409
485
  "6.12",
410
486
  ]
411
487
 
488
+ def is_5_socket_ps(self):
489
+ """Helper check whether it's version of LK 2.0 PS with 5 sockets."""
490
+ return type(self) == LK_HW_20_PS and self.software_version == "6.12a"
491
+
492
+ # region Parser methods that modifies data in _get()
493
+ def _parse_outs(self, data: Dict[str, Any], path: str) -> None:
494
+ """Parse outputs OUT with including negation."""
495
+ # Parser is called post mapping so original keys might be missing already.
496
+ if "out_negation" in data:
497
+ self._context["out_negation"] = int(data.get("out_negation"))
498
+ if "out0" in data:
499
+ out_negation = self._context.get("out_negation")
500
+ number_of_outputs = self.info.extras["number_of_outputs"]
501
+ if self.is_5_socket_ps():
502
+ number_of_outputs = 5
503
+ for name in name_list("out", number_of_outputs, 0):
504
+ data[name] = (
505
+ int_inverted(data[name]) if out_negation else int(data[name])
506
+ )
507
+ # endregion
508
+
412
509
  def _get(
413
510
  self,
414
511
  data: Dict[str, Any],
@@ -416,13 +513,38 @@ class LK_HW_20_PS(DeviceModel):
416
513
  skip_keys: Union[List[str], None] = None,
417
514
  remove_mapped_keys: bool = False,
418
515
  ) -> Dict[str, Any]:
419
- if path == "/board.xml":
516
+ """Manage properties depending on path.
517
+
518
+ Remove problematic keys, or do extra mapping for some keys.
519
+ """
520
+ if self.is_5_socket_ps() and path == "/st2.xml":
521
+ # For PS HW 2.0 SW 6.12a (5 socket)
420
522
  if skip_keys is None:
421
523
  skip_keys = set()
524
+ # Ignore r5-r11 from normal mapping - process that post _get()
525
+ skip_keys.update(["r5", "r6", "r7", "r8", "r9", "r10", "r11"])
526
+ elif path == "/board.xml":
422
527
  # Ignore few variables for /board.xml as they overlap in /st2.xml and /board.xml,
423
528
  # but with different meaning (out reset time instead of remote control).
529
+ if skip_keys is None:
530
+ skip_keys = set()
424
531
  skip_keys.update({"r0", "r1", "r2", "r3", "r4"})
425
- return super()._get(data, path, skip_keys, remove_mapped_keys)
532
+ get_result = super()._get(data, path, skip_keys, remove_mapped_keys)
533
+ if self.is_5_socket_ps() and path == "/st2.xml":
534
+ # For PS HW 2.0 SW 6.12a (5 socket) apply parsing for r5-r9
535
+ mapping = {
536
+ "r5": {"name": "out0_name", "format": str},
537
+ "r6": {"name": "out1_name", "format": str},
538
+ "r7": {"name": "out2_name", "format": str},
539
+ "r8": {"name": "out3_name", "format": str},
540
+ "r9": {"name": "out4_name", "format": str},
541
+ }
542
+ for key, mapper in mapping.items():
543
+ get_result[mapper["name"]] = mapper["format"](get_result[key])
544
+ if remove_mapped_keys:
545
+ for key in mapping:
546
+ get_result.pop(key)
547
+ return get_result
426
548
 
427
549
  def _set_out(
428
550
  self, index: Union[int, List[int]], value: Union[int, List[int], None]
@@ -434,28 +556,27 @@ class LK_HW_20_PS(DeviceModel):
434
556
  value: 0-1 (single or list)
435
557
  """
436
558
  cmd = "/outs.cgi?"
437
- if isinstance(index, list):
438
- if value is None:
559
+ if value is None:
560
+ # Handle different toggle command
561
+ if isinstance(index, list):
439
562
  cmd += "out=" + "".join(map(str, index))
440
- elif isinstance(value, list):
441
- cmd += "&".join(
442
- [
443
- "out{}={}".format(ix, int_inverted(val))
444
- for ix, val in zip(index, value)
445
- ]
446
- )
447
563
  else:
448
- cmd += "&".join(
449
- ["out{}={}".format(ix, int_inverted(value)) for ix in index]
450
- )
564
+ cmd += f"out={index}"
451
565
  else:
452
- if value is None:
453
- cmd += "out={}".format(index)
454
- else:
455
- cmd += "out{}={}".format(index, int_inverted(value))
456
- # Fix for HW2.0 - HW2.0 uses NON inverted for out5=Y,
457
- # so Y=1 to turn on and Y=0 to turn off.
458
- if self.hardware_version == "2.0":
566
+ cmd = set_cmd_helper(
567
+ self,
568
+ cmd,
569
+ "out",
570
+ index,
571
+ value,
572
+ operator.xor(
573
+ self._context["out_negation"], self.info.extras["outputs_inverted"]
574
+ ),
575
+ False,
576
+ )
577
+ # Fix command for OUT5 (non-inverted) is only for LK HW 2.0 PS 6G (HW=2.0, SW=6.12),
578
+ # but PS 5G simply doesn't have OUT5, so for simplicity checks for LK_HW_20_PS.
579
+ if type(self) == LK_HW_20_PS:
459
580
  if "out5=0" in cmd:
460
581
  cmd = cmd.replace("out5=0", "out5=1")
461
582
  elif "out5=1" in cmd:
@@ -480,6 +601,7 @@ class LK_HW_20(LK_HW_20_PS):
480
601
  "lc20",
481
602
  "https://tinycontrol.pl/en/archives/lan-controller-20/#firmware",
482
603
  fw_update_method=FWUpdateMethod.TFTP,
604
+ extras={"number_of_outputs": 6, "outputs_inverted": True},
483
605
  )
484
606
  mapping: ClassVar[Dict[str, Dict]] = {
485
607
  # Overwrite PS mapping (there will be extra b30, b31)
@@ -532,7 +654,7 @@ class LK_HW_20(LK_HW_20_PS):
532
654
  "dz": {"name": "power2_iD4_divisor", "format": int},
533
655
  "mm": {"name": "power2_iD4_unit", "format": str},
534
656
  "mh": {"name": "power2_iD4_divisor2", "format": int},
535
- # Negation of iD (int with bin 0000)
657
+ # Negation of iD (int with bin 0000) - it does not change visual state of inputs.
536
658
  "db": {"name": "iD_negation", "format": str},
537
659
  # --- board.xml
538
660
  "ds": {"name": "ds_read_id", "format": str},
@@ -542,62 +664,30 @@ class LK_HW_20(LK_HW_20_PS):
542
664
  def check_version(
543
665
  cls, hardware_version: Union[str, None], software_version: str
544
666
  ) -> bool:
545
- return hardware_version == "2.0" and software_version not in [
546
- "6.00",
547
- "6.09",
548
- "6.10",
549
- "6.12a",
550
- "6.12",
551
- ]
667
+ return (
668
+ hardware_version == "2.0"
669
+ and LK_HW_20_PS.check_version(hardware_version, software_version) is False
670
+ )
552
671
 
553
- def _set_pwm(
554
- self, index: Union[int, List[int]], value: Union[int, List[int]]
555
- ) -> str:
556
- """Prepare command for setting PWM.
672
+ def _set_pwm(self, index: int, value: int) -> str:
673
+ """Prepare command for setting PWM(0).
557
674
 
558
675
  Arguments:
559
- index: 0-3 (single or list)
560
- value: 0-1 (single or list)
561
- NOTE: 1-3 can be only set, not read
676
+ index: 0 (single int)
677
+ value: 0-1 (single int)
562
678
  """
563
- cmd = "/ind.cgi?"
564
- if isinstance(index, list):
565
- if isinstance(value, list):
566
- cmd += "&".join(
567
- ["pwm{}={}".format(ix, val) for ix, val in zip(index, value)]
568
- )
569
- else:
570
- cmd += "&".join(["pwm{}={}".format(ix, value) for ix in index])
571
- else:
572
- cmd += "pwm{}={}".format(index, value)
679
+ cmd = set_cmd_helper(self, "/ind.cgi?", "pwm", index, value, False, False)
573
680
  cmd = cmd.replace("pwm0", "pwm")
574
681
  return cmd
575
682
 
576
- def _set_pwm_duty(
577
- self, index: Union[int, List[int]], value: Union[int, List[int]]
578
- ) -> str:
579
- """Prepare command for setting PWM duty.
683
+ def _set_pwm_duty(self, index: int, value: int) -> str:
684
+ """Prepare command for setting PWM(0) duty.
580
685
 
581
686
  Arguments:
582
- index: 0-3 (single or list)
583
- value: 0-100 (single or list)
584
- NOTE: 1-3 can be only set, not read
687
+ index: 0 (single int)
688
+ value: 0-100 (single int)
585
689
  """
586
- cmd = "/ind.cgi?"
587
- if isinstance(index, list):
588
- if isinstance(value, list):
589
- cmd += "&".join(
590
- [
591
- "pwmd{}={}".format(ix, int(val * 10))
592
- for ix, val in zip(index, value)
593
- ]
594
- )
595
- else:
596
- cmd += "&".join(
597
- ["pwmd{}={}".format(ix, int(value * 10)) for ix in index]
598
- )
599
- else:
600
- cmd += "pwmd{}={}".format(index, int(value * 10))
690
+ cmd = set_cmd_helper(self, "/ind.cgi?", "pwmd", index, value*10, False, False)
601
691
  cmd = cmd.replace("pwmd0", "pwmd")
602
692
  return cmd
603
693
 
@@ -617,7 +707,7 @@ class LK_HW_20(LK_HW_20_PS):
617
707
  index - 1-6
618
708
  value - not used for LK2.X
619
709
  """
620
- cmd = "/ind.cgi?ds={}".format(index)
710
+ cmd = f"/ind.cgi?ds={index}"
621
711
  return cmd
622
712
 
623
713
  def get_ds_id(self) -> str:
@@ -628,10 +718,7 @@ class LK_HW_20(LK_HW_20_PS):
628
718
 
629
719
  @dataclass
630
720
  class LK_HW_25(LK_HW_20):
631
- """Methods for working with LK2.5.
632
-
633
- Note: for outputs it uses unified values 0 - off, 1 - on.
634
- """
721
+ """Methods for working with LK2.5."""
635
722
 
636
723
  info: ClassVar[Union[DeviceInfo, None]] = DeviceInfo(
637
724
  "LK HW 2.5",
@@ -639,13 +726,43 @@ class LK_HW_25(LK_HW_20):
639
726
  "lc25",
640
727
  "https://tinycontrol.pl/en/lan-controller-25/firmware-docs/#firmware",
641
728
  fw_update_method=FWUpdateMethod.TFTP,
729
+ extras={"number_of_outputs": 6, "outputs_inverted": True},
642
730
  )
643
731
 
644
732
  @classmethod
645
733
  def check_version(
646
734
  cls, hardware_version: Union[str, None], software_version: str
647
735
  ) -> bool:
648
- return hardware_version == "2.5" and software_version != "6.15"
736
+ return (
737
+ hardware_version == "2.5"
738
+ and LK_HW_25_PS.check_version(hardware_version, software_version) is False
739
+ )
740
+
741
+ # pylint: disable=useless-super-delegation, useless-parent-delegation
742
+ def _set_pwm(
743
+ self, index: Union[int, List[int]], value: Union[int, List[int]]
744
+ ) -> str:
745
+ """Prepare command for setting PWM.
746
+
747
+ Arguments:
748
+ index: 0-3 (single or list)
749
+ value: 0-1 (single or list)
750
+ NOTE: 1-3 can be only set, not read
751
+ """
752
+ return super()._set_pwm(index, value)
753
+
754
+ # pylint: disable=useless-super-delegation, useless-parent-delegation
755
+ def _set_pwm_duty(
756
+ self, index: Union[int, List[int]], value: Union[int, List[int]]
757
+ ) -> str:
758
+ """Prepare command for setting PWM duty.
759
+
760
+ Arguments:
761
+ index: 0-3 (single or list)
762
+ value: 0-100 (single or list)
763
+ NOTE: 1-3 can be only set, not read
764
+ """
765
+ return super()._set_pwm_duty(index, value)
649
766
 
650
767
 
651
768
  @dataclass
@@ -654,13 +771,31 @@ class LK_HW_25_PS(LK_HW_20_PS):
654
771
  "IP Power Socket v2 (LK2.5)",
655
772
  DeviceFamily.PS,
656
773
  fw_update_method=FWUpdateMethod.TFTP,
774
+ extras={"number_of_outputs": 6, "outputs_inverted": False},
657
775
  )
776
+ mapping: ClassVar[Dict[str, Dict]] = {
777
+ **LK_HW_20_PS.mapping,
778
+ "out0": {"name": "out0", "format": int},
779
+ "out1": {"name": "out1", "format": int},
780
+ "out2": {"name": "out2", "format": int},
781
+ "out3": {"name": "out3", "format": int},
782
+ "out4": {"name": "out4", "format": int},
783
+ "out5": {"name": "out5", "format": int},
784
+ # There are 6 DS instead of 4
785
+ "ia5": {"name": "ds5", "format": float_div10},
786
+ "ia6": {"name": "ds6", "format": float_div10},
787
+ "ia7": {"name": "iAValue1", "format": float_div100}, # Voltage input
788
+ "ia8": {"name": "boardVoltage", "format": float_div10},
789
+ # Names divided with *
790
+ "d": {"name": "dsName1-6_iAName1_iDName1-2", "format": str},
791
+ }
658
792
 
659
793
  @classmethod
660
794
  def check_version(
661
795
  cls, hardware_version: Union[str, None], software_version: str
662
796
  ) -> bool:
663
- return hardware_version == "2.5" and software_version == "6.15"
797
+ # Tested FW 6.15 on LK2.5 and it returns HW 2.0, so include it.
798
+ return hardware_version in ["2.0", "2.5"] and software_version == "6.15"
664
799
 
665
800
 
666
801
  @dataclass
@@ -677,7 +812,7 @@ class LK_HW_30(DeviceModel):
677
812
  "lc30",
678
813
  "https://tinycontrol.pl/en/archives/lan-controller-30/#firmware",
679
814
  fw_update_method=FWUpdateMethod.TFTP,
680
- extras={"number_of_outputs": 6},
815
+ extras={"number_of_outputs": 6, "number_of_digital_inputs": 4},
681
816
  )
682
817
  mapping: ClassVar[Dict[str, Dict]] = {
683
818
  # OUTs - further parsed with _parse_outs
@@ -779,6 +914,7 @@ class LK_HW_30(DeviceModel):
779
914
  "_parse_outs",
780
915
  "_parse_diffs",
781
916
  "_parse_custom_readings",
917
+ "_parse_inpd",
782
918
  ]
783
919
 
784
920
  @classmethod
@@ -915,6 +1051,21 @@ class LK_HW_30(DeviceModel):
915
1051
  int_inverted(data[name]) if out_negation else int(data[name])
916
1052
  )
917
1053
 
1054
+ def _parse_inpd(self, data: Dict[str, Any], path: str):
1055
+ """Parse digital inputs and include negation in returned values."""
1056
+ if "iDNegation1" in data:
1057
+ for name in name_list(
1058
+ "iDNegation", self.info.extras["number_of_digital_inputs"]
1059
+ ):
1060
+ self._context[name] = data[name]
1061
+ if "iDValue1" in data and "iDNegation1" in self._context:
1062
+ for name1, name2 in zip(
1063
+ name_list("iDValue", self.info.extras["number_of_digital_inputs"]),
1064
+ name_list("iDNegation", self.info.extras["number_of_digital_inputs"]),
1065
+ ):
1066
+ if self._context.get(name2):
1067
+ data[name1] = int_inverted(data[name1])
1068
+
918
1069
  # endregion
919
1070
 
920
1071
  def _set_out(
@@ -928,26 +1079,9 @@ class LK_HW_30(DeviceModel):
928
1079
  NOTE: When out is negated it will negate passed value,
929
1080
  so value=1 will actually set 0 and value=0 set 1.
930
1081
  """
931
- cmd = "/outs.cgi?"
932
- if value is not None and self._context.get("out_negation"):
933
- if isinstance(value, list):
934
- value = [int_inverted(val) for val in value]
935
- else:
936
- value = int_inverted(value)
937
- if isinstance(index, list):
938
- if value is None:
939
- cmd += "&".join(["out=out{}".format(ix) for ix in index])
940
- elif isinstance(value, list):
941
- cmd += "&".join(
942
- ["out{}={}".format(ix, val) for ix, val in zip(index, value)]
943
- )
944
- else:
945
- cmd += "&".join(["out{}={}".format(ix, value) for ix in index])
946
- else:
947
- if value is None:
948
- cmd += "out=out{}".format(index)
949
- else:
950
- cmd += "out{}={}".format(index, value)
1082
+ cmd = set_cmd_helper(
1083
+ self, "/outs.cgi?", "out", index, value, self._context["out_negation"], True
1084
+ )
951
1085
  return cmd
952
1086
 
953
1087
  def _set_pwm(
@@ -959,21 +1093,7 @@ class LK_HW_30(DeviceModel):
959
1093
  index: 0-3 (single or list)
960
1094
  value: 0-1 (single or list)
961
1095
  """
962
- cmd = "/outs.cgi?"
963
- if isinstance(index, list):
964
- if value is None:
965
- cmd += "&".join(["pwm=pwm{}".format(ix) for ix in index])
966
- elif isinstance(value, list):
967
- cmd += "&".join(
968
- ["pwm{}={}".format(ix, val) for ix, val in zip(index, value)]
969
- )
970
- else:
971
- cmd += "&".join(["pwm{}={}".format(ix, value) for ix in index])
972
- else:
973
- if value is None:
974
- cmd += "pwm=pwm{}".format(index)
975
- else:
976
- cmd += "pwm{}={}".format(index, value)
1096
+ cmd = set_cmd_helper(self, "/outs.cgi?", "pwm", index, value, False, True)
977
1097
  return cmd
978
1098
 
979
1099
  def _set_pwm_duty(
@@ -985,16 +1105,16 @@ class LK_HW_30(DeviceModel):
985
1105
  index: 0-3 (single or list)
986
1106
  value: 0-100 (single or list)
987
1107
  """
988
- cmd = "/stm.cgi?"
989
- if isinstance(index, list):
990
- if isinstance(value, list):
991
- cmd += "&".join(
992
- ["pwmd={}{}".format(ix, int(val)) for ix, val in zip(index, value)]
993
- )
994
- else:
995
- cmd += "&".join(["pwmd={}{}".format(ix, int(value)) for ix in index])
996
- else:
997
- cmd += "pwmd={}{}".format(index, int(value))
1108
+ cmd = set_cmd_helper(
1109
+ self,
1110
+ "/stm.cgi?",
1111
+ "pwmd",
1112
+ index,
1113
+ value,
1114
+ False,
1115
+ False,
1116
+ "{cmd_param}={index}{value}",
1117
+ )
998
1118
  return cmd
999
1119
 
1000
1120
  def _set_pwm_freq(
@@ -1006,16 +1126,16 @@ class LK_HW_30(DeviceModel):
1006
1126
  index: 0-1 (single or list; 0 - pwm0, 1 - pwm1-3 shared)
1007
1127
  value: 1-1_000_000
1008
1128
  """
1009
- cmd = "/stm.cgi?"
1010
- if isinstance(index, list):
1011
- if isinstance(value, list):
1012
- cmd += "&".join(
1013
- ["pwmf={}{}".format(ix, int(val)) for ix, val in zip(index, value)]
1014
- )
1015
- else:
1016
- cmd += "&".join(["pwmf={}{}".format(ix, int(value)) for ix in index])
1017
- else:
1018
- cmd += "pwmf={}{}".format(index, int(value))
1129
+ cmd = set_cmd_helper(
1130
+ self,
1131
+ "/stm.cgi?",
1132
+ "pwmf",
1133
+ index,
1134
+ value,
1135
+ False,
1136
+ False,
1137
+ "{cmd_param}={index}{value}",
1138
+ )
1019
1139
  return cmd
1020
1140
 
1021
1141
  def _set_ds(
@@ -1027,16 +1147,16 @@ class LK_HW_30(DeviceModel):
1027
1147
  index: 1-8
1028
1148
  value: DS ID
1029
1149
  """
1030
- cmd = "/stm.cgi?"
1031
- if isinstance(index, list):
1032
- if isinstance(value, list):
1033
- cmd += "&".join(
1034
- ["dswrite={}:{}".format(ix, val) for ix, val in zip(index, value)]
1035
- )
1036
- else:
1037
- cmd += "&".join(["dswrite={}:{}".format(ix, value) for ix in index])
1038
- else:
1039
- cmd += "dswrite={}:{}".format(index, value)
1150
+ cmd = set_cmd_helper(
1151
+ self,
1152
+ "/stm.cgi?",
1153
+ "dswrite",
1154
+ index,
1155
+ value,
1156
+ False,
1157
+ False,
1158
+ "{cmd_param}={index}:{value}",
1159
+ )
1040
1160
  return cmd
1041
1161
 
1042
1162
  def get_ds_id(self) -> str:
@@ -1047,8 +1167,10 @@ class LK_HW_30(DeviceModel):
1047
1167
  def _get_all(self) -> List[str]:
1048
1168
  """Prepare list of URLs to fetch data from."""
1049
1169
  urls = ["/json/all.json", "/json/pwmpid.json"]
1050
- if self.hardware_version >= "3.5" and "1.50" > self.software_version >= "1.22b":
1051
- urls.append("/json/events_per.json")
1170
+ # For LK3.0 add few extra paths, as all.json is incomplete there.
1171
+ if LK_HW_30.check_version(self.hardware_version, self.software_version):
1172
+ urls.insert(0, "/json/inputs.json")
1173
+ urls.insert(0, "/json/outputs.json")
1052
1174
  return urls
1053
1175
 
1054
1176
  def _reset_to_defaults(self):
@@ -1074,48 +1196,40 @@ class LK_HW_30(DeviceModel):
1074
1196
  calibration: -32768 - 32767 (calibration offset)
1075
1197
  multiplier: 0.01 - 327.67 (before sending it will be int(X*100)
1076
1198
  """
1077
- cmd = "/inpa.cgi?"
1078
- if isinstance(index, list):
1079
- if isinstance(sensor, list):
1080
- cmd += "&".join(
1081
- [
1082
- "sensor={}{}".format(ix - 1, val)
1083
- for ix, val in zip(index, sensor)
1084
- ]
1085
- )
1086
- else:
1087
- cmd += "&".join(["sensor={}{}".format(ix - 1, sensor) for ix in index])
1088
- if isinstance(calibration, list):
1089
- cmd += "&".join(
1090
- [
1091
- "calibration={}{}".format(ix - 1, val)
1092
- for ix, val in zip(index, calibration)
1093
- ]
1094
- )
1095
- elif calibration is not None:
1096
- cmd += "&".join(
1097
- ["calibration={}{}".format(ix - 1, calibration) for ix in index]
1098
- )
1099
- if isinstance(multiplier, list):
1100
- cmd += "&".join(
1101
- [
1102
- "multiplier={}{}".format(ix - 1, int(val * 100))
1103
- for ix, val in zip(index, multiplier)
1104
- ]
1105
- )
1106
- elif multiplier is not None:
1107
- cmd += "&".join(
1108
- [
1109
- "multiplier={}{}".format(ix - 1, int(multiplier * 100))
1110
- for ix in index
1111
- ]
1112
- )
1113
- else:
1114
- cmd += "sensor={}{}".format(index - 1, sensor)
1115
- if calibration is not None:
1116
- cmd += "&calibration={}{}".format(index - 1, calibration)
1117
- if multiplier is not None:
1118
- cmd += "&multiplier={}{}".format(index - 1, int(multiplier * 100))
1199
+ index = apply_index_offset(index, -1) # -1 each index
1200
+ cmd = set_cmd_helper(
1201
+ self,
1202
+ "/inpa.cgi?",
1203
+ "sensor",
1204
+ index,
1205
+ sensor,
1206
+ False,
1207
+ False,
1208
+ "{cmd_param}={index}{value}",
1209
+ )
1210
+ if calibration is not None:
1211
+ cmd = set_cmd_helper(
1212
+ self,
1213
+ cmd,
1214
+ "calibration",
1215
+ index,
1216
+ calibration,
1217
+ False,
1218
+ False,
1219
+ "{cmd_param}={index}{value}",
1220
+ )
1221
+ if multiplier is not None:
1222
+ multiplier = apply_index_offset(multiplier, 100, lambda a, b: int(a * b))
1223
+ cmd = set_cmd_helper(
1224
+ self,
1225
+ cmd,
1226
+ "multiplier",
1227
+ index,
1228
+ multiplier,
1229
+ False,
1230
+ False,
1231
+ "{cmd_param}={index}{value}",
1232
+ )
1119
1233
  return self.get(cmd)
1120
1234
 
1121
1235
 
@@ -1129,7 +1243,7 @@ class LK_HW_35(LK_HW_30):
1129
1243
  "lc35",
1130
1244
  "https://tinycontrol.pl/en/lan-controller-35/firmware/#firmware",
1131
1245
  fw_update_method=FWUpdateMethod.TFTP,
1132
- extras={"number_of_outputs": 6},
1246
+ extras={"number_of_outputs": 6, "number_of_digital_inputs": 4},
1133
1247
  )
1134
1248
 
1135
1249
  @classmethod
@@ -1142,6 +1256,20 @@ class LK_HW_35(LK_HW_30):
1142
1256
  and not software_version.endswith("dcdc")
1143
1257
  )
1144
1258
 
1259
+ # region Parser methods that modifies data in _get()
1260
+ def _parse_inpd(self, data: Dict[str, Any], path: str):
1261
+ """Apply fix for negation of digital inputs for earlier SW.
1262
+
1263
+ Since SW 1.49, LK 3.5 automatically applies negation to readings,
1264
+ but older SWs works like LK3.0, so for those use inherited parsing.
1265
+ """
1266
+ if (
1267
+ LK_HW_35.check_version(self.hardware_version, self.software_version)
1268
+ and self.software_version < "1.49"
1269
+ ):
1270
+ super()._parse_inpd(data, path)
1271
+ # endregion
1272
+
1145
1273
  def _set_var(
1146
1274
  self, index: Union[int, List[int]], value: Union[int, List[int]]
1147
1275
  ) -> str:
@@ -1151,18 +1279,19 @@ class LK_HW_35(LK_HW_30):
1151
1279
  index: 1-8 (single or list)
1152
1280
  value: 0-1 (single or list)
1153
1281
  """
1154
- cmd = "/outs.cgi?"
1155
- if isinstance(index, list):
1156
- if isinstance(value, list):
1157
- cmd += "&".join(
1158
- ["vout{}={}".format(ix - 1, val) for ix, val in zip(index, value)]
1159
- )
1160
- else:
1161
- cmd += "&".join(["vout{}={}".format(ix - 1, value) for ix in index])
1162
- else:
1163
- cmd += "vout{}={}".format(index - 1, value)
1282
+ # Fix indexes to be 0-based, as outside we use 1-based indexes/names for VARs.
1283
+ index = apply_index_offset(index, -1)
1284
+ cmd = set_cmd_helper(self, "/outs.cgi?", "vout", index, value, False, False)
1164
1285
  return cmd
1165
1286
 
1287
+ def _get_all(self) -> List[str]:
1288
+ """Prepare list of URLs to fetch data from."""
1289
+ urls = super()._get_all()
1290
+ # Early SWs for LK3.5 require extra path to get state of EVENT.
1291
+ if LK_HW_35.check_version(self.hardware_version, self.software_version) and "1.50" > self.software_version >= "1.22b":
1292
+ urls.append("/json/events_per.json")
1293
+ return urls
1294
+
1166
1295
 
1167
1296
  @dataclass
1168
1297
  class LK_HW_39(LK_HW_35):
@@ -1242,6 +1371,10 @@ class LK_HW_40(DeviceModel):
1242
1371
  "netMac": {"name": "mac", "format": str},
1243
1372
  "softwareVersion": {"name": "software_version", "format": str},
1244
1373
  "hardwareVersion": {"name": "hardware_version", "format": str},
1374
+ "pm1": {"name": "pm1.0", "format": float},
1375
+ "pm2": {"name": "pm2.5", "format": float},
1376
+ "pm4": {"name": "pm4.0", "format": float},
1377
+ "pm10": {"name": "pm10.0", "format": float},
1245
1378
  }
1246
1379
  parsers: ClassVar[List[str]] = ["_parse_outs"]
1247
1380
 
@@ -1273,26 +1406,15 @@ class LK_HW_40(DeviceModel):
1273
1406
  NOTE: When out is negated it will negate passed value,
1274
1407
  so value=1 will actually set 0 and value=0 set 1.
1275
1408
  """
1276
- cmd = "/api/v1/save/?"
1277
- if value is not None and self._context.get("out_negation"):
1278
- if isinstance(value, list):
1279
- value = [int_inverted(val) for val in value]
1280
- else:
1281
- value = int_inverted(value)
1282
- if isinstance(index, list):
1283
- if value is None:
1284
- cmd += "&".join(["out=out{}".format(ix) for ix in index])
1285
- elif isinstance(value, list):
1286
- cmd += "&".join(
1287
- ["out{}={}".format(ix, val) for ix, val in zip(index, value)]
1288
- )
1289
- else:
1290
- cmd += "&".join(["out{}={}".format(ix, value) for ix in index])
1291
- else:
1292
- if value is None:
1293
- cmd += "out=out{}".format(index)
1294
- else:
1295
- cmd += "out{}={}".format(index, value)
1409
+ cmd = set_cmd_helper(
1410
+ self,
1411
+ "/api/v1/save/?",
1412
+ "out",
1413
+ index,
1414
+ value,
1415
+ self._context["out_negation"],
1416
+ True,
1417
+ )
1296
1418
  return cmd
1297
1419
 
1298
1420
  def _set_pwm(
@@ -1304,21 +1426,7 @@ class LK_HW_40(DeviceModel):
1304
1426
  index: 1-3 (single or list)
1305
1427
  value: 0-1 (single or list)
1306
1428
  """
1307
- cmd = "/api/v1/save/?"
1308
- if isinstance(index, list):
1309
- if value is None:
1310
- cmd += "&".join(["pwm=pwm{}".format(ix) for ix in index])
1311
- elif isinstance(value, list):
1312
- cmd += "&".join(
1313
- ["pwm{}={}".format(ix, val) for ix, val in zip(index, value)]
1314
- )
1315
- else:
1316
- cmd += "&".join(["pwm{}={}".format(ix, value) for ix in index])
1317
- else:
1318
- if value is None:
1319
- cmd += "pwm=pwm{}".format(index)
1320
- else:
1321
- cmd += "pwm{}={}".format(index, value)
1429
+ cmd = set_cmd_helper(self, "/api/v1/save/?", "pwm", index, value, False, True)
1322
1430
  return cmd
1323
1431
 
1324
1432
  def _set_pwm_duty(
@@ -1330,19 +1438,7 @@ class LK_HW_40(DeviceModel):
1330
1438
  index: 1-3 (single or list)
1331
1439
  value: 0-100 (single or list)
1332
1440
  """
1333
- cmd = "/api/v1/save/?"
1334
- if isinstance(index, list):
1335
- if isinstance(value, list):
1336
- cmd += "&".join(
1337
- [
1338
- "pwmDuty{}={}".format(ix, int(val))
1339
- for ix, val in zip(index, value)
1340
- ]
1341
- )
1342
- else:
1343
- cmd += "&".join(["pwmDuty{}={}".format(ix, int(value)) for ix in index])
1344
- else:
1345
- cmd += "pwmDuty{}={}".format(index, int(value))
1441
+ cmd = set_cmd_helper(self, "/api/v1/save/?", "pwmDuty", index, value, False, False)
1346
1442
  return cmd
1347
1443
 
1348
1444
  def _set_pwm_freq(self, index: Any, value: int) -> str:
@@ -1364,16 +1460,7 @@ class LK_HW_40(DeviceModel):
1364
1460
  index: 1-8 (single or list)
1365
1461
  value: 0-1 (single or list)
1366
1462
  """
1367
- cmd = "/api/v1/save/?"
1368
- if isinstance(index, list):
1369
- if isinstance(value, list):
1370
- cmd += "&".join(
1371
- ["var{}={}".format(ix, val) for ix, val in zip(index, value)]
1372
- )
1373
- else:
1374
- cmd += "&".join(["var{}={}".format(ix, value) for ix in index])
1375
- else:
1376
- cmd += "var{}={}".format(index, value)
1463
+ cmd = set_cmd_helper(self, "/api/v1/save/?", "var", index, value, False, False)
1377
1464
  return cmd
1378
1465
 
1379
1466
  def _set_ds(
@@ -1385,16 +1472,7 @@ class LK_HW_40(DeviceModel):
1385
1472
  index: 1-8
1386
1473
  value: DS ID
1387
1474
  """
1388
- cmd = "/api/v1/save/?"
1389
- if isinstance(index, list):
1390
- if isinstance(value, list):
1391
- cmd += "&".join(
1392
- ["dsID{}={}".format(ix, val) for ix, val in zip(index, value)]
1393
- )
1394
- else:
1395
- cmd += "&".join(["dsID{}={}".format(ix, value) for ix in index])
1396
- else:
1397
- cmd += "dsID{}={}".format(index, value)
1475
+ cmd = set_cmd_helper(self, "/api/v1/save/?", "dsID", index, value, False, False)
1398
1476
  return cmd
1399
1477
 
1400
1478
  def get_ds_id(self) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinytoolslib
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Set of tools for use with Tinycontrol devices like LK2.X, LK3.X, LK4.X or tcPDU.
5
5
  Author-email: Bartek Barszczewski <tinycontrol.software@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -1 +0,0 @@
1
- __version__ = '0.4.0'
File without changes
File without changes