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,109 @@
1
+ """Module to support Renogy Pro BMS.
2
+
3
+ Project: aiobmsble, https://pypi.org/p/aiobmsble/
4
+ License: Apache-2.0, http://www.apache.org/licenses/
5
+ """
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, MatcherPattern
12
+ from aiobmsble.bms.renogy_bms import BMS as RenogyBMS
13
+
14
+
15
+ class BMS(RenogyBMS):
16
+ """Renogy Pro battery class implementation."""
17
+
18
+ HEAD: bytes = b"\xff\x03" # SOP, read fct (x03)
19
+ FIELDS: tuple[BMSdp, ...] = (
20
+ BMSdp("voltage", 5, 2, False, lambda x: x / 10),
21
+ BMSdp("current", 3, 2, True, lambda x: x / 10),
22
+ BMSdp("design_capacity", 11, 4, False, lambda x: x // 1000),
23
+ BMSdp("cycle_charge", 7, 4, False, lambda x: x / 1000),
24
+ BMSdp("cycles", 15, 2, False, lambda x: x),
25
+ )
26
+
27
+ def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
28
+ """Intialize private BMS members."""
29
+ super().__init__(ble_device, reconnect)
30
+ self._char_write_handle: int = -1
31
+
32
+ @staticmethod
33
+ def matcher_dict_list() -> list[MatcherPattern]:
34
+ """Provide BluetoothMatcher definition."""
35
+ return [
36
+ {
37
+ "local_name": "RNGRBP*",
38
+ "manufacturer_id": 0xE14C,
39
+ "connectable": True,
40
+ },
41
+ ]
42
+
43
+ @staticmethod
44
+ def device_info() -> dict[str, str]:
45
+ """Return device information for the battery management system."""
46
+ return {"manufacturer": "Renogy", "model": "Bluetooth battery pro"}
47
+
48
+ async def _init_connection(
49
+ self, char_notify: BleakGATTCharacteristic | int | str | None = None
50
+ ) -> None:
51
+ """Initialize RX/TX characteristics and protocol state."""
52
+ char_notify_handle: int = -1
53
+ self._char_write_handle = -1
54
+ assert char_notify is None, "char_notify not used for Renogy Pro BMS"
55
+
56
+ for service in self._client.services:
57
+ self._log.debug(
58
+ "service %s (#%i): %s",
59
+ service.uuid,
60
+ service.handle,
61
+ service.description,
62
+ )
63
+ for char in service.characteristics:
64
+ self._log.debug(
65
+ "characteristic %s (#%i): %s",
66
+ char.uuid,
67
+ char.handle,
68
+ char.properties,
69
+ )
70
+ if (
71
+ service.uuid == BMS.uuid_services()[0]
72
+ and char.uuid == normalize_uuid_str(BMS.uuid_tx())
73
+ and any(
74
+ prop in char.properties
75
+ for prop in ("write", "write-without-response")
76
+ )
77
+ ):
78
+ self._char_write_handle = char.handle
79
+ if (
80
+ service.uuid == BMS.uuid_services()[1]
81
+ and char.uuid == normalize_uuid_str(BMS.uuid_rx())
82
+ and "notify" in char.properties
83
+ ):
84
+ char_notify_handle = char.handle
85
+
86
+ if char_notify_handle == -1 or self._char_write_handle == -1:
87
+ self._log.debug("failed to detect characteristics.")
88
+ await self._client.disconnect()
89
+ raise ConnectionError(f"Failed to detect characteristics from {self.name}.")
90
+ self._log.debug(
91
+ "using characteristics handle #%i (notify), #%i (write).",
92
+ char_notify_handle,
93
+ self._char_write_handle,
94
+ )
95
+
96
+ await super()._init_connection(char_notify_handle)
97
+
98
+ async def _await_reply(
99
+ self,
100
+ data: bytes,
101
+ char: int | str | None = None,
102
+ wait_for_notify: bool = True,
103
+ max_size: int = 0,
104
+ ) -> None:
105
+ """Send data to the BMS and wait for valid reply notification."""
106
+
107
+ await super()._await_reply(
108
+ data, self._char_write_handle, wait_for_notify, max_size
109
+ )
@@ -0,0 +1,190 @@
1
+ """Module to support RoyPow BMS.
2
+
3
+ Project: aiobmsble, https://pypi.org/p/aiobmsble/
4
+ License: Apache-2.0, http://www.apache.org/licenses/
5
+ """
6
+
7
+ from typing import Final
8
+
9
+ from bleak.backends.characteristic import BleakGATTCharacteristic
10
+ from bleak.backends.device import BLEDevice
11
+ from bleak.uuids import normalize_uuid_str
12
+
13
+ from aiobmsble import BMSdp, BMSsample, BMSvalue, MatcherPattern
14
+ from aiobmsble.basebms import BaseBMS
15
+
16
+
17
+ class BMS(BaseBMS):
18
+ """RoyPow BMS implementation."""
19
+
20
+ _HEAD: Final[bytes] = b"\xea\xd1\x01"
21
+ _TAIL: Final[int] = 0xF5
22
+ _BT_MODULE_MSG: Final[bytes] = b"AT+STAT\r\n" # AT cmd from BLE module
23
+ _MIN_LEN: Final[int] = len(_HEAD) + 1
24
+ _FIELDS: Final[tuple[BMSdp, ...]] = (
25
+ BMSdp("battery_level", 7, 1, False, lambda x: x, 0x4),
26
+ BMSdp("voltage", 47, 2, False, lambda x: x / 100, 0x4),
27
+ BMSdp(
28
+ "current",
29
+ 6,
30
+ 3,
31
+ False,
32
+ lambda x: (x & 0xFFFF) * (-1 if (x >> 16) & 0x1 else 1) / 100,
33
+ 0x3,
34
+ ),
35
+ BMSdp("problem_code", 9, 3, False, lambda x: x, 0x3),
36
+ BMSdp(
37
+ "cycle_charge",
38
+ 24,
39
+ 4,
40
+ False,
41
+ lambda x: ((x & 0xFFFF0000) | (x & 0xFF00) >> 8 | (x & 0xFF) << 8) / 1000,
42
+ 0x4,
43
+ ),
44
+ BMSdp("runtime", 30, 2, False, lambda x: x * 60, 0x4),
45
+ BMSdp("temp_sensors", 13, 1, False, lambda x: x, 0x3),
46
+ BMSdp("cycles", 9, 2, False, lambda x: x, 0x4),
47
+ )
48
+ _CMDS: Final[set[int]] = set({field.idx for field in _FIELDS})
49
+
50
+ def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
51
+ """Initialize BMS."""
52
+ super().__init__(ble_device, reconnect)
53
+ self._data_final: dict[int, bytearray] = {}
54
+ self._exp_len: int = 0
55
+
56
+ @staticmethod
57
+ def matcher_dict_list() -> list[MatcherPattern]:
58
+ """Provide BluetoothMatcher definition."""
59
+ return [
60
+ {
61
+ "service_uuid": BMS.uuid_services()[0],
62
+ "manufacturer_id": manufacturer_id,
63
+ "connectable": True,
64
+ }
65
+ for manufacturer_id in (0x01A8, 0x0B31, 0x8AFB)
66
+ ]
67
+
68
+ @staticmethod
69
+ def device_info() -> dict[str, str]:
70
+ """Return device information for the battery management system."""
71
+ return {"manufacturer": "RoyPow", "model": "SmartBMS"}
72
+
73
+ @staticmethod
74
+ def uuid_services() -> list[str]:
75
+ """Return list of 128-bit UUIDs of services required by BMS."""
76
+ return [normalize_uuid_str("ffe0")]
77
+
78
+ @staticmethod
79
+ def uuid_rx() -> str:
80
+ """Return 16-bit UUID of characteristic that provides notification/read property."""
81
+ return "ffe1"
82
+
83
+ @staticmethod
84
+ def uuid_tx() -> str:
85
+ """Return 16-bit UUID of characteristic that provides write property."""
86
+ return "ffe1"
87
+
88
+ @staticmethod
89
+ def _calc_values() -> frozenset[BMSvalue]:
90
+ return frozenset(
91
+ {
92
+ "battery_charging",
93
+ "cycle_capacity",
94
+ "delta_voltage",
95
+ "power",
96
+ "temperature",
97
+ }
98
+ ) # calculate further values from BMS provided set ones
99
+
100
+ def _notification_handler(
101
+ self, _sender: BleakGATTCharacteristic, data: bytearray
102
+ ) -> None:
103
+ """Handle the RX characteristics notify event (new data arrives)."""
104
+ if not (data := data.removeprefix(BMS._BT_MODULE_MSG)):
105
+ self._log.debug("filtering AT cmd")
106
+ return
107
+
108
+ if (
109
+ data.startswith(BMS._HEAD)
110
+ and not self._data.startswith(BMS._HEAD)
111
+ and len(data) > len(BMS._HEAD)
112
+ ):
113
+ self._exp_len = data[len(BMS._HEAD)]
114
+ self._data.clear()
115
+
116
+ self._data += data
117
+ self._log.debug(
118
+ "RX BLE data (%s): %s", "start" if data == self._data else "cnt.", data
119
+ )
120
+
121
+ if not self._data.startswith(BMS._HEAD):
122
+ self._data.clear()
123
+ return
124
+
125
+ # verify that data is long enough
126
+ if len(self._data) < BMS._MIN_LEN + self._exp_len:
127
+ return
128
+
129
+ end_idx: Final[int] = BMS._MIN_LEN + self._exp_len - 1
130
+ if self._data[end_idx] != BMS._TAIL:
131
+ self._log.debug("incorrect EOF: %s", self._data)
132
+ self._data.clear()
133
+ return
134
+
135
+ if (crc := BMS._crc(self._data[len(BMS._HEAD) : end_idx - 1])) != self._data[
136
+ end_idx - 1
137
+ ]:
138
+ self._log.debug(
139
+ "invalid checksum 0x%X != 0x%X", self._data[end_idx - 1], crc
140
+ )
141
+ self._data.clear()
142
+ return
143
+
144
+ self._data_final[self._data[5]] = self._data.copy()
145
+ self._data.clear()
146
+ self._data_event.set()
147
+
148
+ @staticmethod
149
+ def _crc(frame: bytearray) -> int:
150
+ """Calculate XOR of all frame bytes."""
151
+ crc: int = 0
152
+ for b in frame:
153
+ crc ^= b
154
+ return crc
155
+
156
+ @staticmethod
157
+ def _cmd(cmd: bytes) -> bytes:
158
+ """Assemble a RoyPow BMS command."""
159
+ data: Final[bytearray] = bytearray([len(cmd) + 2, *cmd])
160
+ return bytes([*BMS._HEAD, *data, BMS._crc(data), BMS._TAIL])
161
+
162
+ async def _async_update(self) -> BMSsample:
163
+ """Update battery status information."""
164
+
165
+ self._data.clear()
166
+ self._data_final.clear()
167
+ for cmd in range(2, 5):
168
+ await self._await_reply(BMS._cmd(bytes([0xFF, cmd])))
169
+
170
+ result: BMSsample = BMS._decode_data(BMS._FIELDS, self._data_final)
171
+
172
+ # remove remaining runtime if battery is charging
173
+ if result.get("runtime") == 0xFFFF * 60:
174
+ result.pop("runtime", None)
175
+
176
+ result["cell_voltages"] = BMS._cell_voltages(
177
+ self._data_final.get(0x2, bytearray()),
178
+ cells=max(0, (len(self._data_final.get(0x2, bytearray())) - 11) // 2),
179
+ start=9,
180
+ )
181
+ result["temp_values"] = BMS._temp_values(
182
+ self._data_final.get(0x3, bytearray()),
183
+ values=result.get("temp_sensors", 0),
184
+ start=14,
185
+ size=1,
186
+ signed=False,
187
+ offset=40,
188
+ )
189
+
190
+ return result
@@ -0,0 +1,249 @@
1
+ """Module to support Seplos V3 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 typing import Any, 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, BMSpackvalue, BMSsample, BMSvalue, MatcherPattern
15
+ from aiobmsble.basebms import BaseBMS, crc_modbus
16
+
17
+
18
+ class BMS(BaseBMS):
19
+ """Seplos V3 Smart BMS class implementation."""
20
+
21
+ CMD_READ: Final[list[int]] = [0x01, 0x04]
22
+ HEAD_LEN: Final[int] = 3
23
+ CRC_LEN: Final[int] = 2
24
+ PIA_LEN: Final[int] = 0x11
25
+ PIB_LEN: Final[int] = 0x1A
26
+ EIA_LEN: Final[int] = PIB_LEN
27
+ EIB_LEN: Final[int] = 0x16
28
+ EIC_LEN: Final[int] = 0x5
29
+ _TEMP_START: Final[int] = HEAD_LEN + 32
30
+ QUERY: Final[dict[str, tuple[int, int, int]]] = {
31
+ # name: cmd, reg start, length
32
+ "EIA": (0x4, 0x2000, EIA_LEN),
33
+ "EIB": (0x4, 0x2100, EIB_LEN),
34
+ "EIC": (0x1, 0x2200, EIC_LEN),
35
+ }
36
+ PQUERY: Final[dict[str, tuple[int, int, int]]] = {
37
+ "PIA": (0x4, 0x1000, PIA_LEN),
38
+ "PIB": (0x4, 0x1100, PIB_LEN),
39
+ }
40
+ _FIELDS: Final[tuple[BMSdp, ...]] = (
41
+ BMSdp("temperature", 20, 2, True, lambda x: x / 10, EIB_LEN), # avg. ctemp
42
+ BMSdp("voltage", 0, 4, False, lambda x: BMS._swap32(x) / 100, EIA_LEN),
43
+ BMSdp("current", 4, 4, True, lambda x: BMS._swap32(x, True) / 10, EIA_LEN),
44
+ BMSdp("cycle_charge", 8, 4, False, lambda x: BMS._swap32(x) / 100, EIA_LEN),
45
+ BMSdp("pack_count", 44, 2, False, lambda x: x, EIA_LEN),
46
+ BMSdp("cycles", 46, 2, False, lambda x: x, EIA_LEN),
47
+ BMSdp("battery_level", 48, 2, False, lambda x: x / 10, EIA_LEN),
48
+ BMSdp("problem_code", 1, 9, False, lambda x: x & 0xFFFF00FF00FF0000FF, EIC_LEN),
49
+ ) # Protocol Seplos V3
50
+ _PFIELDS: Final[list[tuple[BMSpackvalue, int, bool, Callable[[int], Any]]]] = [
51
+ ("pack_voltages", 0, False, lambda x: x / 100),
52
+ ("pack_currents", 2, True, lambda x: x / 100),
53
+ ("pack_battery_levels", 10, False, lambda x: x / 10),
54
+ ("pack_cycles", 14, False, lambda x: x),
55
+ ] # Protocol Seplos V3
56
+ _CMDS: Final[set[int]] = {field[2] for field in QUERY.values()} | {
57
+ field[2] for field in PQUERY.values()
58
+ }
59
+
60
+ def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
61
+ """Intialize private BMS members."""
62
+ super().__init__(ble_device, reconnect)
63
+ self._data_final: dict[int, bytearray] = {}
64
+ self._pack_count: int = 0 # number of battery packs
65
+ self._pkglen: int = 0 # expected packet length
66
+
67
+ @staticmethod
68
+ def matcher_dict_list() -> list[MatcherPattern]:
69
+ """Provide BluetoothMatcher definition."""
70
+ return [
71
+ {
72
+ "local_name": pattern,
73
+ "service_uuid": BMS.uuid_services()[0],
74
+ "connectable": True,
75
+ }
76
+ for pattern in {f"SP{num}?B*" for num in range(10)} | {"CSY*"}
77
+ ]
78
+
79
+ @staticmethod
80
+ def device_info() -> dict[str, str]:
81
+ """Return device information for the battery management system."""
82
+ return {"manufacturer": "Seplos", "model": "Smart BMS V3"}
83
+
84
+ # setup UUIDs
85
+ # serv 0000fff0-0000-1000-8000-00805f9b34fb
86
+ # char 0000fff1-0000-1000-8000-00805f9b34fb (#16): ['read', 'notify']
87
+ # char 0000fff2-0000-1000-8000-00805f9b34fb (#20): ['read', 'write-without-response', 'write']
88
+ @staticmethod
89
+ def uuid_services() -> list[str]:
90
+ """Return list of 128-bit UUIDs of services required by BMS."""
91
+ return [normalize_uuid_str("fff0")]
92
+
93
+ @staticmethod
94
+ def uuid_rx() -> str:
95
+ """Return 16-bit UUID of characteristic that provides notification/read property."""
96
+ return "fff1"
97
+
98
+ @staticmethod
99
+ def uuid_tx() -> str:
100
+ """Return 16-bit UUID of characteristic that provides write property."""
101
+ return "fff2"
102
+
103
+ @staticmethod
104
+ def _calc_values() -> frozenset[BMSvalue]:
105
+ return frozenset({"power", "battery_charging", "cycle_capacity", "runtime"})
106
+
107
+ def _notification_handler(
108
+ self, _sender: BleakGATTCharacteristic, data: bytearray
109
+ ) -> None:
110
+ """Retrieve BMS data update."""
111
+
112
+ if (
113
+ len(data) > BMS.HEAD_LEN + BMS.CRC_LEN
114
+ and data[0] <= self._pack_count
115
+ and data[1] & 0x7F in BMS.CMD_READ # include read errors
116
+ and data[2] >= BMS.HEAD_LEN + BMS.CRC_LEN
117
+ ):
118
+ self._data = bytearray()
119
+ self._pkglen = data[2] + BMS.HEAD_LEN + BMS.CRC_LEN
120
+ elif ( # error message
121
+ len(data) == BMS.HEAD_LEN + BMS.CRC_LEN
122
+ and data[0] <= self._pack_count
123
+ and data[1] & 0x80
124
+ ):
125
+ self._log.debug("RX error: %X", data[2])
126
+ self._data = bytearray()
127
+ self._pkglen = BMS.HEAD_LEN + BMS.CRC_LEN
128
+
129
+ self._data += data
130
+ self._log.debug(
131
+ "RX BLE data (%s): %s", "start" if data == self._data else "cnt.", data
132
+ )
133
+
134
+ # verify that data is long enough
135
+ if len(self._data) < self._pkglen:
136
+ return
137
+
138
+ if (crc := crc_modbus(self._data[: self._pkglen - 2])) != int.from_bytes(
139
+ self._data[self._pkglen - 2 : self._pkglen], "little"
140
+ ):
141
+ self._log.debug(
142
+ "invalid checksum 0x%X != 0x%X",
143
+ int.from_bytes(self._data[self._pkglen - 2 : self._pkglen], "little"),
144
+ crc,
145
+ )
146
+ self._data = bytearray()
147
+ return
148
+
149
+ if self._data[2] >> 1 not in BMS._CMDS or self._data[1] & 0x80:
150
+ self._log.debug(
151
+ "unknown message: %s, length: %s", self._data[0:2], self._data[2]
152
+ )
153
+ self._data = bytearray()
154
+ return
155
+
156
+ if len(self._data) != self._pkglen:
157
+ self._log.debug(
158
+ "wrong data length (%i!=%s): %s",
159
+ len(self._data),
160
+ self._pkglen,
161
+ self._data,
162
+ )
163
+
164
+ self._data_final[self._data[0] << 8 | self._data[2] >> 1] = self._data
165
+ self._data = bytearray()
166
+ self._data_event.set()
167
+
168
+ async def _init_connection(
169
+ self, char_notify: BleakGATTCharacteristic | int | str | None = None
170
+ ) -> None:
171
+ """Initialize RX/TX characteristics."""
172
+ await super()._init_connection()
173
+ self._pack_count = 0
174
+ self._pkglen = 0
175
+
176
+ @staticmethod
177
+ def _swap32(value: int, signed: bool = False) -> int:
178
+ """Swap high and low 16bit in 32bit integer."""
179
+
180
+ value = ((value >> 16) & 0xFFFF) | (value & 0xFFFF) << 16
181
+ if signed and value & 0x80000000:
182
+ value = -0x100000000 + value
183
+ return value
184
+
185
+ @staticmethod
186
+ def _cmd(device: int, cmd: int, start: int, count: int) -> bytes:
187
+ """Assemble a Seplos BMS command."""
188
+ assert device >= 0x00 and (device <= 0x10 or device in (0xC0, 0xE0))
189
+ assert cmd in (0x01, 0x04) # allow only read commands
190
+ assert start >= 0 and count > 0 and start + count <= 0xFFFF
191
+ frame: bytearray = bytearray([device, cmd])
192
+ frame += int.to_bytes(start, 2, byteorder="big")
193
+ frame += int.to_bytes(count * (0x10 if cmd == 0x1 else 0x1), 2, byteorder="big")
194
+ frame += int.to_bytes(crc_modbus(frame), 2, byteorder="little")
195
+ return bytes(frame)
196
+
197
+ async def _async_update(self) -> BMSsample:
198
+ """Update battery status information."""
199
+ for block in BMS.QUERY.values():
200
+ await self._await_reply(BMS._cmd(0x0, *block))
201
+
202
+ data: BMSsample = BMS._decode_data(
203
+ BMS._FIELDS, self._data_final, offset=BMS.HEAD_LEN
204
+ )
205
+
206
+ self._pack_count = min(data.get("pack_count", 0), 0x10)
207
+
208
+ for pack in range(1, 1 + self._pack_count):
209
+ for block in BMS.PQUERY.values():
210
+ await self._await_reply(self._cmd(pack, *block))
211
+
212
+ for key, idx, sign, func in BMS._PFIELDS:
213
+ data.setdefault(key, []).append(
214
+ func(
215
+ int.from_bytes(
216
+ self._data_final[pack << 8 | BMS.PIA_LEN][
217
+ BMS.HEAD_LEN + idx : BMS.HEAD_LEN + idx + 2
218
+ ],
219
+ byteorder="big",
220
+ signed=sign,
221
+ )
222
+ )
223
+ )
224
+
225
+ pack_cells: list[float] = BMS._cell_voltages(
226
+ self._data_final[pack << 8 | BMS.PIB_LEN], cells=16, start=BMS.HEAD_LEN
227
+ )
228
+ # update per pack delta voltage
229
+ data["delta_voltage"] = max(
230
+ data.get("delta_voltage", 0),
231
+ round(max(pack_cells) - min(pack_cells), 3),
232
+ )
233
+ # add individual cell voltages
234
+ data.setdefault("cell_voltages", []).extend(pack_cells)
235
+ # add temperature sensors (4x cell temperature + 4 reserved)
236
+ data.setdefault("temp_values", []).extend(
237
+ BMS._temp_values(
238
+ self._data_final[pack << 8 | BMS.PIB_LEN],
239
+ values=4,
240
+ start=BMS._TEMP_START,
241
+ signed=False,
242
+ offset=2731,
243
+ divider=10,
244
+ )
245
+ )
246
+
247
+ self._data_final.clear()
248
+
249
+ return data