aiobmsble 0.2.0__py3-none-any.whl → 0.2.2__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.
Files changed (37) hide show
  1. aiobmsble/__init__.py +5 -1
  2. aiobmsble/__main__.py +5 -1
  3. aiobmsble/basebms.py +10 -1
  4. aiobmsble/bms/__init__.py +5 -0
  5. aiobmsble/bms/abc_bms.py +168 -0
  6. aiobmsble/bms/ant_bms.py +200 -0
  7. aiobmsble/bms/braunpwr_bms.py +171 -0
  8. aiobmsble/bms/cbtpwr_bms.py +172 -0
  9. aiobmsble/bms/cbtpwr_vb_bms.py +188 -0
  10. aiobmsble/bms/daly_bms.py +168 -0
  11. aiobmsble/bms/dpwrcore_bms.py +211 -0
  12. aiobmsble/bms/dummy_bms.py +93 -0
  13. aiobmsble/bms/ecoworthy_bms.py +155 -0
  14. aiobmsble/bms/ective_bms.py +181 -0
  15. aiobmsble/bms/ej_bms.py +237 -0
  16. aiobmsble/bms/felicity_bms.py +143 -0
  17. aiobmsble/bms/jbd_bms.py +207 -0
  18. aiobmsble/bms/jikong_bms.py +305 -0
  19. aiobmsble/bms/neey_bms.py +218 -0
  20. aiobmsble/bms/ogt_bms.py +218 -0
  21. aiobmsble/bms/pro_bms.py +148 -0
  22. aiobmsble/bms/redodo_bms.py +131 -0
  23. aiobmsble/bms/renogy_bms.py +152 -0
  24. aiobmsble/bms/renogy_pro_bms.py +109 -0
  25. aiobmsble/bms/roypow_bms.py +190 -0
  26. aiobmsble/bms/seplos_bms.py +249 -0
  27. aiobmsble/bms/seplos_v2_bms.py +209 -0
  28. aiobmsble/bms/tdt_bms.py +203 -0
  29. aiobmsble/bms/tianpwr_bms.py +142 -0
  30. aiobmsble/utils.py +16 -6
  31. {aiobmsble-0.2.0.dist-info → aiobmsble-0.2.2.dist-info}/METADATA +3 -2
  32. aiobmsble-0.2.2.dist-info/RECORD +36 -0
  33. aiobmsble-0.2.0.dist-info/RECORD +0 -10
  34. {aiobmsble-0.2.0.dist-info → aiobmsble-0.2.2.dist-info}/WHEEL +0 -0
  35. {aiobmsble-0.2.0.dist-info → aiobmsble-0.2.2.dist-info}/entry_points.txt +0 -0
  36. {aiobmsble-0.2.0.dist-info → aiobmsble-0.2.2.dist-info}/licenses/LICENSE +0 -0
  37. {aiobmsble-0.2.0.dist-info → aiobmsble-0.2.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,305 @@
1
+ """Module to support Jikong Smart BMS.
2
+
3
+ Project: aiobmsble, https://pypi.org/p/aiobmsble/
4
+ License: Apache-2.0, http://www.apache.org/licenses/
5
+ """
6
+
7
+ import asyncio
8
+ from typing import Final
9
+
10
+ from bleak.backends.characteristic import BleakGATTCharacteristic
11
+ from bleak.backends.device import BLEDevice
12
+ from bleak.uuids import normalize_uuid_str
13
+
14
+ from aiobmsble import BMSdp, BMSmode, BMSsample, BMSvalue, MatcherPattern
15
+ from aiobmsble.basebms import BaseBMS, crc_sum
16
+
17
+
18
+ class BMS(BaseBMS):
19
+ """Jikong Smart BMS class implementation."""
20
+
21
+ HEAD_RSP: Final = bytes([0x55, 0xAA, 0xEB, 0x90]) # header for responses
22
+ HEAD_CMD: Final = bytes([0xAA, 0x55, 0x90, 0xEB]) # header for commands (endiness!)
23
+ _READY_MSG: Final = HEAD_CMD + bytes([0xC8, 0x01, 0x01] + [0x00] * 12 + [0x44])
24
+ _BT_MODULE_MSG: Final = bytes([0x41, 0x54, 0x0D, 0x0A]) # AT\r\n from BLE module
25
+ TYPE_POS: Final[int] = 4 # frame type is right after the header
26
+ INFO_LEN: Final[int] = 300
27
+ _FIELDS: Final[tuple[BMSdp, ...]] = ( # Protocol: JK02_32S; JK02_24S has offset -32
28
+ BMSdp("voltage", 150, 4, False, lambda x: x / 1000),
29
+ BMSdp("current", 158, 4, True, lambda x: x / 1000),
30
+ BMSdp("battery_level", 173, 1, False, lambda x: x),
31
+ BMSdp("cycle_charge", 174, 4, False, lambda x: x / 1000),
32
+ BMSdp("cycles", 182, 4, False, lambda x: x),
33
+ BMSdp("balance_current", 170, 2, True, lambda x: x / 1000),
34
+ BMSdp("temp_sensors", 214, 2, True, lambda x: x),
35
+ BMSdp("problem_code", 166, 4, False, lambda x: x),
36
+ )
37
+
38
+ def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
39
+ """Intialize private BMS members."""
40
+ super().__init__(ble_device, reconnect)
41
+ self._data_final: bytearray = bytearray()
42
+ self._char_write_handle: int = -1
43
+ self._bms_info: dict[str, str] = {}
44
+ self._prot_offset: int = 0
45
+ self._sw_version: int = 0
46
+ self._valid_reply: int = 0x02
47
+ self._bms_ready: bool = False
48
+
49
+ @staticmethod
50
+ def matcher_dict_list() -> list[MatcherPattern]:
51
+ """Provide BluetoothMatcher definition."""
52
+ return [
53
+ {
54
+ "service_uuid": BMS.uuid_services()[0],
55
+ "connectable": True,
56
+ "manufacturer_id": 0x0B65,
57
+ },
58
+ ]
59
+
60
+ @staticmethod
61
+ def device_info() -> dict[str, str]:
62
+ """Return device information for the battery management system."""
63
+ return {"manufacturer": "Jikong", "model": "Smart BMS"}
64
+
65
+ @staticmethod
66
+ def uuid_services() -> list[str]:
67
+ """Return list of 128-bit UUIDs of services required by BMS."""
68
+ return [normalize_uuid_str("ffe0")]
69
+
70
+ @staticmethod
71
+ def uuid_rx() -> str:
72
+ """Return 16-bit UUID of characteristic that provides notification/read property."""
73
+ return "ffe1"
74
+
75
+ @staticmethod
76
+ def uuid_tx() -> str:
77
+ """Return 16-bit UUID of characteristic that provides write property."""
78
+ return "ffe1"
79
+
80
+ @staticmethod
81
+ def _calc_values() -> frozenset[BMSvalue]:
82
+ return frozenset(
83
+ {
84
+ "power",
85
+ "battery_charging",
86
+ "cycle_capacity",
87
+ "runtime",
88
+ "temperature",
89
+ }
90
+ )
91
+
92
+ def _notification_handler(
93
+ self, _sender: BleakGATTCharacteristic, data: bytearray
94
+ ) -> None:
95
+ """Retrieve BMS data update."""
96
+
97
+ if data.startswith(BMS._BT_MODULE_MSG):
98
+ self._log.debug("filtering AT cmd")
99
+ if not (data := data.removeprefix(BMS._BT_MODULE_MSG)):
100
+ return
101
+
102
+ if (
103
+ len(self._data) >= self.INFO_LEN
104
+ and (data.startswith((BMS.HEAD_RSP, BMS.HEAD_CMD)))
105
+ ) or not self._data.startswith(BMS.HEAD_RSP):
106
+ self._data = bytearray()
107
+
108
+ self._data += data
109
+
110
+ self._log.debug(
111
+ "RX BLE data (%s): %s", "start" if data == self._data else "cnt.", data
112
+ )
113
+
114
+ # verify that data is long enough
115
+ if (
116
+ len(self._data) < BMS.INFO_LEN and self._data.startswith(BMS.HEAD_RSP)
117
+ ) or len(self._data) < BMS.TYPE_POS + 1:
118
+ return
119
+
120
+ # check that message type is expected
121
+ if self._data[BMS.TYPE_POS] != self._valid_reply:
122
+ self._log.debug(
123
+ "unexpected message type 0x%X (length %i): %s",
124
+ self._data[BMS.TYPE_POS],
125
+ len(self._data),
126
+ self._data,
127
+ )
128
+ return
129
+
130
+ # trim AT\r\n message from the end
131
+ if self._data.endswith(BMS._BT_MODULE_MSG):
132
+ self._log.debug("trimming AT cmd")
133
+ self._data = self._data.removesuffix(BMS._BT_MODULE_MSG)
134
+
135
+ # set BMS ready if msg is attached to last responses (v19.05)
136
+ if self._data[BMS.INFO_LEN :].startswith(BMS._READY_MSG):
137
+ self._log.debug("BMS ready.")
138
+ self._bms_ready = True
139
+ self._data = self._data[: BMS.INFO_LEN]
140
+
141
+ # trim message in case oversized
142
+ if len(self._data) > BMS.INFO_LEN:
143
+ self._log.debug("wrong data length (%i): %s", len(self._data), self._data)
144
+ self._data = self._data[: BMS.INFO_LEN]
145
+
146
+ if (crc := crc_sum(self._data[:-1])) != self._data[-1]:
147
+ self._log.debug("invalid checksum 0x%X != 0x%X", self._data[-1], crc)
148
+ return
149
+
150
+ self._data_final = self._data.copy()
151
+ self._data_event.set()
152
+
153
+ async def _init_connection(
154
+ self, char_notify: BleakGATTCharacteristic | int | str | None = None
155
+ ) -> None:
156
+ """Initialize RX/TX characteristics and protocol state."""
157
+ char_notify_handle: int = -1
158
+ self._char_write_handle = -1
159
+ self._bms_ready = False
160
+
161
+ for service in self._client.services:
162
+ for char in service.characteristics:
163
+ self._log.debug(
164
+ "discovered %s (#%i): %s", char.uuid, char.handle, char.properties
165
+ )
166
+ if char.uuid == normalize_uuid_str(
167
+ BMS.uuid_rx()
168
+ ) or char.uuid == normalize_uuid_str(BMS.uuid_tx()):
169
+ if "notify" in char.properties:
170
+ char_notify_handle = char.handle
171
+ if (
172
+ "write" in char.properties
173
+ or "write-without-response" in char.properties
174
+ ):
175
+ self._char_write_handle = char.handle
176
+ if char_notify_handle == -1 or self._char_write_handle == -1:
177
+ self._log.debug("failed to detect characteristics.")
178
+ await self._client.disconnect()
179
+ raise ConnectionError(f"Failed to detect characteristics from {self.name}.")
180
+ self._log.debug(
181
+ "using characteristics handle #%i (notify), #%i (write).",
182
+ char_notify_handle,
183
+ self._char_write_handle,
184
+ )
185
+
186
+ await super()._init_connection()
187
+
188
+ # query device info frame (0x03) and wait for BMS ready (0xC8)
189
+ self._valid_reply = 0x03
190
+ await self._await_reply(self._cmd(b"\x97"), char=self._char_write_handle)
191
+ self._bms_info = BMS._dec_devinfo(self._data_final or bytearray())
192
+ self._log.debug("device information: %s", self._bms_info)
193
+ self._prot_offset = (
194
+ -32 if int(self._bms_info.get("sw_version", "")[:2]) < 11 else 0
195
+ )
196
+ if not self._bms_ready:
197
+ self._valid_reply = 0xC8 # BMS ready confirmation
198
+ await asyncio.wait_for(self._wait_event(), timeout=BMS.TIMEOUT)
199
+ self._valid_reply = 0x02 # cell information
200
+
201
+ @staticmethod
202
+ def _cmd(cmd: bytes, value: list[int] | None = None) -> bytes:
203
+ """Assemble a Jikong BMS command."""
204
+ value = [] if value is None else value
205
+ assert len(value) <= 13
206
+ frame: bytearray = bytearray(
207
+ [*BMS.HEAD_CMD, cmd[0], len(value), *value]
208
+ ) + bytearray(13 - len(value))
209
+ frame.append(crc_sum(frame))
210
+ return bytes(frame)
211
+
212
+ @staticmethod
213
+ def _dec_devinfo(data: bytearray) -> dict[str, str]:
214
+ fields: Final[dict[str, int]] = {
215
+ "hw_version": 22,
216
+ "sw_version": 30,
217
+ }
218
+ return {
219
+ key: data[idx : idx + 8].decode(errors="replace").strip("\x00")
220
+ for key, idx in fields.items()
221
+ }
222
+
223
+ def _temp_pos(self) -> list[tuple[int, int]]:
224
+ sw_majv: Final[int] = int(self._bms_info.get("sw_version", "")[:2])
225
+ if sw_majv >= 14:
226
+ return [(0, 144), (1, 162), (2, 164), (3, 254), (4, 256), (5, 258)]
227
+ if sw_majv >= 11:
228
+ return [(0, 144), (1, 162), (2, 164), (3, 254)]
229
+ return [(0, 130), (1, 132), (2, 134)]
230
+
231
+ @staticmethod
232
+ def _temp_sensors(
233
+ data: bytearray, temp_pos: list[tuple[int, int]], mask: int
234
+ ) -> list[int | float]:
235
+ return [
236
+ (value / 10)
237
+ for idx, pos in temp_pos
238
+ if mask & (1 << idx)
239
+ and (
240
+ value := int.from_bytes(
241
+ data[pos : pos + 2], byteorder="little", signed=True
242
+ )
243
+ )
244
+ != -2000
245
+ ]
246
+
247
+ @staticmethod
248
+ def _conv_data(data: bytearray, offs: int, sw_majv: int) -> BMSsample:
249
+ """Return BMS data from status message."""
250
+
251
+ result: BMSsample = BMS._decode_data(
252
+ BMS._FIELDS, data, byteorder="little", offset=offs
253
+ )
254
+ result["cell_count"] = int.from_bytes(
255
+ data[70 + (offs >> 1) : 74 + (offs >> 1)], byteorder="little"
256
+ ).bit_count()
257
+
258
+ result["delta_voltage"] = (
259
+ int.from_bytes(
260
+ data[76 + (offs >> 1) : 78 + (offs >> 1)], byteorder="little"
261
+ )
262
+ / 1000
263
+ )
264
+
265
+ if sw_majv >= 15:
266
+ result["battery_mode"] = (
267
+ BMSmode(data[280 + offs])
268
+ if data[280 + offs] in BMSmode
269
+ else BMSmode.UNKNOWN
270
+ )
271
+
272
+ return result
273
+
274
+ async def _async_update(self) -> BMSsample:
275
+ """Update battery status information."""
276
+ if not self._data_event.is_set() or self._data_final[4] != 0x02:
277
+ # request cell info (only if data is not constantly published)
278
+ self._log.debug("requesting cell info")
279
+ await self._await_reply(
280
+ data=BMS._cmd(b"\x96"), char=self._char_write_handle
281
+ )
282
+
283
+ data: BMSsample = self._conv_data(
284
+ self._data_final,
285
+ self._prot_offset,
286
+ int(self._bms_info.get("sw_version", "")[:2]),
287
+ )
288
+ data["temp_values"] = BMS._temp_sensors(
289
+ self._data_final, self._temp_pos(), data.get("temp_sensors", 0)
290
+ )
291
+
292
+ data["problem_code"] = (
293
+ ((data.get("problem_code", 0)) >> 16)
294
+ if self._prot_offset
295
+ else (data.get("problem_code", 0) & 0xFFFF)
296
+ )
297
+
298
+ data["cell_voltages"] = BMS._cell_voltages(
299
+ self._data_final,
300
+ cells=data.get("cell_count", 0),
301
+ start=6,
302
+ byteorder="little",
303
+ )
304
+
305
+ return data
@@ -0,0 +1,218 @@
1
+ """Module to support Neey Smart BMS.
2
+
3
+ Project: aiobmsble, https://pypi.org/p/aiobmsble/
4
+ License: Apache-2.0, http://www.apache.org/licenses/
5
+ """
6
+
7
+ from collections.abc import Callable
8
+ from struct import unpack_from
9
+ from typing import Any, Final, Literal
10
+
11
+ from bleak.backends.characteristic import BleakGATTCharacteristic
12
+ from bleak.backends.device import BLEDevice
13
+ from bleak.uuids import normalize_uuid_str
14
+
15
+ from aiobmsble import BMSsample, BMSvalue, MatcherPattern
16
+ from aiobmsble.basebms import BaseBMS, crc_sum
17
+
18
+
19
+ class BMS(BaseBMS):
20
+ """Neey Smart BMS class implementation."""
21
+
22
+ _BT_MODULE_MSG: Final = bytes([0x41, 0x54, 0x0D, 0x0A]) # AT\r\n from BLE module
23
+ _HEAD_RSP: Final = bytes([0x55, 0xAA, 0x11, 0x01]) # start, dev addr, read cmd
24
+ _HEAD_CMD: Final = bytes(
25
+ [0xAA, 0x55, 0x11, 0x01]
26
+ ) # header for commands (endiness!)
27
+ _TAIL: Final[int] = 0xFF # end of message
28
+ _TYPE_POS: Final[int] = 4 # frame type is right after the header
29
+ _MIN_FRAME: Final[int] = 10 # header length
30
+ _FIELDS: Final[list[tuple[BMSvalue, int, str, Callable[[int], Any]]]] = [
31
+ ("voltage", 201, "<f", lambda x: round(x, 3)),
32
+ ("delta_voltage", 209, "<f", lambda x: round(x, 3)),
33
+ ("problem_code", 216, "B", lambda x: x if x in {1, 3, 7, 8, 9, 10, 11} else 0),
34
+ ("balance_current", 217, "<f", lambda x: round(x, 3)),
35
+ ]
36
+
37
+ def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
38
+ """Intialize private BMS members."""
39
+ super().__init__(ble_device, reconnect)
40
+ self._data_final: bytearray = bytearray()
41
+ self._bms_info: dict[str, str] = {}
42
+ self._exp_len: int = BMS._MIN_FRAME
43
+ self._valid_reply: int = 0x02
44
+
45
+ @staticmethod
46
+ def matcher_dict_list() -> list[MatcherPattern]:
47
+ """Provide BluetoothMatcher definition."""
48
+ return [
49
+ {
50
+ "local_name": pattern,
51
+ "service_uuid": normalize_uuid_str("fee7"),
52
+ "connectable": True,
53
+ }
54
+ for pattern in ("EK-*", "GW-*")
55
+ ]
56
+
57
+ @staticmethod
58
+ def device_info() -> dict[str, str]:
59
+ """Return device information for the battery management system."""
60
+ return {"manufacturer": "Neey", "model": "Balancer"}
61
+
62
+ @staticmethod
63
+ def uuid_services() -> list[str]:
64
+ """Return list of 128-bit UUIDs of services required by BMS."""
65
+ return [normalize_uuid_str("ffe0")]
66
+
67
+ @staticmethod
68
+ def uuid_rx() -> str:
69
+ """Return 16-bit UUID of characteristic that provides notification/read property."""
70
+ return "ffe1"
71
+
72
+ @staticmethod
73
+ def uuid_tx() -> str:
74
+ """Return 16-bit UUID of characteristic that provides write property."""
75
+ return "ffe1"
76
+
77
+ @staticmethod
78
+ def _calc_values() -> frozenset[BMSvalue]:
79
+ return frozenset({"temperature"})
80
+
81
+ def _notification_handler(
82
+ self, _sender: BleakGATTCharacteristic, data: bytearray
83
+ ) -> None:
84
+ """Retrieve BMS data update."""
85
+
86
+ if (
87
+ len(self._data) >= self._exp_len or not self._data.startswith(BMS._HEAD_RSP)
88
+ ) and data.startswith(BMS._HEAD_RSP):
89
+ self._data = bytearray()
90
+ self._exp_len = max(
91
+ int.from_bytes(data[6:8], byteorder="little", signed=False),
92
+ BMS._MIN_FRAME,
93
+ )
94
+
95
+ self._data += data
96
+
97
+ self._log.debug(
98
+ "RX BLE data (%s): %s", "start" if data == self._data else "cnt.", data
99
+ )
100
+
101
+ # verify that data is long enough
102
+ if len(self._data) < self._exp_len:
103
+ return
104
+
105
+ if not self._data.startswith(BMS._HEAD_RSP):
106
+ self._log.debug("incorrect frame start.")
107
+ return
108
+
109
+ # trim message in case oversized
110
+ if len(self._data) > self._exp_len:
111
+ self._log.debug("wrong data length (%i): %s", len(self._data), self._data)
112
+ self._data = self._data[: self._exp_len]
113
+
114
+ if self._data[-1] != BMS._TAIL:
115
+ self._log.debug("incorrect frame end.")
116
+ return
117
+
118
+ # check that message type is expected
119
+ if self._data[BMS._TYPE_POS] != self._valid_reply:
120
+ self._log.debug(
121
+ "unexpected message type 0x%X (length %i): %s",
122
+ self._data[BMS._TYPE_POS],
123
+ len(self._data),
124
+ self._data,
125
+ )
126
+ return
127
+
128
+ if (crc := crc_sum(self._data[:-2])) != self._data[-2]:
129
+ self._log.debug("invalid checksum 0x%X != 0x%X", self._data[-2], crc)
130
+ return
131
+
132
+ self._data_final = self._data.copy()
133
+ self._data_event.set()
134
+
135
+ async def _init_connection(
136
+ self, char_notify: BleakGATTCharacteristic | int | str | None = None
137
+ ) -> None:
138
+ """Initialize RX/TX characteristics and protocol state."""
139
+ await super()._init_connection(char_notify)
140
+
141
+ # query device info frame (0x03) and wait for BMS ready (0xC8)
142
+ self._valid_reply = 0x01
143
+ await self._await_reply(self._cmd(b"\x01"))
144
+ self._bms_info = BMS._dec_devinfo(self._data_final or bytearray())
145
+ self._log.debug("device information: %s", self._bms_info)
146
+
147
+ self._valid_reply = 0x02 # cell information
148
+
149
+ @staticmethod
150
+ def _cmd(cmd: bytes, reg: int = 0, value: list[int] | None = None) -> bytes:
151
+ """Assemble a Neey BMS command."""
152
+ value = [] if value is None else value
153
+ assert len(value) <= 11
154
+ frame: bytearray = bytearray( # 0x14 frame length
155
+ [*BMS._HEAD_CMD, cmd[0], reg & 0xFF, 0x14, *value]
156
+ ) + bytearray(11 - len(value))
157
+ frame += bytes([crc_sum(frame), BMS._TAIL])
158
+ return bytes(frame)
159
+
160
+ @staticmethod
161
+ def _dec_devinfo(data: bytearray) -> dict[str, str]:
162
+ fields: Final[dict[str, int]] = {
163
+ "hw_version": 24,
164
+ "sw_version": 32,
165
+ }
166
+ return {
167
+ key: data[idx : idx + 8].decode(errors="replace").strip("\x00")
168
+ for key, idx in fields.items()
169
+ }
170
+
171
+ @staticmethod
172
+ def _cell_voltages(
173
+ data: bytearray,
174
+ *,
175
+ cells: int,
176
+ start: int,
177
+ size: int = 2,
178
+ byteorder: Literal["little", "big"] = "big",
179
+ divider: int = 1000,
180
+ ) -> list[float]:
181
+ """Parse cell voltages from message."""
182
+ return [
183
+ round(value, 3)
184
+ for idx in range(cells)
185
+ if (value := unpack_from("<f", data, start + idx * size)[0])
186
+ ]
187
+
188
+ @staticmethod
189
+ def _temp_sensors(data: bytearray, sensors: int) -> list[int | float]:
190
+ return [
191
+ round(unpack_from("<f", data, 221 + idx * 4)[0], 2)
192
+ for idx in range(sensors)
193
+ ]
194
+
195
+ @staticmethod
196
+ def _conv_data(data: bytearray) -> BMSsample:
197
+ """Return BMS data from status message."""
198
+ result: BMSsample = {}
199
+ for key, idx, fmt, func in BMS._FIELDS:
200
+ result[key] = func(unpack_from(fmt, data, idx)[0])
201
+
202
+ return result
203
+
204
+ async def _async_update(self) -> BMSsample:
205
+ """Update battery status information."""
206
+ if not self._data_event.is_set() or self._data_final[4] != 0x02:
207
+ # request cell info (only if data is not constantly published)
208
+ self._log.debug("requesting cell info")
209
+ await self._await_reply(data=BMS._cmd(b"\x02"))
210
+
211
+ data: BMSsample = self._conv_data(self._data_final)
212
+ data["temp_values"] = BMS._temp_sensors(self._data_final, 2)
213
+
214
+ data["cell_voltages"] = BMS._cell_voltages(
215
+ self._data_final, cells=24, start=9, byteorder="little", size=4
216
+ )
217
+
218
+ return data