aiobmsble 0.2.3__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
aiobmsble/__init__.py CHANGED
@@ -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 # [#]
aiobmsble/__main__.py CHANGED
@@ -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
 
aiobmsble/basebms.py CHANGED
@@ -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
 
aiobmsble/bms/abc_bms.py CHANGED
@@ -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
 
aiobmsble/bms/ant_bms.py CHANGED
@@ -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
aiobmsble/bms/daly_bms.py CHANGED
@@ -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
aiobmsble/bms/ej_bms.py CHANGED
@@ -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
aiobmsble/bms/jbd_bms.py CHANGED
@@ -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] = {}
aiobmsble/bms/neey_bms.py CHANGED
@@ -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
aiobmsble/bms/ogt_bms.py CHANGED
@@ -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)
aiobmsble/bms/pro_bms.py CHANGED
@@ -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()
aiobmsble/bms/tdt_bms.py CHANGED
@@ -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
 
@@ -0,0 +1,37 @@
1
+ aiobmsble/__init__.py,sha256=7Qg39LPp9W98ymZX67uOZzbPspJ4JuSUenSxLm12jlo,3119
2
+ aiobmsble/__main__.py,sha256=1r0MZLjx3lxJ6dw8pLha4tdUPvUcgU0hzXa50N3w3ks,3157
3
+ aiobmsble/basebms.py,sha256=p18gHcy7rL_NEiZZEFwF0z5b6nZH9fd1zKGJpcyK5LA,19818
4
+ aiobmsble/utils.py,sha256=ckcOXMLTpm4oCxbGKco88cPVP4nOgiTJ16ebFlvsj_E,5805
5
+ aiobmsble/bms/__init__.py,sha256=ZE4Uezyd5fs3os4_bt6Pnzsfrp38LTXItdvJ9-zBiR0,165
6
+ aiobmsble/bms/abc_bms.py,sha256=ug4AeTHiOcMWB4MVTjKI69mbq1wk-J89BbIE2-LeH-w,5993
7
+ aiobmsble/bms/ant_bms.py,sha256=3kNX0Oy7EDxlb1pynARnttTi-QnzSsFovR5MgrveUOo,7236
8
+ aiobmsble/bms/ant_leg_bms.py,sha256=Cz0mi3P7_TaKDodZjGs0dcBq0Mjdk8YhwSfs--ijyMQ,5711
9
+ aiobmsble/bms/braunpwr_bms.py,sha256=fINnmqN3jxZPcKXPcjGPkptU65xxTtuIrilkWlKaWeI,5977
10
+ aiobmsble/bms/cbtpwr_bms.py,sha256=r2EGxFhGJCQvn8iRa1SJY-7xey64Q6MhaKKTfsgo_T0,6277
11
+ aiobmsble/bms/cbtpwr_vb_bms.py,sha256=hivPpOXCc946RH4Z8kQXEC9GkFUTtxJC2KSaIjAvBd0,6626
12
+ aiobmsble/bms/daly_bms.py,sha256=XVz5_sOBBPhBWqlq6PLOXpwglPa1YCfMqOBOT2cQpss,5850
13
+ aiobmsble/bms/dpwrcore_bms.py,sha256=ATUw4bnXPgm7TvAA2ej7H0GKkDMhWfDUqi9CrLE1mps,6666
14
+ aiobmsble/bms/dummy_bms.py,sha256=gso6-0Bs5veVJfp6XNjZiigVAr6JAb_o9hFu-zlsURw,3503
15
+ aiobmsble/bms/ecoworthy_bms.py,sha256=-Y6TEktbTwcIfCUEfAQkRIdqxPH0B93-Fhhm93oeGKk,5410
16
+ aiobmsble/bms/ective_bms.py,sha256=GGcZgqC8BB7v0xm_st7wGvGNmayxCBYq-rhOrevaMzg,5851
17
+ aiobmsble/bms/ej_bms.py,sha256=8AiS26r2_uapXMLI34d-eECh5wKXOHhD2eRIeKhApLQ,8174
18
+ aiobmsble/bms/felicity_bms.py,sha256=oDXlJzYlZF5TtXVC1VYfMybzti1yEKjzGETop12vGYk,4738
19
+ aiobmsble/bms/jbd_bms.py,sha256=wy1EAjxZZkDps9Y5H7cGTgaV8dYz_3n1iQUxP-ImqA4,7167
20
+ aiobmsble/bms/jikong_bms.py,sha256=DOxYo2FwVZOdGtaxRwL3HzffuX0_z3pYZrsckia18Ik,11153
21
+ aiobmsble/bms/neey_bms.py,sha256=dTdyThI2MMNVfEQ2ArPbY5bDkKMGn6NSfz2G68qYZ6Q,7666
22
+ aiobmsble/bms/ogt_bms.py,sha256=gIEt1h1e0ZXESgmL_VwzH34FiyH0pvIfGwjpc3hgIZ0,7988
23
+ aiobmsble/bms/pro_bms.py,sha256=TyAbjZBYaWOLtZ4rWWVWHag8o6x-roeujWobMlnb14A,5079
24
+ aiobmsble/bms/redodo_bms.py,sha256=p6lvJ9fzJS-CkjaB7dSz3_j9-a5Ls3ul1jGBI0p6Ezk,4484
25
+ aiobmsble/bms/renogy_bms.py,sha256=q_SO0LvW0MLtdNvretnactt4UoYnq5Zje3iDrRAum9k,5080
26
+ aiobmsble/bms/renogy_pro_bms.py,sha256=EsPlnrtdn7F4o-LRIIwTnVLIkvm4yfc5uEpeJlG3bCU,3969
27
+ aiobmsble/bms/roypow_bms.py,sha256=ZNuHrMYxQLIGJuao1-DR7pNE2tH6OVGpMaZZxHl2o9I,6169
28
+ aiobmsble/bms/seplos_bms.py,sha256=R4A77uwjy5XDbbPNwzfqOZxaQfIvGs-Vnd-0Ch0zw-s,9532
29
+ aiobmsble/bms/seplos_v2_bms.py,sha256=un6vvMwWXnFzAUgOnHYg69Jokd9MH-9MjqELOQD6Uok,7494
30
+ aiobmsble/bms/tdt_bms.py,sha256=ksGPwTAcn4WDrjSh8a0OzokDhWqrOcixLU3XfndfPyI,7033
31
+ aiobmsble/bms/tianpwr_bms.py,sha256=2EdaCnY4_PxfRUUrU1hQgwvst5fHDz6yvGS7FU_zYxs,4947
32
+ aiobmsble-0.3.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
33
+ aiobmsble-0.3.0.dist-info/METADATA,sha256=Qba-JrJA3Pl0oXx8agKIsc_jq7BuM-d4jetgvtPpDoE,4755
34
+ aiobmsble-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
+ aiobmsble-0.3.0.dist-info/entry_points.txt,sha256=HSC_C3nQikc3nk0a6mcG92RuIM7wAzozjBVfDojJceo,54
36
+ aiobmsble-0.3.0.dist-info/top_level.txt,sha256=YHBVzg45mJ3vPz0sl_TpMB0edMqqhD61kwJj4EPAk9g,10
37
+ aiobmsble-0.3.0.dist-info/RECORD,,
@@ -1,36 +0,0 @@
1
- aiobmsble/__init__.py,sha256=zIqeiJBneqIUo61GIeqHh0gt9itp19OierNrMLEw25Y,3049
2
- aiobmsble/__main__.py,sha256=swsFTPO4Cz8fsFwfGfjCv0M6EI9g8VDhC-5HaCo-spI,3113
3
- aiobmsble/basebms.py,sha256=pPhzj6pJUFtVnbHR5bauRi4BpuWJwQ9GXFueEDCVIZw,18813
4
- aiobmsble/utils.py,sha256=ckcOXMLTpm4oCxbGKco88cPVP4nOgiTJ16ebFlvsj_E,5805
5
- aiobmsble/bms/__init__.py,sha256=ZE4Uezyd5fs3os4_bt6Pnzsfrp38LTXItdvJ9-zBiR0,165
6
- aiobmsble/bms/abc_bms.py,sha256=wud4DTj5Cbo9CjsQ96Tn12CXOSoozvKTIa9pWGpEo1s,5992
7
- aiobmsble/bms/ant_bms.py,sha256=3YY3Nod6KhylBqYFo2vDgy76MpdYtKbckzDC0SDoEZM,7159
8
- aiobmsble/bms/braunpwr_bms.py,sha256=_Fl9yQQtzmQyveCQNso6ahX0O1PHxrf5LL4Ef7k5GHg,5976
9
- aiobmsble/bms/cbtpwr_bms.py,sha256=p4bS3oyVirUFC2-2nbF2EfCrShx8ynpjXEkLPdC0llA,6276
10
- aiobmsble/bms/cbtpwr_vb_bms.py,sha256=AJhesOKX2yzrsfXQcXufG9E7iX2YJMo-syLKwSMeMLw,6625
11
- aiobmsble/bms/daly_bms.py,sha256=Ql7Ajv06OSp0m_16vcUy9y_W1JeLOPFWChghD0xr59U,5849
12
- aiobmsble/bms/dpwrcore_bms.py,sha256=6o4cKtEs8_Fic_se7W32BXlP9K5d3T4_CHiNnnFnMHo,6665
13
- aiobmsble/bms/dummy_bms.py,sha256=1OvcZByFAPtHhz53JyAaDVZ02a1JkCissTRHQKcYyog,3504
14
- aiobmsble/bms/ecoworthy_bms.py,sha256=VL3pbU1AtrNBwAIlhjinTiy86Lu8ITvwe55A66GXwGU,5409
15
- aiobmsble/bms/ective_bms.py,sha256=jvkXaJe0_MvHeV8BiDg7oxCzT_z0WvBvuxybzbbY8Sk,5850
16
- aiobmsble/bms/ej_bms.py,sha256=B8KWs0Py91TrvvxLKIIwgQ5ppkeQtq8tsQ024jQCeKA,8028
17
- aiobmsble/bms/felicity_bms.py,sha256=RPTvmnDuedErIiVKdsUR-w8zhF7_IOj_gmk7A2VIHTo,4737
18
- aiobmsble/bms/jbd_bms.py,sha256=eWXbKIgWEDlj18TWho5Q2K3YfLyhCJVJOgC6tc5VfxU,7166
19
- aiobmsble/bms/jikong_bms.py,sha256=vl5XQx5-ksYMUaopHdd8Pzw_Rw88zF_0I8m23TPD6BE,11152
20
- aiobmsble/bms/neey_bms.py,sha256=DsIqt2MP9E6lJACw0wDlkx0gMyIPpCwMvIGLz36Otbc,7665
21
- aiobmsble/bms/ogt_bms.py,sha256=t1mxa0l2umlH7p3AKSHclieqQGHRYtgt_iFMVQ7ry8U,7987
22
- aiobmsble/bms/pro_bms.py,sha256=PwP6OeXOj6W3Svu80LOgvqnFaUSdGIm6uTO5If2VhKk,5078
23
- aiobmsble/bms/redodo_bms.py,sha256=EVFWurOvkCuHplHjn7NTqaY-0TmbRBUtFDjrBXqvUcM,4369
24
- aiobmsble/bms/renogy_bms.py,sha256=Pju6kZea0G1uGNONcxQymJ_TW6rk02ovGgIn1-dE_68,5079
25
- aiobmsble/bms/renogy_pro_bms.py,sha256=PO7Q0NaPAf9vzU24PvVubxk7k64L241rQESrk26ul4c,3968
26
- aiobmsble/bms/roypow_bms.py,sha256=l9oJvTPcvS54DDjgeu8Wn74nvwYaEbaK5F4JHGAt_RE,6168
27
- aiobmsble/bms/seplos_bms.py,sha256=mgNcYy1E9KMyE-J8jk8mxF2RZ1D3AZtMYNePVq3d0bs,9517
28
- aiobmsble/bms/seplos_v2_bms.py,sha256=nmzOLHqcaxDnZBCb1i_E1qdF7NN8K10-FAGnnIfjtjA,7493
29
- aiobmsble/bms/tdt_bms.py,sha256=9mFrjmkNo6YY_7klfvJi1_qq8J53o_Ivep9bPO2El3A,7032
30
- aiobmsble/bms/tianpwr_bms.py,sha256=U_du6TzYj_SXZ_f_DHVYVCDhku5hwWWdkkITtJJLNX8,4946
31
- aiobmsble-0.2.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
32
- aiobmsble-0.2.3.dist-info/METADATA,sha256=uT1jajia9dsgQWZzlbuNmBr-N1zS8AoL_VLEKUNBhZg,4711
33
- aiobmsble-0.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
34
- aiobmsble-0.2.3.dist-info/entry_points.txt,sha256=HSC_C3nQikc3nk0a6mcG92RuIM7wAzozjBVfDojJceo,54
35
- aiobmsble-0.2.3.dist-info/top_level.txt,sha256=YHBVzg45mJ3vPz0sl_TpMB0edMqqhD61kwJj4EPAk9g,10
36
- aiobmsble-0.2.3.dist-info/RECORD,,