aiobmsble 0.2.3__tar.gz → 0.3.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.
- {aiobmsble-0.2.3/aiobmsble.egg-info → aiobmsble-0.3.0}/PKG-INFO +14 -14
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/README.md +11 -7
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/__init__.py +2 -0
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/__main__.py +3 -1
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/basebms.py +38 -13
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/abc_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/ant_bms.py +4 -3
- aiobmsble-0.3.0/aiobmsble/bms/ant_leg_bms.py +177 -0
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/braunpwr_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/cbtpwr_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/cbtpwr_vb_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/daly_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/dpwrcore_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/dummy_bms.py +3 -3
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/ecoworthy_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/ective_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/ej_bms.py +9 -3
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/felicity_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/jbd_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/jikong_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/neey_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/ogt_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/pro_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/redodo_bms.py +5 -2
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/renogy_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/renogy_pro_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/roypow_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/seplos_bms.py +3 -3
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/seplos_v2_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/tdt_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/tianpwr_bms.py +2 -2
- {aiobmsble-0.2.3 → aiobmsble-0.3.0/aiobmsble.egg-info}/PKG-INFO +14 -14
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble.egg-info/SOURCES.txt +1 -0
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble.egg-info/requires.txt +2 -6
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/pyproject.toml +4 -8
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/tests/test_basebms.py +53 -4
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/LICENSE +0 -0
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/MANIFEST.in +0 -0
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/__init__.py +0 -0
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/utils.py +0 -0
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble.egg-info/dependency_links.txt +0 -0
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble.egg-info/entry_points.txt +0 -0
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble.egg-info/top_level.txt +0 -0
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/setup.cfg +0 -0
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/tests/test_examples.py +0 -0
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/tests/test_fuzzing.py +0 -0
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/tests/test_main.py +0 -0
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/tests/test_plugins.py +0 -0
- {aiobmsble-0.2.3 → aiobmsble-0.3.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: aiobmsble
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
4
|
Summary: Asynchronous Python library to query battery management systems via Bluetooth Low Energy.
|
5
5
|
Author: Patrick Loschmidt
|
6
6
|
Maintainer: Patrick Loschmidt
|
@@ -18,12 +18,8 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
18
|
Requires-Python: >=3.12
|
19
19
|
Description-Content-Type: text/markdown
|
20
20
|
License-File: LICENSE
|
21
|
-
Requires-Dist: bleak
|
21
|
+
Requires-Dist: bleak>=1.0.1
|
22
22
|
Requires-Dist: bleak-retry-connector>=4.0.2
|
23
|
-
Requires-Dist: asyncio
|
24
|
-
Requires-Dist: logging
|
25
|
-
Requires-Dist: statistics
|
26
|
-
Requires-Dist: typing
|
27
23
|
Provides-Extra: dev
|
28
24
|
Requires-Dist: pytest; extra == "dev"
|
29
25
|
Requires-Dist: pytest-asyncio; extra == "dev"
|
@@ -31,7 +27,7 @@ Requires-Dist: pytest-cov; extra == "dev"
|
|
31
27
|
Requires-Dist: pytest-xdist; extra == "dev"
|
32
28
|
Requires-Dist: hypothesis; extra == "dev"
|
33
29
|
Requires-Dist: mypy; extra == "dev"
|
34
|
-
Requires-Dist: ruff; extra == "dev"
|
30
|
+
Requires-Dist: ruff~=0.12.1; extra == "dev"
|
35
31
|
Dynamic: license-file
|
36
32
|
|
37
33
|
[![License][license-shield]](LICENSE)
|
@@ -55,7 +51,11 @@ from the command line after [installation](#installation). In case you need the
|
|
55
51
|
### From a Script
|
56
52
|
This example can also be found as an [example](/examples/minimal.py) in the respective [folder](/main/examples).
|
57
53
|
```python
|
58
|
-
"""Example of using the aiobmsble library to find a BLE device by name and print its
|
54
|
+
"""Example of using the aiobmsble library to find a BLE device by name and print its sensor data.
|
55
|
+
|
56
|
+
Project: aiobmsble, https://pypi.org/p/aiobmsble/
|
57
|
+
License: Apache-2.0, http://www.apache.org/licenses/
|
58
|
+
"""
|
59
59
|
|
60
60
|
import asyncio
|
61
61
|
import logging
|
@@ -66,9 +66,9 @@ from bleak.backends.device import BLEDevice
|
|
66
66
|
from bleak.exc import BleakError
|
67
67
|
|
68
68
|
from aiobmsble import BMSsample
|
69
|
-
from aiobmsble.bms.dummy_bms import BMS # use the right BMS class for your device
|
69
|
+
from aiobmsble.bms.dummy_bms import BMS # TODO: use the right BMS class for your device
|
70
70
|
|
71
|
-
NAME: Final[str] = "BT Device Name" #
|
71
|
+
NAME: Final[str] = "BT Device Name" # TODO: replace with the name of your BLE device
|
72
72
|
|
73
73
|
# Configure logging
|
74
74
|
logging.basicConfig(level=logging.INFO)
|
@@ -84,11 +84,11 @@ async def main(dev_name) -> None:
|
|
84
84
|
return
|
85
85
|
|
86
86
|
logger.info("Found device: %s (%s)", device.name, device.address)
|
87
|
-
bms = BMS(ble_device=device, reconnect=True)
|
88
87
|
try:
|
89
|
-
|
90
|
-
|
91
|
-
|
88
|
+
async with BMS(ble_device=device) as bms:
|
89
|
+
logger.info("Updating BMS data...")
|
90
|
+
data: BMSsample = await bms.async_update()
|
91
|
+
logger.info("BMS data: %s", repr(data).replace(", ", ",\n\t"))
|
92
92
|
except BleakError as ex:
|
93
93
|
logger.error("Failed to update BMS: %s", type(ex).__name__)
|
94
94
|
|
@@ -19,7 +19,11 @@ from the command line after [installation](#installation). In case you need the
|
|
19
19
|
### From a Script
|
20
20
|
This example can also be found as an [example](/examples/minimal.py) in the respective [folder](/main/examples).
|
21
21
|
```python
|
22
|
-
"""Example of using the aiobmsble library to find a BLE device by name and print its
|
22
|
+
"""Example of using the aiobmsble library to find a BLE device by name and print its sensor data.
|
23
|
+
|
24
|
+
Project: aiobmsble, https://pypi.org/p/aiobmsble/
|
25
|
+
License: Apache-2.0, http://www.apache.org/licenses/
|
26
|
+
"""
|
23
27
|
|
24
28
|
import asyncio
|
25
29
|
import logging
|
@@ -30,9 +34,9 @@ from bleak.backends.device import BLEDevice
|
|
30
34
|
from bleak.exc import BleakError
|
31
35
|
|
32
36
|
from aiobmsble import BMSsample
|
33
|
-
from aiobmsble.bms.dummy_bms import BMS # use the right BMS class for your device
|
37
|
+
from aiobmsble.bms.dummy_bms import BMS # TODO: use the right BMS class for your device
|
34
38
|
|
35
|
-
NAME: Final[str] = "BT Device Name" #
|
39
|
+
NAME: Final[str] = "BT Device Name" # TODO: replace with the name of your BLE device
|
36
40
|
|
37
41
|
# Configure logging
|
38
42
|
logging.basicConfig(level=logging.INFO)
|
@@ -48,11 +52,11 @@ async def main(dev_name) -> None:
|
|
48
52
|
return
|
49
53
|
|
50
54
|
logger.info("Found device: %s (%s)", device.name, device.address)
|
51
|
-
bms = BMS(ble_device=device, reconnect=True)
|
52
55
|
try:
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
+
async with BMS(ble_device=device) as bms:
|
57
|
+
logger.info("Updating BMS data...")
|
58
|
+
data: BMSsample = await bms.async_update()
|
59
|
+
logger.info("BMS data: %s", repr(data).replace(", ", ",\n\t"))
|
56
60
|
except BleakError as ex:
|
57
61
|
logger.error("Failed to update BMS: %s", type(ex).__name__)
|
58
62
|
|
@@ -19,6 +19,7 @@ type BMSvalue = Literal[
|
|
19
19
|
"cycles",
|
20
20
|
"cycle_capacity",
|
21
21
|
"cycle_charge",
|
22
|
+
"total_charge",
|
22
23
|
"delta_voltage",
|
23
24
|
"problem",
|
24
25
|
"runtime",
|
@@ -69,6 +70,7 @@ class BMSsample(TypedDict, total=False):
|
|
69
70
|
cell_count: int # [#]
|
70
71
|
cell_voltages: list[float] # [V]
|
71
72
|
cycle_charge: int | float # [Ah]
|
73
|
+
total_charge: int # [Ah], overall discharged
|
72
74
|
design_capacity: int # [Ah]
|
73
75
|
pack_count: int # [#]
|
74
76
|
temp_sensors: int # [#]
|
@@ -51,7 +51,7 @@ async def detect_bms() -> None:
|
|
51
51
|
|
52
52
|
if bms_cls := bms_identify(advertisement):
|
53
53
|
logger.info("Found matching BMS type: %s", bms_cls.device_id())
|
54
|
-
bms: BaseBMS = bms_cls(ble_device=ble_dev
|
54
|
+
bms: BaseBMS = bms_cls(ble_device=ble_dev)
|
55
55
|
|
56
56
|
try:
|
57
57
|
logger.info("Updating BMS data...")
|
@@ -59,6 +59,8 @@ async def detect_bms() -> None:
|
|
59
59
|
logger.info("BMS data: %s", repr(data).replace(", '", ",\n\t'"))
|
60
60
|
except (BleakError, TimeoutError) as exc:
|
61
61
|
logger.error("Failed to update BMS: %s", type(exc).__name__)
|
62
|
+
finally:
|
63
|
+
await bms.disconnect()
|
62
64
|
|
63
65
|
logger.info("done.")
|
64
66
|
|
@@ -9,7 +9,8 @@ import asyncio
|
|
9
9
|
from collections.abc import Callable, MutableMapping
|
10
10
|
import logging
|
11
11
|
from statistics import fmean
|
12
|
-
from
|
12
|
+
from types import TracebackType
|
13
|
+
from typing import Any, Final, Literal, Self
|
13
14
|
|
14
15
|
from bleak import BleakClient
|
15
16
|
from bleak.backends.characteristic import BleakGATTCharacteristic
|
@@ -44,26 +45,28 @@ class BaseBMS(ABC):
|
|
44
45
|
def __init__(
|
45
46
|
self,
|
46
47
|
ble_device: BLEDevice,
|
47
|
-
|
48
|
+
keep_alive: bool = True,
|
48
49
|
logger_name: str = "",
|
49
50
|
) -> None:
|
50
51
|
"""Intialize the BMS.
|
51
52
|
|
52
|
-
|
53
|
+
`_notification_handler`: the callback function used for notifications from `uuid_rx()`
|
53
54
|
characteristic. Not defined as abstract in this base class, as it can be both,
|
54
55
|
a normal or async function
|
55
56
|
|
56
57
|
Args:
|
57
|
-
logger_name (str): name of the logger for the BMS instance (usually file name)
|
58
58
|
ble_device (BLEDevice): the Bleak device to connect to
|
59
|
-
|
59
|
+
keep_alive (bool): if true, the connection will be kept active after each update.
|
60
|
+
Make sure to call `disconnect()` when done using the BMS class or better use
|
61
|
+
`async with` context manager (requires `keep_alive=True`).
|
62
|
+
logger_name (str): name of the logger for the BMS instance, default: module name
|
60
63
|
|
61
64
|
"""
|
62
65
|
assert (
|
63
66
|
getattr(self, "_notification_handler", None) is not None
|
64
67
|
), "BMS class must define _notification_handler method"
|
65
68
|
self._ble_device: Final[BLEDevice] = ble_device
|
66
|
-
self.
|
69
|
+
self._keep_alive: Final[bool] = keep_alive
|
67
70
|
self.name: Final[str] = self._ble_device.name or "undefined"
|
68
71
|
self._inv_wr_mode: bool | None = None # invert write mode (WNR <-> W)
|
69
72
|
logger_name = logger_name or self.__class__.__module__
|
@@ -83,6 +86,22 @@ class BaseBMS(ABC):
|
|
83
86
|
self._data: bytearray = bytearray()
|
84
87
|
self._data_event: Final[asyncio.Event] = asyncio.Event()
|
85
88
|
|
89
|
+
async def __aenter__(self) -> Self:
|
90
|
+
"""Asynchronous context manager to implement `async with` functionality."""
|
91
|
+
if not self._keep_alive:
|
92
|
+
raise ValueError("usage of context manager requires `keep_alive=True`.")
|
93
|
+
await self._connect()
|
94
|
+
return self
|
95
|
+
|
96
|
+
async def __aexit__(
|
97
|
+
self,
|
98
|
+
typ: type[BaseException] | None,
|
99
|
+
exc: BaseException | None,
|
100
|
+
tb: TracebackType | None,
|
101
|
+
) -> None:
|
102
|
+
"""Asynchronous context manager exit functionality."""
|
103
|
+
await self.disconnect()
|
104
|
+
|
86
105
|
@classmethod
|
87
106
|
def get_bms_module(cls) -> str:
|
88
107
|
"""Return BMS module name, e.g. aiobmsble.bms.dummy_bms."""
|
@@ -177,6 +196,10 @@ class BaseBMS(ABC):
|
|
177
196
|
{"voltage", "cycle_charge"},
|
178
197
|
lambda: round(data.get("voltage", 0) * data.get("cycle_charge", 0), 3),
|
179
198
|
),
|
199
|
+
"cycles": (
|
200
|
+
{"design_capacity", "total_charge"},
|
201
|
+
lambda: data.get("total_charge", 0) // data.get("design_capacity", 0),
|
202
|
+
),
|
180
203
|
"power": (
|
181
204
|
{"voltage", "current"},
|
182
205
|
lambda: round(data.get("voltage", 0) * current, 3),
|
@@ -384,7 +407,7 @@ class BaseBMS(ABC):
|
|
384
407
|
data: BMSsample = await self._async_update()
|
385
408
|
self._add_missing_values(data, self._calc_values())
|
386
409
|
|
387
|
-
if self.
|
410
|
+
if not self._keep_alive:
|
388
411
|
# disconnect after data update to force reconnect next time (slow!)
|
389
412
|
await self.disconnect()
|
390
413
|
|
@@ -478,16 +501,18 @@ class BaseBMS(ABC):
|
|
478
501
|
|
479
502
|
"""
|
480
503
|
return [
|
481
|
-
value
|
504
|
+
(value - offset) / divider
|
482
505
|
for idx in range(values)
|
483
506
|
if (len(data) >= start + (idx + 1) * size)
|
484
507
|
and (
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
508
|
+
(
|
509
|
+
value := int.from_bytes(
|
510
|
+
data[start + idx * size : start + (idx + 1) * size],
|
511
|
+
byteorder=byteorder,
|
512
|
+
signed=signed,
|
513
|
+
)
|
489
514
|
)
|
490
|
-
|
515
|
+
or (offset == 0)
|
491
516
|
)
|
492
517
|
]
|
493
518
|
|
@@ -47,9 +47,9 @@ class BMS(BaseBMS):
|
|
47
47
|
)
|
48
48
|
_RESPS: Final[set[int]] = {field.idx for field in _FIELDS} | {0xF4} # cell voltages
|
49
49
|
|
50
|
-
def __init__(self, ble_device: BLEDevice,
|
50
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
51
51
|
"""Initialize BMS."""
|
52
|
-
super().__init__(ble_device,
|
52
|
+
super().__init__(ble_device, keep_alive)
|
53
53
|
self._data_final: dict[int, bytearray] = {}
|
54
54
|
self._exp_reply: set[int] = set()
|
55
55
|
|
@@ -41,13 +41,14 @@ class BMS(BaseBMS):
|
|
41
41
|
| ((x & 0xF) if (x & 0xF) not in (0x1, 0x4, 0xB, 0xC, 0xF) else 0),
|
42
42
|
),
|
43
43
|
BMSdp("cycle_charge", 54, 4, False, lambda x: x / 1e6),
|
44
|
+
BMSdp("total_charge", 58, 4, False, lambda x: x // 1000),
|
44
45
|
BMSdp("delta_voltage", 82, 2, False, lambda x: x / 1000),
|
45
46
|
BMSdp("power", 62, 4, True, lambda x: x / 1),
|
46
47
|
)
|
47
48
|
|
48
|
-
def __init__(self, ble_device: BLEDevice,
|
49
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
49
50
|
"""Initialize BMS."""
|
50
|
-
super().__init__(ble_device,
|
51
|
+
super().__init__(ble_device, keep_alive)
|
51
52
|
self._data_final: bytearray = bytearray()
|
52
53
|
self._valid_reply: int = BMS._CMD_STAT | 0x10 # valid reply mask
|
53
54
|
self._exp_len: int = BMS._MIN_LEN
|
@@ -87,7 +88,7 @@ class BMS(BaseBMS):
|
|
87
88
|
@staticmethod
|
88
89
|
def _calc_values() -> frozenset[BMSvalue]:
|
89
90
|
return frozenset(
|
90
|
-
{"cycle_capacity", "temperature"}
|
91
|
+
{"cycle_capacity", "cycles", "temperature"}
|
91
92
|
) # calculate further values from BMS provided set ones
|
92
93
|
|
93
94
|
async def _init_connection(
|
@@ -0,0 +1,177 @@
|
|
1
|
+
"""Module to support ANT BMS."""
|
2
|
+
|
3
|
+
import contextlib
|
4
|
+
from enum import IntEnum
|
5
|
+
from typing import Final, override
|
6
|
+
|
7
|
+
from bleak.backends.characteristic import BleakGATTCharacteristic
|
8
|
+
from bleak.backends.device import BLEDevice
|
9
|
+
from bleak.uuids import normalize_uuid_str
|
10
|
+
|
11
|
+
from aiobmsble import BMSdp, BMSsample, BMSvalue, MatcherPattern
|
12
|
+
from aiobmsble.basebms import BaseBMS, crc_sum
|
13
|
+
|
14
|
+
|
15
|
+
class BMS(BaseBMS):
|
16
|
+
"""ANT BMS (legacy) implementation."""
|
17
|
+
|
18
|
+
class CMD(IntEnum):
|
19
|
+
"""Command codes for ANT BMS."""
|
20
|
+
|
21
|
+
GET = 0xDB
|
22
|
+
SET = 0xA5
|
23
|
+
|
24
|
+
class ADR(IntEnum):
|
25
|
+
"""Address codes for ANT BMS."""
|
26
|
+
|
27
|
+
STATUS = 0x00
|
28
|
+
|
29
|
+
_RX_HEADER: Final[bytes] = b"\xaa\x55\xaa"
|
30
|
+
_RX_HEADER_RSP_STAT: Final[bytes] = b"\xaa\x55\xaa\xff"
|
31
|
+
|
32
|
+
_RSP_STAT: Final[int] = 0xFF
|
33
|
+
_RSP_STAT_LEN: Final[int] = 140
|
34
|
+
|
35
|
+
_FIELDS: Final[tuple[BMSdp, ...]] = (
|
36
|
+
BMSdp("voltage", 4, 2, False, lambda x: x / 10),
|
37
|
+
BMSdp("current", 70, 4, True, lambda x: x / -10),
|
38
|
+
BMSdp("battery_level", 74, 1, False),
|
39
|
+
BMSdp("design_capacity", 75, 4, False, lambda x: x // 1e6),
|
40
|
+
BMSdp("cycle_charge", 79, 4, False, lambda x: x / 1e6),
|
41
|
+
BMSdp("total_charge", 83, 4, False, lambda x: x // 1000),
|
42
|
+
BMSdp("runtime", 87, 4, False),
|
43
|
+
BMSdp("cell_count", 123, 1, False),
|
44
|
+
)
|
45
|
+
|
46
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
47
|
+
"""Initialize BMS."""
|
48
|
+
super().__init__(ble_device, keep_alive)
|
49
|
+
self._data_final: bytearray
|
50
|
+
|
51
|
+
@staticmethod
|
52
|
+
@override
|
53
|
+
def matcher_dict_list() -> list[MatcherPattern]:
|
54
|
+
"""Provide BluetoothMatcher definition."""
|
55
|
+
return [
|
56
|
+
{
|
57
|
+
"local_name": "ANT-BLE*",
|
58
|
+
"service_uuid": BMS.uuid_services()[0],
|
59
|
+
"manufacturer_id": 1623,
|
60
|
+
"connectable": True,
|
61
|
+
}
|
62
|
+
]
|
63
|
+
|
64
|
+
@staticmethod
|
65
|
+
@override
|
66
|
+
def device_info() -> dict[str, str]:
|
67
|
+
"""Return device information for the battery management system."""
|
68
|
+
return {"manufacturer": "ANT", "model": "Smart BMS"}
|
69
|
+
|
70
|
+
@staticmethod
|
71
|
+
@override
|
72
|
+
def uuid_services() -> list[str]:
|
73
|
+
"""Return list of 128-bit UUIDs of services required by BMS."""
|
74
|
+
return [normalize_uuid_str("ffe0")] # change service UUID here!
|
75
|
+
|
76
|
+
@staticmethod
|
77
|
+
@override
|
78
|
+
def uuid_rx() -> str:
|
79
|
+
"""Return 16-bit UUID of characteristic that provides notification/read property."""
|
80
|
+
return "ffe1"
|
81
|
+
|
82
|
+
@staticmethod
|
83
|
+
@override
|
84
|
+
def uuid_tx() -> str:
|
85
|
+
"""Return 16-bit UUID of characteristic that provides write property."""
|
86
|
+
return "ffe1"
|
87
|
+
|
88
|
+
@staticmethod
|
89
|
+
@override
|
90
|
+
def _calc_values() -> frozenset[BMSvalue]:
|
91
|
+
return frozenset(
|
92
|
+
(
|
93
|
+
"battery_charging",
|
94
|
+
"cycle_capacity",
|
95
|
+
"cycles",
|
96
|
+
"delta_voltage",
|
97
|
+
"power",
|
98
|
+
"temperature",
|
99
|
+
)
|
100
|
+
) # calculate further values from BMS provided set ones
|
101
|
+
|
102
|
+
def _notification_handler(
|
103
|
+
self, _sender: BleakGATTCharacteristic, data: bytearray
|
104
|
+
) -> None:
|
105
|
+
"""Handle the RX characteristics notify event (new data arrives)."""
|
106
|
+
|
107
|
+
self._log.debug("RX BLE data: %s", data)
|
108
|
+
|
109
|
+
if data.startswith(BMS._RX_HEADER_RSP_STAT):
|
110
|
+
self._data = bytearray()
|
111
|
+
elif not self._data:
|
112
|
+
self._log.debug("invalid start of frame")
|
113
|
+
return
|
114
|
+
|
115
|
+
self._data += data
|
116
|
+
|
117
|
+
_data_len: Final[int] = len(self._data)
|
118
|
+
if _data_len < BMS._RSP_STAT_LEN:
|
119
|
+
return
|
120
|
+
|
121
|
+
if _data_len > BMS._RSP_STAT_LEN:
|
122
|
+
self._log.debug("invalid length %d > %d", _data_len, BMS._RSP_STAT_LEN)
|
123
|
+
self._data.clear()
|
124
|
+
return
|
125
|
+
|
126
|
+
if (local_crc := crc_sum(self._data[4:-2], 2)) != (
|
127
|
+
remote_crc := int.from_bytes(self._data[-2:], byteorder="big", signed=False)
|
128
|
+
):
|
129
|
+
self._log.debug("invalid checksum 0x%X != 0x%X", local_crc, remote_crc)
|
130
|
+
self._data.clear()
|
131
|
+
return
|
132
|
+
|
133
|
+
self._data_final = self._data.copy()
|
134
|
+
self._data.clear()
|
135
|
+
self._data_event.set()
|
136
|
+
|
137
|
+
@staticmethod
|
138
|
+
def _cmd(cmd: CMD, adr: ADR, value: int = 0x0000) -> bytes:
|
139
|
+
"""Assemble a ANT BMS command."""
|
140
|
+
_frame = bytearray((cmd, cmd, adr))
|
141
|
+
_frame += value.to_bytes(2, "big")
|
142
|
+
_frame += crc_sum(_frame[2:], 1).to_bytes(1, "big")
|
143
|
+
return bytes(_frame)
|
144
|
+
|
145
|
+
@override
|
146
|
+
async def _async_update(self) -> BMSsample:
|
147
|
+
"""Update battery status information."""
|
148
|
+
await self._await_reply(BMS._cmd(BMS.CMD.GET, BMS.ADR.STATUS))
|
149
|
+
|
150
|
+
_data: bytearray = self._data_final
|
151
|
+
result: BMSsample = BMS._decode_data(
|
152
|
+
BMS._FIELDS, _data, byteorder="big", offset=0
|
153
|
+
)
|
154
|
+
|
155
|
+
result["cell_voltages"] = BMS._cell_voltages(
|
156
|
+
_data,
|
157
|
+
cells=result["cell_count"],
|
158
|
+
start=6,
|
159
|
+
size=2,
|
160
|
+
byteorder="big",
|
161
|
+
divider=1000,
|
162
|
+
)
|
163
|
+
|
164
|
+
if not result["design_capacity"]:
|
165
|
+
# Workaround for some BMS always reporting 0 for design_capacity
|
166
|
+
result.pop("design_capacity")
|
167
|
+
with contextlib.suppress(ZeroDivisionError):
|
168
|
+
result["design_capacity"] = int(
|
169
|
+
round((result["cycle_charge"] / result["battery_level"]) * 100, -1)
|
170
|
+
) # leads to `cycles` not available when level == 0
|
171
|
+
|
172
|
+
# ANT-BMS carries 6 slots for temp sensors but only 4 looks like being connected by default
|
173
|
+
result["temp_values"] = BMS._temp_values(
|
174
|
+
_data, values=4, start=91, size=2, byteorder="big", signed=True
|
175
|
+
)
|
176
|
+
|
177
|
+
return result
|
@@ -38,9 +38,9 @@ class BMS(BaseBMS):
|
|
38
38
|
0xF5, # BMS boot version
|
39
39
|
}
|
40
40
|
|
41
|
-
def __init__(self, ble_device: BLEDevice,
|
41
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
42
42
|
"""Intialize private BMS members."""
|
43
|
-
super().__init__(ble_device,
|
43
|
+
super().__init__(ble_device, keep_alive)
|
44
44
|
self._data_final: dict[int, bytearray] = {}
|
45
45
|
self._exp_reply: tuple[int] = (0x01,)
|
46
46
|
|
@@ -37,9 +37,9 @@ class BMS(BaseBMS):
|
|
37
37
|
)
|
38
38
|
_CMDS: Final[list[int]] = list({field.idx for field in _FIELDS})
|
39
39
|
|
40
|
-
def __init__(self, ble_device: BLEDevice,
|
40
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
41
41
|
"""Intialize private BMS members."""
|
42
|
-
super().__init__(ble_device,
|
42
|
+
super().__init__(ble_device, keep_alive)
|
43
43
|
|
44
44
|
@staticmethod
|
45
45
|
def matcher_dict_list() -> list[MatcherPattern]:
|
@@ -35,9 +35,9 @@ class BMS(BaseBMS):
|
|
35
35
|
BMSdp("problem_code", 15, 6, False, lambda x: x & 0xFFF000FF000F),
|
36
36
|
)
|
37
37
|
|
38
|
-
def __init__(self, ble_device: BLEDevice,
|
38
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
39
39
|
"""Initialize BMS."""
|
40
|
-
super().__init__(ble_device,
|
40
|
+
super().__init__(ble_device, keep_alive)
|
41
41
|
self._exp_len: int = 0
|
42
42
|
|
43
43
|
@staticmethod
|
@@ -39,9 +39,9 @@ class BMS(BaseBMS):
|
|
39
39
|
BMSdp("problem_code", 116, 8, False, lambda x: x % 2**64),
|
40
40
|
)
|
41
41
|
|
42
|
-
def __init__(self, ble_device: BLEDevice,
|
42
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
43
43
|
"""Intialize private BMS members."""
|
44
|
-
super().__init__(ble_device,
|
44
|
+
super().__init__(ble_device, keep_alive)
|
45
45
|
|
46
46
|
@staticmethod
|
47
47
|
def matcher_dict_list() -> list[MatcherPattern]:
|
@@ -55,9 +55,9 @@ class BMS(BaseBMS):
|
|
55
55
|
)
|
56
56
|
_CMDS: Final[set[Cmd]] = {Cmd(field.idx) for field in _FIELDS}
|
57
57
|
|
58
|
-
def __init__(self, ble_device: BLEDevice,
|
58
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
59
59
|
"""Intialize private BMS members."""
|
60
|
-
super().__init__(ble_device,
|
60
|
+
super().__init__(ble_device, keep_alive)
|
61
61
|
assert self._ble_device.name is not None # required for unlock
|
62
62
|
self._data_final: dict[int, bytearray] = {}
|
63
63
|
|
@@ -19,9 +19,9 @@ class BMS(BaseBMS):
|
|
19
19
|
# _TAIL: Final[bytes] = b"\xAA" # end of frame
|
20
20
|
# _FRAME_LEN: Final[int] = 10 # length of frame, including SOF and checksum
|
21
21
|
|
22
|
-
def __init__(self, ble_device: BLEDevice,
|
22
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
23
23
|
"""Initialize BMS."""
|
24
|
-
super().__init__(ble_device,
|
24
|
+
super().__init__(ble_device, keep_alive)
|
25
25
|
|
26
26
|
@staticmethod
|
27
27
|
def matcher_dict_list() -> list[MatcherPattern]:
|
@@ -84,7 +84,7 @@ class BMS(BaseBMS):
|
|
84
84
|
self._log.debug("replace with command to UUID %s", BMS.uuid_tx())
|
85
85
|
# await self._await_reply(b"<some_command>")
|
86
86
|
|
87
|
-
#
|
87
|
+
# TODO: parse data from self._data here
|
88
88
|
|
89
89
|
return {
|
90
90
|
"voltage": 12,
|
@@ -42,9 +42,9 @@ class BMS(BaseBMS):
|
|
42
42
|
|
43
43
|
_CMDS: Final[set[int]] = set({field.idx for field in _FIELDS_V1})
|
44
44
|
|
45
|
-
def __init__(self, ble_device: BLEDevice,
|
45
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
46
46
|
"""Initialize BMS."""
|
47
|
-
super().__init__(ble_device,
|
47
|
+
super().__init__(ble_device, keep_alive)
|
48
48
|
self._mac_head: Final[tuple] = tuple(
|
49
49
|
int(self._ble_device.address.replace(":", ""), 16).to_bytes(6) + head
|
50
50
|
for head in BMS._HEAD
|
@@ -33,9 +33,9 @@ class BMS(BaseBMS):
|
|
33
33
|
BMSdp("problem_code", 37, 2, False, lambda x: x),
|
34
34
|
)
|
35
35
|
|
36
|
-
def __init__(self, ble_device: BLEDevice,
|
36
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
37
37
|
"""Initialize BMS."""
|
38
|
-
super().__init__(ble_device,
|
38
|
+
super().__init__(ble_device, keep_alive)
|
39
39
|
self._data_final: bytearray = bytearray()
|
40
40
|
|
41
41
|
@staticmethod
|
@@ -45,9 +45,9 @@ class BMS(BaseBMS):
|
|
45
45
|
), # mask status bits
|
46
46
|
)
|
47
47
|
|
48
|
-
def __init__(self, ble_device: BLEDevice,
|
48
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
49
49
|
"""Initialize BMS."""
|
50
|
-
super().__init__(ble_device,
|
50
|
+
super().__init__(ble_device, keep_alive)
|
51
51
|
self._data_final: bytearray = bytearray()
|
52
52
|
|
53
53
|
@staticmethod
|
@@ -65,7 +65,6 @@ class BMS(BaseBMS):
|
|
65
65
|
"connectable": True,
|
66
66
|
},
|
67
67
|
{"local_name": "SV12V*", "manufacturer_id": 33384, "connectable": True},
|
68
|
-
{"local_name": "LT-24*", "manufacturer_id": 22618, "connectable": True},
|
69
68
|
]
|
70
69
|
+ [ # LiTime
|
71
70
|
MatcherPattern( # LiTime based on ser#
|
@@ -75,6 +74,13 @@ class BMS(BaseBMS):
|
|
75
74
|
)
|
76
75
|
for m_id in (33384, 22618)
|
77
76
|
]
|
77
|
+
+ [ # LiTime based on ser#
|
78
|
+
{
|
79
|
+
"local_name": "LT-24???B-A00[0-2]*",
|
80
|
+
"manufacturer_id": 22618,
|
81
|
+
"connectable": True,
|
82
|
+
}
|
83
|
+
]
|
78
84
|
)
|
79
85
|
|
80
86
|
@staticmethod
|
@@ -36,9 +36,9 @@ class BMS(BaseBMS):
|
|
36
36
|
("battery_level", "BatsocList", lambda x: x[0][0] / 100),
|
37
37
|
]
|
38
38
|
|
39
|
-
def __init__(self, ble_device: BLEDevice,
|
39
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
40
40
|
"""Initialize BMS."""
|
41
|
-
super().__init__(ble_device,
|
41
|
+
super().__init__(ble_device, keep_alive)
|
42
42
|
self._data_final: dict = {}
|
43
43
|
|
44
44
|
@staticmethod
|
@@ -32,9 +32,9 @@ class BMS(BaseBMS):
|
|
32
32
|
BMSdp("problem_code", 20, 2, False, lambda x: x),
|
33
33
|
) # general protocol v4
|
34
34
|
|
35
|
-
def __init__(self, ble_device: BLEDevice,
|
35
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
36
36
|
"""Intialize private BMS members."""
|
37
|
-
super().__init__(ble_device,
|
37
|
+
super().__init__(ble_device, keep_alive)
|
38
38
|
self._valid_reply: int = 0x00
|
39
39
|
self._data_final: bytearray = bytearray()
|
40
40
|
|
@@ -35,9 +35,9 @@ class BMS(BaseBMS):
|
|
35
35
|
BMSdp("problem_code", 166, 4, False, lambda x: x),
|
36
36
|
)
|
37
37
|
|
38
|
-
def __init__(self, ble_device: BLEDevice,
|
38
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
39
39
|
"""Intialize private BMS members."""
|
40
|
-
super().__init__(ble_device,
|
40
|
+
super().__init__(ble_device, keep_alive)
|
41
41
|
self._data_final: bytearray = bytearray()
|
42
42
|
self._char_write_handle: int = -1
|
43
43
|
self._bms_info: dict[str, str] = {}
|
@@ -34,9 +34,9 @@ class BMS(BaseBMS):
|
|
34
34
|
("balance_current", 217, "<f", lambda x: round(x, 3)),
|
35
35
|
]
|
36
36
|
|
37
|
-
def __init__(self, ble_device: BLEDevice,
|
37
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
38
38
|
"""Intialize private BMS members."""
|
39
|
-
super().__init__(ble_device,
|
39
|
+
super().__init__(ble_device, keep_alive)
|
40
40
|
self._data_final: bytearray = bytearray()
|
41
41
|
self._bms_info: dict[str, str] = {}
|
42
42
|
self._exp_len: int = BMS._MIN_FRAME
|
@@ -30,9 +30,9 @@ class BMS(BaseBMS):
|
|
30
30
|
reg: int
|
31
31
|
value: int
|
32
32
|
|
33
|
-
def __init__(self, ble_device: BLEDevice,
|
33
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
34
34
|
"""Intialize private BMS members."""
|
35
|
-
super().__init__(ble_device,
|
35
|
+
super().__init__(ble_device, keep_alive)
|
36
36
|
self._type: str = (
|
37
37
|
self.name[9]
|
38
38
|
if len(self.name) >= 10 and set(self.name[10:]).issubset(digits)
|
@@ -52,9 +52,9 @@ class BMS(BaseBMS):
|
|
52
52
|
BMSdp("power", 32, 4, False, lambda x: x / 100),
|
53
53
|
)
|
54
54
|
|
55
|
-
def __init__(self, ble_device: BLEDevice,
|
55
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
56
56
|
"""Initialize private BMS members."""
|
57
|
-
super().__init__(ble_device,
|
57
|
+
super().__init__(ble_device, keep_alive)
|
58
58
|
self._valid_reply: int = BMS._RT_DATA
|
59
59
|
|
60
60
|
@staticmethod
|
@@ -29,9 +29,9 @@ class BMS(BaseBMS):
|
|
29
29
|
BMSdp("problem_code", 76, 4, False, lambda x: x),
|
30
30
|
)
|
31
31
|
|
32
|
-
def __init__(self, ble_device: BLEDevice,
|
32
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
33
33
|
"""Initialize BMS."""
|
34
|
-
super().__init__(ble_device,
|
34
|
+
super().__init__(ble_device, keep_alive)
|
35
35
|
|
36
36
|
@staticmethod
|
37
37
|
def matcher_dict_list() -> list[MatcherPattern]:
|
@@ -56,6 +56,9 @@ class BMS(BaseBMS):
|
|
56
56
|
"L-24*",
|
57
57
|
"L-51*",
|
58
58
|
"LT-12???BG-A0[7-9]*", # LiTime based on ser#
|
59
|
+
"LT-24???B-A00[3-9]*",
|
60
|
+
"LT-24???B-A0[1-9]*",
|
61
|
+
"LT-24???B-A[1-9]*",
|
59
62
|
"LT-51*",
|
60
63
|
)
|
61
64
|
]
|
@@ -28,9 +28,9 @@ class BMS(BaseBMS):
|
|
28
28
|
BMSdp("cycles", 15, 2, False, lambda x: x),
|
29
29
|
)
|
30
30
|
|
31
|
-
def __init__(self, ble_device: BLEDevice,
|
31
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
32
32
|
"""Initialize BMS."""
|
33
|
-
super().__init__(ble_device,
|
33
|
+
super().__init__(ble_device, keep_alive)
|
34
34
|
|
35
35
|
@staticmethod
|
36
36
|
def matcher_dict_list() -> list[MatcherPattern]:
|
@@ -24,9 +24,9 @@ class BMS(RenogyBMS):
|
|
24
24
|
BMSdp("cycles", 15, 2, False, lambda x: x),
|
25
25
|
)
|
26
26
|
|
27
|
-
def __init__(self, ble_device: BLEDevice,
|
27
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
28
28
|
"""Intialize private BMS members."""
|
29
|
-
super().__init__(ble_device,
|
29
|
+
super().__init__(ble_device, keep_alive)
|
30
30
|
self._char_write_handle: int = -1
|
31
31
|
|
32
32
|
@staticmethod
|
@@ -47,9 +47,9 @@ class BMS(BaseBMS):
|
|
47
47
|
)
|
48
48
|
_CMDS: Final[set[int]] = set({field.idx for field in _FIELDS})
|
49
49
|
|
50
|
-
def __init__(self, ble_device: BLEDevice,
|
50
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
51
51
|
"""Initialize BMS."""
|
52
|
-
super().__init__(ble_device,
|
52
|
+
super().__init__(ble_device, keep_alive)
|
53
53
|
self._data_final: dict[int, bytearray] = {}
|
54
54
|
self._exp_len: int = 0
|
55
55
|
|
@@ -57,9 +57,9 @@ class BMS(BaseBMS):
|
|
57
57
|
field[2] for field in PQUERY.values()
|
58
58
|
}
|
59
59
|
|
60
|
-
def __init__(self, ble_device: BLEDevice,
|
60
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
61
61
|
"""Intialize private BMS members."""
|
62
|
-
super().__init__(ble_device,
|
62
|
+
super().__init__(ble_device, keep_alive)
|
63
63
|
self._data_final: dict[int, bytearray] = {}
|
64
64
|
self._pack_count: int = 0 # number of battery packs
|
65
65
|
self._pkglen: int = 0 # expected packet length
|
@@ -73,7 +73,7 @@ class BMS(BaseBMS):
|
|
73
73
|
"service_uuid": BMS.uuid_services()[0],
|
74
74
|
"connectable": True,
|
75
75
|
}
|
76
|
-
for pattern in {f"SP{num}?B*" for num in range(10)} | {"CSY*"}
|
76
|
+
for pattern in {f"SP{num}?B*" for num in range(10)} | {"CSY*"} | {"SP1??B*"}
|
77
77
|
]
|
78
78
|
|
79
79
|
@staticmethod
|
@@ -36,9 +36,9 @@ class BMS(BaseBMS):
|
|
36
36
|
_GSMD_LEN: Final[int] = _CELL_POS + max((dp.pos + dp.size) for dp in _PFIELDS) + 3
|
37
37
|
_CMDS: Final[list[tuple[int, bytes]]] = [(0x51, b""), (0x61, b"\x00"), (0x62, b"")]
|
38
38
|
|
39
|
-
def __init__(self, ble_device: BLEDevice,
|
39
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
40
40
|
"""Initialize BMS."""
|
41
|
-
super().__init__(ble_device,
|
41
|
+
super().__init__(ble_device, keep_alive)
|
42
42
|
self._data_final: dict[int, bytearray] = {}
|
43
43
|
self._exp_len: int = BMS._MIN_LEN
|
44
44
|
self._exp_reply: set[int] = set()
|
@@ -41,9 +41,9 @@ class BMS(BaseBMS):
|
|
41
41
|
) # problem code is not included in the list, but extra
|
42
42
|
_CMDS: Final[list[int]] = [*list({field.idx for field in _FIELDS}), 0x8D]
|
43
43
|
|
44
|
-
def __init__(self, ble_device: BLEDevice,
|
44
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
45
45
|
"""Initialize BMS."""
|
46
|
-
super().__init__(ble_device,
|
46
|
+
super().__init__(ble_device, keep_alive)
|
47
47
|
self._data_final: dict[int, bytearray] = {}
|
48
48
|
self._cmd_heads: list[int] = BMS._CMD_HEADS
|
49
49
|
self._exp_len: int = 0
|
@@ -37,9 +37,9 @@ class BMS(BaseBMS):
|
|
37
37
|
)
|
38
38
|
_CMDS: Final[set[int]] = set({field.idx for field in _FIELDS}) | set({0x87})
|
39
39
|
|
40
|
-
def __init__(self, ble_device: BLEDevice,
|
40
|
+
def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
|
41
41
|
"""Initialize BMS."""
|
42
|
-
super().__init__(ble_device,
|
42
|
+
super().__init__(ble_device, keep_alive)
|
43
43
|
self._data_final: dict[int, bytearray] = {}
|
44
44
|
|
45
45
|
@staticmethod
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: aiobmsble
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
4
|
Summary: Asynchronous Python library to query battery management systems via Bluetooth Low Energy.
|
5
5
|
Author: Patrick Loschmidt
|
6
6
|
Maintainer: Patrick Loschmidt
|
@@ -18,12 +18,8 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
18
|
Requires-Python: >=3.12
|
19
19
|
Description-Content-Type: text/markdown
|
20
20
|
License-File: LICENSE
|
21
|
-
Requires-Dist: bleak
|
21
|
+
Requires-Dist: bleak>=1.0.1
|
22
22
|
Requires-Dist: bleak-retry-connector>=4.0.2
|
23
|
-
Requires-Dist: asyncio
|
24
|
-
Requires-Dist: logging
|
25
|
-
Requires-Dist: statistics
|
26
|
-
Requires-Dist: typing
|
27
23
|
Provides-Extra: dev
|
28
24
|
Requires-Dist: pytest; extra == "dev"
|
29
25
|
Requires-Dist: pytest-asyncio; extra == "dev"
|
@@ -31,7 +27,7 @@ Requires-Dist: pytest-cov; extra == "dev"
|
|
31
27
|
Requires-Dist: pytest-xdist; extra == "dev"
|
32
28
|
Requires-Dist: hypothesis; extra == "dev"
|
33
29
|
Requires-Dist: mypy; extra == "dev"
|
34
|
-
Requires-Dist: ruff; extra == "dev"
|
30
|
+
Requires-Dist: ruff~=0.12.1; extra == "dev"
|
35
31
|
Dynamic: license-file
|
36
32
|
|
37
33
|
[![License][license-shield]](LICENSE)
|
@@ -55,7 +51,11 @@ from the command line after [installation](#installation). In case you need the
|
|
55
51
|
### From a Script
|
56
52
|
This example can also be found as an [example](/examples/minimal.py) in the respective [folder](/main/examples).
|
57
53
|
```python
|
58
|
-
"""Example of using the aiobmsble library to find a BLE device by name and print its
|
54
|
+
"""Example of using the aiobmsble library to find a BLE device by name and print its sensor data.
|
55
|
+
|
56
|
+
Project: aiobmsble, https://pypi.org/p/aiobmsble/
|
57
|
+
License: Apache-2.0, http://www.apache.org/licenses/
|
58
|
+
"""
|
59
59
|
|
60
60
|
import asyncio
|
61
61
|
import logging
|
@@ -66,9 +66,9 @@ from bleak.backends.device import BLEDevice
|
|
66
66
|
from bleak.exc import BleakError
|
67
67
|
|
68
68
|
from aiobmsble import BMSsample
|
69
|
-
from aiobmsble.bms.dummy_bms import BMS # use the right BMS class for your device
|
69
|
+
from aiobmsble.bms.dummy_bms import BMS # TODO: use the right BMS class for your device
|
70
70
|
|
71
|
-
NAME: Final[str] = "BT Device Name" #
|
71
|
+
NAME: Final[str] = "BT Device Name" # TODO: replace with the name of your BLE device
|
72
72
|
|
73
73
|
# Configure logging
|
74
74
|
logging.basicConfig(level=logging.INFO)
|
@@ -84,11 +84,11 @@ async def main(dev_name) -> None:
|
|
84
84
|
return
|
85
85
|
|
86
86
|
logger.info("Found device: %s (%s)", device.name, device.address)
|
87
|
-
bms = BMS(ble_device=device, reconnect=True)
|
88
87
|
try:
|
89
|
-
|
90
|
-
|
91
|
-
|
88
|
+
async with BMS(ble_device=device) as bms:
|
89
|
+
logger.info("Updating BMS data...")
|
90
|
+
data: BMSsample = await bms.async_update()
|
91
|
+
logger.info("BMS data: %s", repr(data).replace(", ", ",\n\t"))
|
92
92
|
except BleakError as ex:
|
93
93
|
logger.error("Failed to update BMS: %s", type(ex).__name__)
|
94
94
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "aiobmsble"
|
7
|
-
version = "0.
|
7
|
+
version = "0.3.0"
|
8
8
|
requires-python = ">=3.12"
|
9
9
|
description = "Asynchronous Python library to query battery management systems via Bluetooth Low Energy."
|
10
10
|
readme = "README.md"
|
@@ -27,16 +27,12 @@ classifiers = [
|
|
27
27
|
"Topic :: Software Development :: Libraries :: Python Modules"
|
28
28
|
]
|
29
29
|
dependencies = [
|
30
|
-
"bleak
|
30
|
+
"bleak>=1.0.1",
|
31
31
|
"bleak-retry-connector>=4.0.2",
|
32
|
-
"asyncio",
|
33
|
-
"logging",
|
34
|
-
"statistics",
|
35
|
-
"typing"
|
36
32
|
]
|
37
33
|
|
38
34
|
[project.optional-dependencies]
|
39
|
-
dev = ["pytest", "pytest-asyncio", "pytest-cov", "pytest-xdist", "hypothesis", "mypy", "ruff"]
|
35
|
+
dev = ["pytest", "pytest-asyncio", "pytest-cov", "pytest-xdist", "hypothesis", "mypy", "ruff~=0.12.1"]
|
40
36
|
|
41
37
|
[project.urls]
|
42
38
|
Homepage = "https://github.com/patman15/aiobmsble/"
|
@@ -181,7 +177,7 @@ ignore = [
|
|
181
177
|
"PLR0913", # Too many arguments to function call ({c_args} > {max_args})
|
182
178
|
# "PLR0915", # Too many statements ({statements} > {max_statements})
|
183
179
|
"PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable
|
184
|
-
"PLW1641", # __eq__ without __hash__
|
180
|
+
# "PLW1641", # __eq__ without __hash__
|
185
181
|
# "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target
|
186
182
|
# "PT011", # pytest.raises({exception}) is too broad, set the `match` parameter or use a more specific exception
|
187
183
|
"PT018", # Assertion should be broken down into multiple parts
|
@@ -112,10 +112,10 @@ class WMTestBMS(MinTestBMS):
|
|
112
112
|
self,
|
113
113
|
char_tx_properties: list[str],
|
114
114
|
ble_device: BLEDevice,
|
115
|
-
|
115
|
+
keep_alive: bool = True,
|
116
116
|
) -> None:
|
117
117
|
"""Initialize BMS."""
|
118
|
-
super().__init__(ble_device,
|
118
|
+
super().__init__(ble_device, keep_alive)
|
119
119
|
self._char_tx_properties: list[str] = char_tx_properties
|
120
120
|
|
121
121
|
def _wr_response(self, char: int | str) -> bool:
|
@@ -198,6 +198,14 @@ def test_calc_battery_level() -> None:
|
|
198
198
|
assert bms_data == ref | {"battery_level": 42.8, "problem": False}
|
199
199
|
|
200
200
|
|
201
|
+
def test_calc_cycles() -> None:
|
202
|
+
"""Check if missing cycle is correctly calculated."""
|
203
|
+
bms_data: BMSsample = {"total_charge": 1234567, "design_capacity": 256}
|
204
|
+
ref: BMSsample = bms_data.copy()
|
205
|
+
BaseBMS._add_missing_values(bms_data, frozenset({"cycles"}))
|
206
|
+
assert bms_data == ref | {"cycles": 4822, "problem": False}
|
207
|
+
|
208
|
+
|
201
209
|
@pytest.mark.parametrize(
|
202
210
|
("problem_sample"),
|
203
211
|
[
|
@@ -328,7 +336,7 @@ async def test_no_notify(
|
|
328
336
|
"""Test BMS update without waiting for notification event."""
|
329
337
|
patch_bleak_client(MockBleakClient)
|
330
338
|
|
331
|
-
bms: MinTestBMS = MinTestBMS(generate_ble_device(),
|
339
|
+
bms: MinTestBMS = MinTestBMS(generate_ble_device(), keep_alive=False)
|
332
340
|
with caplog.at_level(DEBUG):
|
333
341
|
result: BMSsample = await bms.async_update()
|
334
342
|
assert "MockBleakClient write_gatt_char afe2, data: b'mock_command'" in caplog.text
|
@@ -349,7 +357,7 @@ async def test_disconnect_fail(
|
|
349
357
|
monkeypatch.setattr(MockBleakClient, "disconnect", _raise_bleak_error)
|
350
358
|
patch_bleak_client(MockBleakClient)
|
351
359
|
|
352
|
-
bms: MinTestBMS = MinTestBMS(generate_ble_device(),
|
360
|
+
bms: MinTestBMS = MinTestBMS(generate_ble_device(), keep_alive=False)
|
353
361
|
with caplog.at_level(DEBUG):
|
354
362
|
result: BMSsample = await bms.async_update()
|
355
363
|
assert result == {"problem_code": 21}
|
@@ -375,6 +383,18 @@ async def test_init_connect_fail(
|
|
375
383
|
await bms.async_update()
|
376
384
|
|
377
385
|
|
386
|
+
async def test_context_mgr_fail(
|
387
|
+
patch_bleak_client: Callable[..., None],
|
388
|
+
) -> None:
|
389
|
+
"""Check that context manager enforces `keep_alive=True`."""
|
390
|
+
|
391
|
+
patch_bleak_client(MockBleakClient)
|
392
|
+
|
393
|
+
with pytest.raises(ValueError, match="usage of context manager*"):
|
394
|
+
async with MinTestBMS(generate_ble_device(), keep_alive=False) as bms:
|
395
|
+
await bms.async_update()
|
396
|
+
|
397
|
+
|
378
398
|
def test_crc_calculations() -> None:
|
379
399
|
"""Check if CRC calculations are correct."""
|
380
400
|
# Example data for CRC calculation
|
@@ -487,6 +507,32 @@ def test_cell_voltages(data, cells, start, size, byteorder, divider, expected) -
|
|
487
507
|
(bytearray([0x00, 0x7D]), 0, 0, 2, "big", True, 0, 1, []),
|
488
508
|
# Divider = 1, offset = 7
|
489
509
|
(bytearray([0x00, 0x14]), 1, 0, 2, "big", True, 7, 1, [13]),
|
510
|
+
# no offset, div = 10
|
511
|
+
(
|
512
|
+
bytearray(b"\x65\x64\x00\x40"),
|
513
|
+
4,
|
514
|
+
0,
|
515
|
+
1,
|
516
|
+
"big",
|
517
|
+
True,
|
518
|
+
0,
|
519
|
+
10,
|
520
|
+
[10.1, 10.0, 0.0, 6.4],
|
521
|
+
),
|
522
|
+
# offset -40, div = 10
|
523
|
+
(
|
524
|
+
bytearray(b"\x65\x64\x00\x40"),
|
525
|
+
4,
|
526
|
+
0,
|
527
|
+
1,
|
528
|
+
"big",
|
529
|
+
True,
|
530
|
+
40,
|
531
|
+
10,
|
532
|
+
[6.1, 6.0, 2.4],
|
533
|
+
),
|
534
|
+
# offset -25, div = 1
|
535
|
+
(bytearray(b"\x65\x64\x00\x40"), 4, 0, 1, "big", True, 25, 1, [76, 75, 39]),
|
490
536
|
],
|
491
537
|
ids=[
|
492
538
|
"two_signed_big_endian",
|
@@ -496,6 +542,9 @@ def test_cell_voltages(data, cells, start, size, byteorder, divider, expected) -
|
|
496
542
|
"not_enough_data",
|
497
543
|
"zero_values",
|
498
544
|
"divider1_offset10",
|
545
|
+
"no offset",
|
546
|
+
"offset-40",
|
547
|
+
"div-1",
|
499
548
|
],
|
500
549
|
)
|
501
550
|
def test_temp_values(
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|