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.
- aiobmsble/__init__.py +53 -8
- aiobmsble/__main__.py +51 -27
- aiobmsble/basebms.py +266 -50
- aiobmsble/bms/__init__.py +1 -0
- aiobmsble/bms/abc_bms.py +164 -0
- aiobmsble/bms/ant_bms.py +196 -0
- aiobmsble/bms/braunpwr_bms.py +167 -0
- aiobmsble/bms/cbtpwr_bms.py +168 -0
- aiobmsble/bms/cbtpwr_vb_bms.py +184 -0
- aiobmsble/bms/daly_bms.py +164 -0
- aiobmsble/bms/dpwrcore_bms.py +207 -0
- aiobmsble/bms/dummy_bms.py +89 -0
- aiobmsble/bms/ecoworthy_bms.py +151 -0
- aiobmsble/bms/ective_bms.py +177 -0
- aiobmsble/bms/ej_bms.py +233 -0
- aiobmsble/bms/felicity_bms.py +139 -0
- aiobmsble/bms/jbd_bms.py +203 -0
- aiobmsble/bms/jikong_bms.py +301 -0
- aiobmsble/bms/neey_bms.py +214 -0
- aiobmsble/bms/ogt_bms.py +214 -0
- aiobmsble/bms/pro_bms.py +144 -0
- aiobmsble/bms/redodo_bms.py +127 -0
- aiobmsble/bms/renogy_bms.py +149 -0
- aiobmsble/bms/renogy_pro_bms.py +105 -0
- aiobmsble/bms/roypow_bms.py +186 -0
- aiobmsble/bms/seplos_bms.py +245 -0
- aiobmsble/bms/seplos_v2_bms.py +205 -0
- aiobmsble/bms/tdt_bms.py +199 -0
- aiobmsble/bms/tianpwr_bms.py +138 -0
- aiobmsble/utils.py +96 -6
- {aiobmsble-0.1.0.dist-info → aiobmsble-0.2.1.dist-info}/METADATA +23 -14
- aiobmsble-0.2.1.dist-info/RECORD +36 -0
- {aiobmsble-0.1.0.dist-info → aiobmsble-0.2.1.dist-info}/WHEEL +1 -1
- aiobmsble-0.1.0.dist-info/RECORD +0 -10
- {aiobmsble-0.1.0.dist-info → aiobmsble-0.2.1.dist-info}/entry_points.txt +0 -0
- {aiobmsble-0.1.0.dist-info → aiobmsble-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {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
|
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
|
-
#
|
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
|
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
|
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.
|
14
|
-
from aiobmsble.utils import
|
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(
|
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
|
30
|
-
"""
|
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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|