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,237 @@
1
+ """Module to support E&J Technology BMS.
2
+
3
+ Project: aiobmsble, https://pypi.org/p/aiobmsble/
4
+ License: Apache-2.0, http://www.apache.org/licenses/
5
+ """
6
+
7
+ from enum import IntEnum
8
+ from string import hexdigits
9
+ from typing import Final, Literal
10
+
11
+ from bleak.backends.characteristic import BleakGATTCharacteristic
12
+ from bleak.backends.device import BLEDevice
13
+
14
+ from aiobmsble import BMSdp, BMSsample, BMSvalue, MatcherPattern
15
+ from aiobmsble.basebms import BaseBMS
16
+
17
+
18
+ class Cmd(IntEnum):
19
+ """BMS operation codes."""
20
+
21
+ RT = 0x2
22
+ CAP = 0x10
23
+
24
+
25
+ class BMS(BaseBMS):
26
+ """E&J Technology BMS implementation."""
27
+
28
+ _BT_MODULE_MSG: Final[bytes] = bytes([0x41, 0x54, 0x0D, 0x0A]) # BLE module message
29
+ _IGNORE_CRC: Final[str] = "libattU"
30
+ _HEAD: Final[bytes] = b"\x3a"
31
+ _TAIL: Final[bytes] = b"\x7e"
32
+ _MAX_CELLS: Final[int] = 16
33
+ _FIELDS: Final[tuple[BMSdp, ...]] = (
34
+ BMSdp(
35
+ "current", 89, 8, False, lambda x: ((x >> 16) - (x & 0xFFFF)) / 100, Cmd.RT
36
+ ),
37
+ BMSdp("battery_level", 123, 2, False, lambda x: x, Cmd.RT),
38
+ BMSdp("cycle_charge", 15, 4, False, lambda x: x / 10, Cmd.CAP),
39
+ BMSdp(
40
+ "temperature", 97, 2, False, lambda x: x - 40, Cmd.RT
41
+ ), # only 1st sensor relevant
42
+ BMSdp("cycles", 115, 4, False, lambda x: x, Cmd.RT),
43
+ BMSdp(
44
+ "problem_code", 105, 4, False, lambda x: x & 0x0FFC, Cmd.RT
45
+ ), # mask status bits
46
+ )
47
+
48
+ def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
49
+ """Initialize BMS."""
50
+ super().__init__(ble_device, reconnect)
51
+ self._data_final: bytearray = bytearray()
52
+
53
+ @staticmethod
54
+ def matcher_dict_list() -> list[MatcherPattern]:
55
+ """Provide BluetoothMatcher definition."""
56
+ return (
57
+ [ # Lithtech Energy (2x), Volthium
58
+ MatcherPattern(local_name=pattern, connectable=True)
59
+ for pattern in ("L-12V???AH-*", "LT-12V-*", "V-12V???Ah-*")
60
+ ]
61
+ + [ # Fliteboard, Electronix battery
62
+ {
63
+ "local_name": "libatt*",
64
+ "manufacturer_id": 21320,
65
+ "connectable": True,
66
+ },
67
+ {"local_name": "SV12V*", "manufacturer_id": 33384, "connectable": True},
68
+ {"local_name": "LT-24*", "manufacturer_id": 22618, "connectable": True},
69
+ ]
70
+ + [ # LiTime
71
+ MatcherPattern( # LiTime based on ser#
72
+ local_name="LT-12???BG-A0[0-6]*",
73
+ manufacturer_id=m_id,
74
+ connectable=True,
75
+ )
76
+ for m_id in (33384, 22618)
77
+ ]
78
+ )
79
+
80
+ @staticmethod
81
+ def device_info() -> dict[str, str]:
82
+ """Return device information for the battery management system."""
83
+ return {"manufacturer": "E&J Technology", "model": "Smart BMS"}
84
+
85
+ @staticmethod
86
+ def uuid_services() -> list[str]:
87
+ """Return list of 128-bit UUIDs of services required by BMS."""
88
+ return ["6e400001-b5a3-f393-e0a9-e50e24dcca9e"]
89
+
90
+ @staticmethod
91
+ def uuid_rx() -> str:
92
+ """Return 128-bit UUID of characteristic that provides notification/read property."""
93
+ return "6e400003-b5a3-f393-e0a9-e50e24dcca9e"
94
+
95
+ @staticmethod
96
+ def uuid_tx() -> str:
97
+ """Return 128-bit UUID of characteristic that provides write property."""
98
+ return "6e400002-b5a3-f393-e0a9-e50e24dcca9e"
99
+
100
+ @staticmethod
101
+ def _calc_values() -> frozenset[BMSvalue]:
102
+ return frozenset(
103
+ {
104
+ "battery_charging",
105
+ "cycle_capacity",
106
+ "delta_voltage",
107
+ "power",
108
+ "runtime",
109
+ "voltage",
110
+ }
111
+ ) # calculate further values from BMS provided set ones
112
+
113
+ def _notification_handler(
114
+ self, _sender: BleakGATTCharacteristic, data: bytearray
115
+ ) -> None:
116
+ """Handle the RX characteristics notify event (new data arrives)."""
117
+
118
+ if data.startswith(BMS._BT_MODULE_MSG):
119
+ self._log.debug("filtering AT cmd")
120
+ if not (data := data.removeprefix(BMS._BT_MODULE_MSG)):
121
+ return
122
+
123
+ if data.startswith(BMS._HEAD): # check for beginning of frame
124
+ self._data.clear()
125
+
126
+ self._data += data
127
+
128
+ self._log.debug(
129
+ "RX BLE data (%s): %s", "start" if data == self._data else "cnt.", data
130
+ )
131
+
132
+ exp_frame_len: Final[int] = (
133
+ int(self._data[7:11], 16)
134
+ if len(self._data) > 10
135
+ and all(chr(c) in hexdigits for c in self._data[7:11])
136
+ else 0xFFFF
137
+ )
138
+
139
+ if not self._data.startswith(BMS._HEAD) or (
140
+ not self._data.endswith(BMS._TAIL) and len(self._data) < exp_frame_len
141
+ ):
142
+ return
143
+
144
+ if not self._data.endswith(BMS._TAIL):
145
+ self._log.debug("incorrect EOF: %s", data)
146
+ self._data.clear()
147
+ return
148
+
149
+ if not all(chr(c) in hexdigits for c in self._data[1:-1]):
150
+ self._log.debug("incorrect frame encoding.")
151
+ self._data.clear()
152
+ return
153
+
154
+ if len(self._data) != exp_frame_len:
155
+ self._log.debug(
156
+ "incorrect frame length %i != %i",
157
+ len(self._data),
158
+ exp_frame_len,
159
+ )
160
+ self._data.clear()
161
+ return
162
+
163
+ if not self.name.startswith(BMS._IGNORE_CRC) and (
164
+ crc := BMS._crc(self._data[1:-3])
165
+ ) != int(self._data[-3:-1], 16):
166
+ # libattU firmware uses no CRC, so we ignore it
167
+ self._log.debug(
168
+ "invalid checksum 0x%X != 0x%X", int(self._data[-3:-1], 16), crc
169
+ )
170
+ self._data.clear()
171
+ return
172
+
173
+ self._log.debug(
174
+ "address: 0x%X, command 0x%X, version: 0x%X, length: 0x%X",
175
+ int(self._data[1:3], 16),
176
+ int(self._data[3:5], 16) & 0x7F,
177
+ int(self._data[5:7], 16),
178
+ len(self._data),
179
+ )
180
+ self._data_final = self._data.copy()
181
+ self._data_event.set()
182
+
183
+ @staticmethod
184
+ def _crc(data: bytearray) -> int:
185
+ return (sum(data) ^ 0xFF) & 0xFF
186
+
187
+ @staticmethod
188
+ def _cell_voltages(
189
+ data: bytearray,
190
+ *,
191
+ cells: int,
192
+ start: int,
193
+ size: int = 2,
194
+ byteorder: Literal["little", "big"] = "big",
195
+ divider: int = 1000,
196
+ ) -> list[float]:
197
+ """Return cell voltages from status message."""
198
+ return [
199
+ (value / divider)
200
+ for idx in range(cells)
201
+ if (value := int(data[start + size * idx : start + size * (idx + 1)], 16))
202
+ ]
203
+
204
+ @staticmethod
205
+ def _conv_data(data: dict[int, bytearray]) -> BMSsample:
206
+ result: BMSsample = {}
207
+ for field in BMS._FIELDS:
208
+ result[field.key] = field.fct(
209
+ int(data[field.idx][field.pos : field.pos + field.size], 16)
210
+ )
211
+ return result
212
+
213
+ async def _async_update(self) -> BMSsample:
214
+ """Update battery status information."""
215
+ raw_data: dict[int, bytearray] = {}
216
+
217
+ # query real-time information and capacity
218
+ for cmd in (b":000250000E03~", b":001031000E05~"):
219
+ await self._await_reply(cmd)
220
+ rsp: int = int(self._data_final[3:5], 16) & 0x7F
221
+ raw_data[rsp] = self._data_final
222
+ if rsp == Cmd.RT and len(self._data_final) == 0x8C:
223
+ # handle metrisun version
224
+ self._log.debug("single frame protocol detected")
225
+ raw_data[Cmd.CAP] = bytearray(15) + self._data_final[125:]
226
+ break
227
+
228
+ if len(raw_data) != len(list(Cmd)) or not all(
229
+ len(value) > 0 for value in raw_data.values()
230
+ ):
231
+ return {}
232
+
233
+ return self._conv_data(raw_data) | {
234
+ "cell_voltages": BMS._cell_voltages(
235
+ raw_data[Cmd.RT], cells=BMS._MAX_CELLS, start=25, size=4
236
+ )
237
+ }
@@ -0,0 +1,143 @@
1
+ """Module to support Felicity 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 json import JSONDecodeError, loads
9
+ from typing import Any, Final
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
17
+
18
+
19
+ class BMS(BaseBMS):
20
+ """Felicity BMS implementation."""
21
+
22
+ _HEAD: Final[bytes] = b"{"
23
+ _TAIL: Final[bytes] = b"}"
24
+ _CMD_PRE: Final[bytes] = b"wifilocalMonitor:" # CMD prefix
25
+ _CMD_BI: Final[bytes] = b"get dev basice infor"
26
+ _CMD_DT: Final[bytes] = b"get Date"
27
+ _CMD_RT: Final[bytes] = b"get dev real infor"
28
+ _FIELDS: Final[list[tuple[BMSvalue, str, Callable[[list], Any]]]] = [
29
+ ("voltage", "Batt", lambda x: x[0][0] / 1000),
30
+ ("current", "Batt", lambda x: x[1][0] / 10),
31
+ (
32
+ "cycle_charge",
33
+ "BatsocList",
34
+ lambda x: (int(x[0][0]) * int(x[0][2])) / 1e7,
35
+ ),
36
+ ("battery_level", "BatsocList", lambda x: x[0][0] / 100),
37
+ ]
38
+
39
+ def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
40
+ """Initialize BMS."""
41
+ super().__init__(ble_device, reconnect)
42
+ self._data_final: dict = {}
43
+
44
+ @staticmethod
45
+ def matcher_dict_list() -> list[MatcherPattern]:
46
+ """Provide BluetoothMatcher definition."""
47
+ return [
48
+ {"local_name": pattern, "connectable": True} for pattern in ("F07*", "F10*")
49
+ ]
50
+
51
+ @staticmethod
52
+ def device_info() -> dict[str, str]:
53
+ """Return device information for the battery management system."""
54
+ return {"manufacturer": "Felicity Solar", "model": "LiFePo4 battery"}
55
+
56
+ @staticmethod
57
+ def uuid_services() -> list[str]:
58
+ """Return list of 128-bit UUIDs of services required by BMS."""
59
+ return [normalize_uuid_str("6e6f736a-4643-4d44-8fa9-0fafd005e455")]
60
+
61
+ @staticmethod
62
+ def uuid_rx() -> str:
63
+ """Return 128-bit UUID of characteristic that provides notification/read property."""
64
+ return "49535458-8341-43f4-a9d4-ec0e34729bb3"
65
+
66
+ @staticmethod
67
+ def uuid_tx() -> str:
68
+ """Return 128-bit UUID of characteristic that provides write property."""
69
+ return "49535258-184d-4bd9-bc61-20c647249616"
70
+
71
+ @staticmethod
72
+ def _calc_values() -> frozenset[BMSvalue]:
73
+ return frozenset(
74
+ {
75
+ "battery_charging",
76
+ "cycle_capacity",
77
+ "delta_voltage",
78
+ "power",
79
+ "runtime",
80
+ "temperature",
81
+ }
82
+ ) # calculate further values from BMS provided set ones
83
+
84
+ def _notification_handler(
85
+ self, _sender: BleakGATTCharacteristic, data: bytearray
86
+ ) -> None:
87
+ """Handle the RX characteristics notify event (new data arrives)."""
88
+
89
+ if data.startswith(BMS._HEAD):
90
+ self._data = bytearray()
91
+
92
+ self._data += data
93
+ self._log.debug(
94
+ "RX BLE data (%s): %s", "start" if data == self._data else "cnt.", data
95
+ )
96
+
97
+ if not data.endswith(BMS._TAIL):
98
+ return
99
+
100
+ try:
101
+ self._data_final = loads(self._data)
102
+ except (JSONDecodeError, UnicodeDecodeError):
103
+ self._log.debug("JSON decode error: %s", self._data)
104
+ return
105
+
106
+ if (ver := self._data_final.get("CommVer", 0)) != 1:
107
+ self._log.debug("Unknown protocol version (%i)", ver)
108
+ return
109
+
110
+ self._data_event.set()
111
+
112
+ @staticmethod
113
+ def _conv_data(data: dict) -> BMSsample:
114
+ result: BMSsample = {}
115
+ for key, itm, func in BMS._FIELDS:
116
+ result[key] = func(data.get(itm, []))
117
+ return result
118
+
119
+ @staticmethod
120
+ def _conv_cells(data: dict) -> list[float]:
121
+ return [(value / 1000) for value in data.get("BatcelList", [])[0]]
122
+
123
+ @staticmethod
124
+ def _conv_temp(data: dict) -> list[float]:
125
+ return [
126
+ (value / 10) for value in data.get("BtemList", [])[0] if value != 0x7FFF
127
+ ]
128
+
129
+ async def _async_update(self) -> BMSsample:
130
+ """Update battery status information."""
131
+
132
+ await self._await_reply(BMS._CMD_PRE + BMS._CMD_RT)
133
+
134
+ return (
135
+ BMS._conv_data(self._data_final)
136
+ | {"temp_values": BMS._conv_temp(self._data_final)}
137
+ | {"cell_voltages": BMS._conv_cells(self._data_final)}
138
+ | {
139
+ "problem_code": int(
140
+ self._data_final.get("Bwarn", 0) + self._data_final.get("Bfault", 0)
141
+ )
142
+ }
143
+ )
@@ -0,0 +1,207 @@
1
+ """Module to support JBD 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 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
+ """JBD Smart BMS class implementation."""
19
+
20
+ HEAD_RSP: Final[bytes] = bytes([0xDD]) # header for responses
21
+ HEAD_CMD: Final[bytes] = bytes([0xDD, 0xA5]) # read header for commands
22
+ TAIL: Final[int] = 0x77 # tail for command
23
+ INFO_LEN: Final[int] = 7 # minimum frame size
24
+ BASIC_INFO: Final[int] = 23 # basic info data length
25
+ _FIELDS: Final[tuple[BMSdp, ...]] = (
26
+ BMSdp("temp_sensors", 26, 1, False, lambda x: x), # count is not limited
27
+ BMSdp("voltage", 4, 2, False, lambda x: x / 100),
28
+ BMSdp("current", 6, 2, True, lambda x: x / 100),
29
+ BMSdp("battery_level", 23, 1, False, lambda x: x),
30
+ BMSdp("cycle_charge", 8, 2, False, lambda x: x / 100),
31
+ BMSdp("cycles", 12, 2, False, lambda x: x),
32
+ BMSdp("problem_code", 20, 2, False, lambda x: x),
33
+ ) # general protocol v4
34
+
35
+ def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
36
+ """Intialize private BMS members."""
37
+ super().__init__(ble_device, reconnect)
38
+ self._valid_reply: int = 0x00
39
+ self._data_final: bytearray = bytearray()
40
+
41
+ @staticmethod
42
+ def matcher_dict_list() -> list[MatcherPattern]:
43
+ """Provide BluetoothMatcher definition."""
44
+ return [
45
+ MatcherPattern(
46
+ local_name=pattern,
47
+ service_uuid=BMS.uuid_services()[0],
48
+ connectable=True,
49
+ )
50
+ for pattern in (
51
+ "JBD-*",
52
+ "SP0?S*",
53
+ "SP1?S*",
54
+ "SP2?S*",
55
+ "AP2?S*",
56
+ "GJ-*", # accurat batteries
57
+ "SX1*", # Supervolt v3
58
+ "DP04S*", # ECO-WORTHY, DCHOUSE
59
+ "ECO-LFP*", # ECO-WORTHY rack (use m_id?)
60
+ "121?0*", # Eleksol, Ultimatron
61
+ "12200*",
62
+ "12300*",
63
+ "SBL-*", # SBL
64
+ "LT40AH", # LionTron
65
+ "PKT*", # Perfektium
66
+ "gokwh*",
67
+ "OGR-*", # OGRPHY
68
+ "DWC*", # Vatrer
69
+ "DXD*", # Vatrer
70
+ "xiaoxiang*", # xiaoxiang BMS
71
+ "AL12-*", # Aolithium
72
+ "BS20*", # BasenGreen
73
+ "BT LP*", # LANPWR
74
+ )
75
+ ] + [
76
+ MatcherPattern(
77
+ service_uuid=BMS.uuid_services()[0],
78
+ manufacturer_id=m_id,
79
+ connectable=True,
80
+ )
81
+ for m_id in (0x0211, 0x3E70, 0xC1A4)
82
+ # Liontron, LISMART1240LX/LISMART1255LX,
83
+ # LionTron XL19110253 / EPOCH batteries 12.8V 460Ah - 12460A-H
84
+ ]
85
+
86
+ @staticmethod
87
+ def device_info() -> dict[str, str]:
88
+ """Return device information for the battery management system."""
89
+ return {"manufacturer": "Jiabaida", "model": "Smart BMS"}
90
+
91
+ @staticmethod
92
+ def uuid_services() -> list[str]:
93
+ """Return list of 128-bit UUIDs of services required by BMS."""
94
+ return [normalize_uuid_str("ff00")]
95
+
96
+ @staticmethod
97
+ def uuid_rx() -> str:
98
+ """Return 16-bit UUID of characteristic that provides notification/read property."""
99
+ return "ff01"
100
+
101
+ @staticmethod
102
+ def uuid_tx() -> str:
103
+ """Return 16-bit UUID of characteristic that provides write property."""
104
+ return "ff02"
105
+
106
+ @staticmethod
107
+ def _calc_values() -> frozenset[BMSvalue]:
108
+ return frozenset(
109
+ {
110
+ "power",
111
+ "battery_charging",
112
+ "cycle_capacity",
113
+ "runtime",
114
+ "delta_voltage",
115
+ "temperature",
116
+ }
117
+ )
118
+
119
+ def _notification_handler(
120
+ self, _sender: BleakGATTCharacteristic, data: bytearray
121
+ ) -> None:
122
+ # check if answer is a heading of basic info (0x3) or cell block info (0x4)
123
+ if (
124
+ data.startswith(self.HEAD_RSP)
125
+ and len(self._data) > self.INFO_LEN
126
+ and data[1] in (0x03, 0x04)
127
+ and data[2] == 0x00
128
+ and len(self._data) >= self.INFO_LEN + self._data[3]
129
+ ):
130
+ self._data = bytearray()
131
+
132
+ self._data += data
133
+ self._log.debug(
134
+ "RX BLE data (%s): %s", "start" if data == self._data else "cnt.", data
135
+ )
136
+
137
+ # verify that data is long enough
138
+ if (
139
+ len(self._data) < BMS.INFO_LEN
140
+ or len(self._data) < BMS.INFO_LEN + self._data[3]
141
+ ):
142
+ return
143
+
144
+ # check correct frame ending
145
+ frame_end: Final[int] = BMS.INFO_LEN + self._data[3] - 1
146
+ if self._data[frame_end] != BMS.TAIL:
147
+ self._log.debug("incorrect frame end (length: %i).", len(self._data))
148
+ return
149
+
150
+ if (crc := BMS._crc(self._data[2 : frame_end - 2])) != int.from_bytes(
151
+ self._data[frame_end - 2 : frame_end], "big"
152
+ ):
153
+ self._log.debug(
154
+ "invalid checksum 0x%X != 0x%X",
155
+ int.from_bytes(self._data[frame_end - 2 : frame_end], "big"),
156
+ crc,
157
+ )
158
+ return
159
+
160
+ if len(self._data) != BMS.INFO_LEN + self._data[3]:
161
+ self._log.debug("wrong data length (%i): %s", len(self._data), self._data)
162
+
163
+ if self._data[1] != self._valid_reply:
164
+ self._log.debug("unexpected response (type 0x%X)", self._data[1])
165
+ return
166
+
167
+ self._data_final = self._data
168
+ self._data_event.set()
169
+
170
+ @staticmethod
171
+ def _crc(frame: bytearray) -> int:
172
+ """Calculate JBD frame CRC."""
173
+ return 0x10000 - sum(frame)
174
+
175
+ @staticmethod
176
+ def _cmd(cmd: bytes) -> bytes:
177
+ """Assemble a JBD BMS command."""
178
+ frame = bytearray([*BMS.HEAD_CMD, cmd[0], 0x00])
179
+ frame.extend([*BMS._crc(frame[2:4]).to_bytes(2, "big"), BMS.TAIL])
180
+ return bytes(frame)
181
+
182
+ async def _await_cmd_resp(self, cmd: int) -> None:
183
+ msg: Final[bytes] = BMS._cmd(bytes([cmd]))
184
+ self._valid_reply = msg[2]
185
+ await self._await_reply(msg)
186
+ self._valid_reply = 0x00
187
+
188
+ async def _async_update(self) -> BMSsample:
189
+ """Update battery status information."""
190
+ data: BMSsample = {}
191
+ await self._await_cmd_resp(0x03)
192
+ data = BMS._decode_data(BMS._FIELDS, self._data_final)
193
+ data["temp_values"] = BMS._temp_values(
194
+ self._data_final,
195
+ values=data.get("temp_sensors", 0),
196
+ start=27,
197
+ signed=False,
198
+ offset=2731,
199
+ divider=10,
200
+ )
201
+
202
+ await self._await_cmd_resp(0x04)
203
+ data["cell_voltages"] = BMS._cell_voltages(
204
+ self._data_final, cells=self._data_final[3] // 2, start=4, byteorder="big"
205
+ )
206
+
207
+ return data