aiobmsble 0.1.0__py3-none-any.whl → 0.2.1__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.
Files changed (37) hide show
  1. aiobmsble/__init__.py +53 -8
  2. aiobmsble/__main__.py +51 -27
  3. aiobmsble/basebms.py +266 -50
  4. aiobmsble/bms/__init__.py +1 -0
  5. aiobmsble/bms/abc_bms.py +164 -0
  6. aiobmsble/bms/ant_bms.py +196 -0
  7. aiobmsble/bms/braunpwr_bms.py +167 -0
  8. aiobmsble/bms/cbtpwr_bms.py +168 -0
  9. aiobmsble/bms/cbtpwr_vb_bms.py +184 -0
  10. aiobmsble/bms/daly_bms.py +164 -0
  11. aiobmsble/bms/dpwrcore_bms.py +207 -0
  12. aiobmsble/bms/dummy_bms.py +89 -0
  13. aiobmsble/bms/ecoworthy_bms.py +151 -0
  14. aiobmsble/bms/ective_bms.py +177 -0
  15. aiobmsble/bms/ej_bms.py +233 -0
  16. aiobmsble/bms/felicity_bms.py +139 -0
  17. aiobmsble/bms/jbd_bms.py +203 -0
  18. aiobmsble/bms/jikong_bms.py +301 -0
  19. aiobmsble/bms/neey_bms.py +214 -0
  20. aiobmsble/bms/ogt_bms.py +214 -0
  21. aiobmsble/bms/pro_bms.py +144 -0
  22. aiobmsble/bms/redodo_bms.py +127 -0
  23. aiobmsble/bms/renogy_bms.py +149 -0
  24. aiobmsble/bms/renogy_pro_bms.py +105 -0
  25. aiobmsble/bms/roypow_bms.py +186 -0
  26. aiobmsble/bms/seplos_bms.py +245 -0
  27. aiobmsble/bms/seplos_v2_bms.py +205 -0
  28. aiobmsble/bms/tdt_bms.py +199 -0
  29. aiobmsble/bms/tianpwr_bms.py +138 -0
  30. aiobmsble/utils.py +96 -6
  31. {aiobmsble-0.1.0.dist-info → aiobmsble-0.2.1.dist-info}/METADATA +23 -14
  32. aiobmsble-0.2.1.dist-info/RECORD +36 -0
  33. {aiobmsble-0.1.0.dist-info → aiobmsble-0.2.1.dist-info}/WHEEL +1 -1
  34. aiobmsble-0.1.0.dist-info/RECORD +0 -10
  35. {aiobmsble-0.1.0.dist-info → aiobmsble-0.2.1.dist-info}/entry_points.txt +0 -0
  36. {aiobmsble-0.1.0.dist-info → aiobmsble-0.2.1.dist-info}/licenses/LICENSE +0 -0
  37. {aiobmsble-0.1.0.dist-info → aiobmsble-0.2.1.dist-info}/top_level.txt +0 -0
aiobmsble/__init__.py CHANGED
@@ -1,9 +1,12 @@
1
1
  """Package for battery management systems (BMS) via Bluetooth LE."""
2
2
 
3
- from typing import Literal, TypedDict
3
+ from collections.abc import Callable
4
+ from enum import IntEnum
5
+ from typing import Any, Literal, NamedTuple, TypedDict
4
6
 
5
7
  type BMSvalue = Literal[
6
8
  "battery_charging",
9
+ "battery_mode",
7
10
  "battery_level",
8
11
  "current",
9
12
  "power",
@@ -15,36 +18,77 @@ type BMSvalue = Literal[
15
18
  "delta_voltage",
16
19
  "problem",
17
20
  "runtime",
21
+ "balance_current",
22
+ "cell_count",
18
23
  "cell_voltages",
19
24
  "design_capacity",
25
+ "pack_count",
26
+ "temp_sensors",
20
27
  "temp_values",
21
28
  "problem_code",
22
29
  ]
23
30
 
31
+ type BMSpackvalue = Literal[
32
+ "pack_voltages",
33
+ "pack_currents",
34
+ "pack_battery_levels",
35
+ "pack_cycles",
36
+ ]
37
+
38
+
39
+ class BMSmode(IntEnum):
40
+ """Enumeration of BMS modes."""
41
+
42
+ UNKNOWN = -1
43
+ BULK = 0x00
44
+ ABSORPTION = 0x01
45
+ FLOAT = 0x02
46
+
24
47
 
25
48
  class BMSsample(TypedDict, total=False):
26
49
  """Dictionary representing a sample of battery management system (BMS) data."""
27
50
 
28
- battery_charging: bool
51
+ battery_charging: bool # True: battery charging
52
+ battery_mode: BMSmode # BMS charging mode
29
53
  battery_level: int | float # [%]
30
- current: float # [A]
31
- power: float # [W]
54
+ current: float # [A] (positive: charging)
55
+ power: float # [W] (positive: charging)
32
56
  temperature: int | float # [°C]
33
57
  voltage: float # [V]
34
58
  cycle_capacity: int | float # [Wh]
35
59
  cycles: int # [#]
36
60
  delta_voltage: float # [V]
37
- problem: bool
61
+ problem: bool # True: problem detected
38
62
  runtime: int # [s]
39
- # internal
63
+ # detailed information
64
+ balance_current: float # [A]
65
+ cell_count: int # [#]
40
66
  cell_voltages: list[float] # [V]
41
67
  cycle_charge: int | float # [Ah]
42
68
  design_capacity: int # [Ah]
69
+ pack_count: int # [#]
70
+ temp_sensors: int # [#]
43
71
  temp_values: list[int | float] # [°C]
44
- problem_code: int
72
+ problem_code: int # BMS specific code, 0 no problem
73
+ # battery pack data
74
+ pack_voltages: list[float] # [V]
75
+ pack_currents: list[float] # [A]
76
+ pack_battery_levels: list[int | float] # [%]
77
+ pack_cycles: list[int] # [#]
78
+
79
+
80
+ class BMSdp(NamedTuple):
81
+ """Representation of one BMS data point."""
82
+
83
+ key: BMSvalue # the key of the value to be parsed
84
+ pos: int # position within the message
85
+ size: int # size in bytes
86
+ signed: bool # signed value
87
+ fct: Callable[[int], Any] = lambda x: x # conversion function (default do nothing)
88
+ idx: int = -1 # array index containing the message to be parsed
45
89
 
46
90
 
47
- class AdvertisementPattern(TypedDict, total=False):
91
+ class MatcherPattern(TypedDict, total=False):
48
92
  """Optional patterns that can match Bleak advertisement data."""
49
93
 
50
94
  local_name: str # name pattern that supports Unix shell-style wildcards
@@ -52,3 +96,4 @@ class AdvertisementPattern(TypedDict, total=False):
52
96
  service_data_uuid: str # service data for the service UUID
53
97
  manufacturer_id: int # required manufacturer ID
54
98
  manufacturer_data_start: list[int] # required starting bytes of manufacturer data
99
+ connectable: bool # True if active connections to the device are required
aiobmsble/__main__.py CHANGED
@@ -1,8 +1,9 @@
1
1
  """Example function for package usage."""
2
2
 
3
+ import argparse
3
4
  import asyncio
4
5
  import logging
5
- from types import ModuleType
6
+ from typing import Final
6
7
 
7
8
  from bleak import BleakScanner
8
9
  from bleak.backends.device import BLEDevice
@@ -10,31 +11,31 @@ from bleak.backends.scanner import AdvertisementData
10
11
  from bleak.exc import BleakError
11
12
 
12
13
  from aiobmsble import BMSsample
13
- from aiobmsble.bms import ogt_bms
14
- from aiobmsble.utils import bms_supported
15
-
16
- bms_plugins: list[ModuleType] = [ogt_bms]
14
+ from aiobmsble.basebms import BaseBMS
15
+ from aiobmsble.utils import bms_identify
17
16
 
18
17
  logging.basicConfig(
19
18
  format="%(levelname)s: %(message)s",
20
19
  level=logging.INFO,
21
20
  )
22
- logger: logging.Logger = logging.getLogger(__name__)
23
-
24
- logger.info(
25
- "loaded BMS types: %s", [key.__name__.rsplit(".", 1)[-1] for key in bms_plugins]
26
- )
21
+ logger: logging.Logger = logging.getLogger(__package__)
27
22
 
28
23
 
29
- async def detect_bms() -> None:
30
- """Query a Bluetooth device based on the provided arguments."""
31
-
24
+ async def scan_devices() -> dict[str, tuple[BLEDevice, AdvertisementData]]:
25
+ """Scan for BLE devices and return results."""
32
26
  logger.info("starting scan...")
33
27
  scan_result: dict[str, tuple[BLEDevice, AdvertisementData]] = (
34
28
  await BleakScanner.discover(return_adv=True)
35
29
  )
30
+ logger.info(scan_result)
36
31
  logger.info("%i BT devices in range.", len(scan_result))
32
+ return scan_result
37
33
 
34
+
35
+ async def detect_bms() -> None:
36
+ """Query a Bluetooth device based on the provided arguments."""
37
+
38
+ scan_result: dict[str, tuple[BLEDevice, AdvertisementData]] = await scan_devices()
38
39
  for ble_dev, advertisement in scan_result.values():
39
40
  logger.info(
40
41
  "%s\nBT device '%s' (%s)\n\t%s",
@@ -43,27 +44,50 @@ async def detect_bms() -> None:
43
44
  ble_dev.address,
44
45
  repr(advertisement).replace(", ", ",\n\t"),
45
46
  )
46
- for bms_module in bms_plugins:
47
- if bms_supported(bms_module.BMS, advertisement):
48
- logger.info(
49
- "Found matching BMS type: %s",
50
- bms_module.__name__.rsplit(".", maxsplit=1)[-1],
51
- )
52
- bms = bms_module.BMS(ble_device=ble_dev, reconnect=True)
53
- try:
54
- logger.info("Updating BMS data...")
55
- data: BMSsample = await bms.async_update()
56
- logger.info("BMS data: %s", repr(data).replace(", ", ",\n\t"))
57
- except BleakError as ex:
58
- logger.error("Failed to update BMS: %s", ex)
47
+
48
+ if bms_cls := bms_identify(advertisement):
49
+ logger.info("Found matching BMS type: %s", bms_cls.device_id())
50
+ bms: BaseBMS = bms_cls(ble_device=ble_dev, reconnect=True)
51
+
52
+ try:
53
+ logger.info("Updating BMS data...")
54
+ data: BMSsample = await bms.async_update()
55
+ logger.info("BMS data: %s", repr(data).replace(", '", ",\n\t'"))
56
+ except (BleakError, TimeoutError) as exc:
57
+ logger.error("Failed to update BMS: %s", type(exc).__name__)
59
58
 
60
59
  logger.info("done.")
61
60
 
62
61
 
62
+ def setup_logging(args: argparse.Namespace) -> None:
63
+ """Configure logging based on command line arguments."""
64
+ loglevel: Final[int] = logging.DEBUG if args.verbose else logging.INFO
65
+
66
+ if args.logfile:
67
+ file_handler = logging.FileHandler(args.logfile)
68
+ file_handler.setLevel(loglevel)
69
+ file_handler.setFormatter(
70
+ logging.Formatter("%(asctime)s - %(levelname)s: %(message)s")
71
+ )
72
+ logger.addHandler(file_handler)
73
+
74
+ logger.setLevel(loglevel)
75
+
76
+
63
77
  def main() -> None:
64
78
  """Entry point for the script to run the BMS detection."""
79
+ parser = argparse.ArgumentParser(
80
+ description="Reference script for 'aiobmsble' to show all recognized BMS in range."
81
+ )
82
+ parser.add_argument("-l", "--logfile", type=str, help="Path to the log file")
83
+ parser.add_argument(
84
+ "-v", "--verbose", action="store_true", help="Enable debug logging"
85
+ )
86
+
87
+ setup_logging(parser.parse_args())
88
+
65
89
  asyncio.run(detect_bms())
66
90
 
67
91
 
68
92
  if __name__ == "__main__":
69
- main()
93
+ main() # pragma: no cover