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.
Files changed (49) hide show
  1. {aiobmsble-0.2.3/aiobmsble.egg-info → aiobmsble-0.3.0}/PKG-INFO +14 -14
  2. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/README.md +11 -7
  3. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/__init__.py +2 -0
  4. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/__main__.py +3 -1
  5. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/basebms.py +38 -13
  6. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/abc_bms.py +2 -2
  7. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/ant_bms.py +4 -3
  8. aiobmsble-0.3.0/aiobmsble/bms/ant_leg_bms.py +177 -0
  9. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/braunpwr_bms.py +2 -2
  10. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/cbtpwr_bms.py +2 -2
  11. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/cbtpwr_vb_bms.py +2 -2
  12. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/daly_bms.py +2 -2
  13. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/dpwrcore_bms.py +2 -2
  14. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/dummy_bms.py +3 -3
  15. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/ecoworthy_bms.py +2 -2
  16. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/ective_bms.py +2 -2
  17. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/ej_bms.py +9 -3
  18. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/felicity_bms.py +2 -2
  19. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/jbd_bms.py +2 -2
  20. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/jikong_bms.py +2 -2
  21. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/neey_bms.py +2 -2
  22. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/ogt_bms.py +2 -2
  23. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/pro_bms.py +2 -2
  24. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/redodo_bms.py +5 -2
  25. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/renogy_bms.py +2 -2
  26. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/renogy_pro_bms.py +2 -2
  27. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/roypow_bms.py +2 -2
  28. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/seplos_bms.py +3 -3
  29. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/seplos_v2_bms.py +2 -2
  30. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/tdt_bms.py +2 -2
  31. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/tianpwr_bms.py +2 -2
  32. {aiobmsble-0.2.3 → aiobmsble-0.3.0/aiobmsble.egg-info}/PKG-INFO +14 -14
  33. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble.egg-info/SOURCES.txt +1 -0
  34. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble.egg-info/requires.txt +2 -6
  35. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/pyproject.toml +4 -8
  36. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/tests/test_basebms.py +53 -4
  37. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/LICENSE +0 -0
  38. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/MANIFEST.in +0 -0
  39. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/bms/__init__.py +0 -0
  40. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble/utils.py +0 -0
  41. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble.egg-info/dependency_links.txt +0 -0
  42. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble.egg-info/entry_points.txt +0 -0
  43. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/aiobmsble.egg-info/top_level.txt +0 -0
  44. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/setup.cfg +0 -0
  45. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/tests/test_examples.py +0 -0
  46. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/tests/test_fuzzing.py +0 -0
  47. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/tests/test_main.py +0 -0
  48. {aiobmsble-0.2.3 → aiobmsble-0.3.0}/tests/test_plugins.py +0 -0
  49. {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.2.3
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~=1.1.0
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 senosr data."""
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" # Replace with the name of your BLE device
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
- logger.info("Updating BMS data...")
90
- data: BMSsample = await bms.async_update()
91
- logger.info("BMS data: %s", repr(data).replace(", ", ",\n\t"))
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 senosr data."""
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" # Replace with the name of your BLE device
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
- logger.info("Updating BMS data...")
54
- data: BMSsample = await bms.async_update()
55
- logger.info("BMS data: %s", repr(data).replace(", ", ",\n\t"))
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, reconnect=True)
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 typing import Any, Final, Literal
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
- reconnect: bool = False,
48
+ keep_alive: bool = True,
48
49
  logger_name: str = "",
49
50
  ) -> None:
50
51
  """Intialize the BMS.
51
52
 
52
- notification_handler: the callback function used for notifications from 'uuid_rx()'
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
- reconnect (bool): if true, the connection will be closed after each update
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._reconnect: Final[bool] = reconnect
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._reconnect:
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 / divider if divider != 1 else 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
- value := int.from_bytes(
486
- data[start + idx * size : start + (idx + 1) * size],
487
- byteorder=byteorder,
488
- signed=signed,
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
- - offset
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, reconnect: bool = False) -> None:
50
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
51
51
  """Initialize BMS."""
52
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
49
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
49
50
  """Initialize BMS."""
50
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
41
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
42
42
  """Intialize private BMS members."""
43
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
40
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
41
41
  """Intialize private BMS members."""
42
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
38
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
39
39
  """Initialize BMS."""
40
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
42
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
43
43
  """Intialize private BMS members."""
44
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
58
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
59
59
  """Intialize private BMS members."""
60
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
22
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
23
23
  """Initialize BMS."""
24
- super().__init__(ble_device, reconnect)
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
- # # TODO: parse data from self._data here
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, reconnect: bool = False) -> None:
45
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
46
46
  """Initialize BMS."""
47
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
36
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
37
37
  """Initialize BMS."""
38
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
48
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
49
49
  """Initialize BMS."""
50
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
39
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
40
40
  """Initialize BMS."""
41
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
35
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
36
36
  """Intialize private BMS members."""
37
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
38
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
39
39
  """Intialize private BMS members."""
40
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
37
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
38
38
  """Intialize private BMS members."""
39
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
33
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
34
34
  """Intialize private BMS members."""
35
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
55
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
56
56
  """Initialize private BMS members."""
57
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
32
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
33
33
  """Initialize BMS."""
34
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
31
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
32
32
  """Initialize BMS."""
33
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
27
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
28
28
  """Intialize private BMS members."""
29
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
50
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
51
51
  """Initialize BMS."""
52
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
60
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
61
61
  """Intialize private BMS members."""
62
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
39
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
40
40
  """Initialize BMS."""
41
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
44
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
45
45
  """Initialize BMS."""
46
- super().__init__(ble_device, reconnect)
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, reconnect: bool = False) -> None:
40
+ def __init__(self, ble_device: BLEDevice, keep_alive: bool = True) -> None:
41
41
  """Initialize BMS."""
42
- super().__init__(ble_device, reconnect)
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.2.3
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~=1.1.0
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 senosr data."""
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" # Replace with the name of your BLE device
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
- logger.info("Updating BMS data...")
90
- data: BMSsample = await bms.async_update()
91
- logger.info("BMS data: %s", repr(data).replace(", ", ",\n\t"))
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
 
@@ -15,6 +15,7 @@ aiobmsble.egg-info/top_level.txt
15
15
  aiobmsble/bms/__init__.py
16
16
  aiobmsble/bms/abc_bms.py
17
17
  aiobmsble/bms/ant_bms.py
18
+ aiobmsble/bms/ant_leg_bms.py
18
19
  aiobmsble/bms/braunpwr_bms.py
19
20
  aiobmsble/bms/cbtpwr_bms.py
20
21
  aiobmsble/bms/cbtpwr_vb_bms.py
@@ -1,9 +1,5 @@
1
- bleak~=1.1.0
1
+ bleak>=1.0.1
2
2
  bleak-retry-connector>=4.0.2
3
- asyncio
4
- logging
5
- statistics
6
- typing
7
3
 
8
4
  [dev]
9
5
  pytest
@@ -12,4 +8,4 @@ pytest-cov
12
8
  pytest-xdist
13
9
  hypothesis
14
10
  mypy
15
- ruff
11
+ ruff~=0.12.1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aiobmsble"
7
- version = "0.2.3"
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~=1.1.0",
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
- reconnect: bool = False,
115
+ keep_alive: bool = True,
116
116
  ) -> None:
117
117
  """Initialize BMS."""
118
- super().__init__(ble_device, reconnect)
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(), reconnect=True)
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(), reconnect=True)
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