aiobmsble 0.2.2__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 (51) hide show
  1. {aiobmsble-0.2.2/aiobmsble.egg-info → aiobmsble-0.3.0}/PKG-INFO +15 -15
  2. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/README.md +11 -7
  3. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/__init__.py +2 -0
  4. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/__main__.py +3 -1
  5. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/basebms.py +38 -13
  6. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/abc_bms.py +2 -2
  7. {aiobmsble-0.2.2 → 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.2 → aiobmsble-0.3.0}/aiobmsble/bms/braunpwr_bms.py +2 -2
  10. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/cbtpwr_bms.py +2 -2
  11. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/cbtpwr_vb_bms.py +2 -2
  12. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/daly_bms.py +2 -2
  13. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/dpwrcore_bms.py +2 -2
  14. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/dummy_bms.py +3 -3
  15. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/ecoworthy_bms.py +2 -2
  16. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/ective_bms.py +2 -2
  17. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/ej_bms.py +9 -3
  18. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/felicity_bms.py +2 -2
  19. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/jbd_bms.py +2 -2
  20. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/jikong_bms.py +2 -2
  21. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/neey_bms.py +2 -2
  22. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/ogt_bms.py +2 -2
  23. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/pro_bms.py +2 -2
  24. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/redodo_bms.py +5 -2
  25. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/renogy_bms.py +2 -2
  26. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/renogy_pro_bms.py +2 -2
  27. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/roypow_bms.py +2 -2
  28. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/seplos_bms.py +3 -3
  29. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/seplos_v2_bms.py +2 -2
  30. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/tdt_bms.py +2 -2
  31. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/tianpwr_bms.py +2 -2
  32. {aiobmsble-0.2.2 → aiobmsble-0.3.0/aiobmsble.egg-info}/PKG-INFO +15 -15
  33. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble.egg-info/SOURCES.txt +1 -0
  34. aiobmsble-0.3.0/aiobmsble.egg-info/requires.txt +11 -0
  35. aiobmsble-0.3.0/pyproject.toml +238 -0
  36. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/tests/test_basebms.py +53 -4
  37. aiobmsble-0.2.2/aiobmsble.egg-info/requires.txt +0 -15
  38. aiobmsble-0.2.2/pyproject.toml +0 -238
  39. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/LICENSE +0 -0
  40. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/MANIFEST.in +0 -0
  41. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/bms/__init__.py +0 -0
  42. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble/utils.py +0 -0
  43. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble.egg-info/dependency_links.txt +0 -0
  44. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble.egg-info/entry_points.txt +0 -0
  45. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/aiobmsble.egg-info/top_level.txt +0 -0
  46. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/setup.cfg +0 -0
  47. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/tests/test_examples.py +0 -0
  48. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/tests/test_fuzzing.py +0 -0
  49. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/tests/test_main.py +0 -0
  50. {aiobmsble-0.2.2 → aiobmsble-0.3.0}/tests/test_plugins.py +0 -0
  51. {aiobmsble-0.2.2 → 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.2
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
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
21
+ Requires-Dist: bleak>=1.0.1
22
+ Requires-Dist: bleak-retry-connector>=4.0.2
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] = {}