Skip to content

Commit adc63b3

Browse files
authored
enhance cbtpwr data recovery (#87)
1 parent 9e8e617 commit adc63b3

File tree

2 files changed

+54
-43
lines changed

2 files changed

+54
-43
lines changed

custom_components/bms_ble/plugins/cbtpwr_bms.py

+33-33
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,19 @@
3434
class BMS(BaseBMS):
3535
"""CBT Power Smart BMS class implementation."""
3636

37-
HEAD: Final = bytes([0xAA, 0x55])
38-
TAIL_RX: Final = bytes([0x0D, 0x0A])
39-
TAIL_TX: Final = bytes([0x0A, 0x0D])
40-
MIN_FRAME: Final = len(HEAD) + len(TAIL_RX) + 3 # CMD, LEN, CRC each 1 Byte
41-
CRC_POS: Final = -len(TAIL_RX) - 1
42-
LEN_POS: Final = 3
43-
CMD_POS: Final = 2
37+
HEAD: Final[bytes] = bytes([0xAA, 0x55])
38+
TAIL_RX: Final[bytes] = bytes([0x0D, 0x0A])
39+
TAIL_TX: Final[bytes] = bytes([0x0A, 0x0D])
40+
MIN_FRAME: Final[int] = len(HEAD) + len(TAIL_RX) + 3 # CMD, LEN, CRC each 1 Byte
41+
CRC_POS: Final[int] = -len(TAIL_RX) - 1
42+
LEN_POS: Final[int] = 3
43+
CMD_POS: Final[int] = 2
4444
CELL_VOLTAGE_CMDS: Final[list[int]] = [0x5, 0x6, 0x7, 0x8]
4545

4646
def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
4747
"""Intialize private BMS members."""
4848
super().__init__(LOGGER, self._notification_handler, ble_device, reconnect)
4949
self._data: bytearray = bytearray()
50-
self._data_event = asyncio.Event()
5150
self._FIELDS: Final[
5251
list[tuple[str, int, int, int, bool, Callable[[int], int | float]]]
5352
] = [
@@ -59,6 +58,7 @@ def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
5958
(ATTR_CYCLES, 0x15, 6, 2, False, lambda x: x),
6059
(ATTR_RUNTIME, 0x0C, 14, 2, False, lambda x: float(x * _HRS_TO_SECS / 100)),
6160
]
61+
self._CMDS: Final[list[int]] = list(set(field[1] for field in self._FIELDS))
6262

6363
@staticmethod
6464
def matcher_dict_list() -> list[dict[str, Any]]:
@@ -162,33 +162,33 @@ async def _async_update(self) -> BMSsample:
162162
"""Update battery status information."""
163163
data = {}
164164
resp_cache = {} # variable to avoid multiple queries with same command
165-
for field, cmd, pos, size, sign, fct in self._FIELDS:
166-
LOGGER.debug("(%s) request %s info", self.name, field)
167-
if resp_cache.get(cmd) is None:
168-
await self._client.write_gatt_char(
169-
BMS.uuid_tx(), data=self._gen_frame(cmd.to_bytes(1))
165+
for cmd in self._CMDS:
166+
LOGGER.debug("(%s) request command 0x%X.", self.name, cmd)
167+
await self._client.write_gatt_char(
168+
BMS.uuid_tx(), data=self._gen_frame(cmd.to_bytes(1))
169+
)
170+
try:
171+
await asyncio.wait_for(self._wait_event(), timeout=BAT_TIMEOUT)
172+
except TimeoutError:
173+
continue
174+
if cmd != self._data[self.CMD_POS]:
175+
LOGGER.debug(
176+
"(%s): incorrect response 0x%x to command 0x%x",
177+
self.name,
178+
self._data[self.CMD_POS],
179+
cmd,
170180
)
171-
try:
172-
await asyncio.wait_for(self._wait_event(), timeout=BAT_TIMEOUT)
173-
except TimeoutError:
174-
continue
175-
if cmd != self._data[self.CMD_POS]:
176-
LOGGER.debug(
177-
"(%s): incorrect response 0x%x to command 0x%x",
178-
self.name,
179-
self._data[self.CMD_POS],
180-
cmd,
181-
)
182-
continue
183-
resp_cache[cmd] = self._data.copy()
184-
LOGGER.debug("(%s) %s", self.name, resp_cache)
185-
data |= {
186-
field: fct(
187-
int.from_bytes(
188-
resp_cache[cmd][pos : pos + size], "little", signed=sign
181+
resp_cache[self._data[self.CMD_POS]] = self._data.copy()
182+
183+
for field, cmd, pos, size, sign, fct in self._FIELDS:
184+
if resp_cache.get(cmd):
185+
data |= {
186+
field: fct(
187+
int.from_bytes(
188+
resp_cache[cmd][pos : pos + size], "little", signed=sign
189+
)
189190
)
190-
)
191-
}
191+
}
192192

193193
voltages = {}
194194
for cmd in self.CELL_VOLTAGE_CMDS:

tests/test_cbtpwr_bms.py

+21-10
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ class MockCBTpwrBleakClient(MockBleakClient):
1818
def _response(
1919
self, char_specifier: BleakGATTCharacteristic | int | str | UUID, data: Buffer
2020
) -> bytearray:
21-
if isinstance(char_specifier, str) and normalize_uuid_str(char_specifier) != normalize_uuid_str("ffe9"):
21+
if isinstance(char_specifier, str) and normalize_uuid_str(
22+
char_specifier
23+
) != normalize_uuid_str("ffe9"):
2224
return bytearray()
2325
cmd: int = int(bytearray(data)[2])
2426
assert bytearray(data)[4] == cmd, "incorrect CRC"
@@ -80,7 +82,9 @@ class MockInvalidBleakClient(MockCBTpwrBleakClient):
8082
def _response(
8183
self, char_specifier: BleakGATTCharacteristic | int | str | UUID, data: Buffer
8284
) -> bytearray:
83-
if isinstance(char_specifier, str) and normalize_uuid_str(char_specifier) != normalize_uuid_str("ffe9"):
85+
if isinstance(char_specifier, str) and normalize_uuid_str(
86+
char_specifier
87+
) != normalize_uuid_str("ffe9"):
8488
return bytearray()
8589
cmd: int = int(bytearray(data)[2])
8690
if cmd == 0x0B:
@@ -93,8 +97,8 @@ def _response(
9397
) # wrong CRC
9498
if cmd == 0x0A:
9599
return bytearray(
96-
b"\xAA\x55\x0B\x06\x64\x13\x0D\x00\x00\x00\x95\x0D\x0A"
97-
) # wrong answer to capacity request
100+
b"\xAA\x55\x0B\x08\x58\x34\x00\x00\xBC\xF3\xFF\xFF\x4C\x0D\x0A"
101+
) # wrong answer to capacity request (0xA) with 0xB: voltage, current -> power, charging
98102
return bytearray()
99103

100104
async def disconnect(self) -> bool:
@@ -108,7 +112,9 @@ class MockPartBaseDatBleakClient(MockCBTpwrBleakClient):
108112
def _response(
109113
self, char_specifier: BleakGATTCharacteristic | int | str | UUID, data: Buffer
110114
) -> bytearray:
111-
if isinstance(char_specifier, str) and normalize_uuid_str(char_specifier) != normalize_uuid_str("ffe9"):
115+
if isinstance(char_specifier, str) and normalize_uuid_str(
116+
char_specifier
117+
) != normalize_uuid_str("ffe9"):
112118
return bytearray()
113119
cmd: int = int(bytearray(data)[2])
114120
if cmd == 0x0B:
@@ -125,7 +131,9 @@ class MockAllCellsBleakClient(MockCBTpwrBleakClient):
125131
def _response(
126132
self, char_specifier: BleakGATTCharacteristic | int | str | UUID, data: Buffer
127133
) -> bytearray:
128-
if isinstance(char_specifier, str) and normalize_uuid_str(char_specifier) != normalize_uuid_str("ffe9"):
134+
if isinstance(char_specifier, str) and normalize_uuid_str(
135+
char_specifier
136+
) != normalize_uuid_str("ffe9"):
129137
return bytearray()
130138
cmd: int = int(bytearray(data)[2])
131139
if cmd == 0x05:
@@ -204,9 +212,7 @@ async def test_update(monkeypatch, reconnect_fixture) -> None:
204212

205213
# query again to check already connected state
206214
result = await bms.async_update()
207-
assert (
208-
bms._client.is_connected is not reconnect_fixture
209-
) # noqa: SLF001
215+
assert bms._client.is_connected is not reconnect_fixture # noqa: SLF001
210216

211217
await bms.disconnect()
212218

@@ -227,7 +233,12 @@ async def test_invalid_response(monkeypatch) -> None:
227233
bms = BMS(generate_ble_device("cc:cc:cc:cc:cc:cc", "MockBLEDevice", None, -73))
228234

229235
result = await bms.async_update()
230-
assert result == {}
236+
assert result == {
237+
"battery_charging": False,
238+
"current": -3.14,
239+
"power": -42.076,
240+
"voltage": 13.4,
241+
}
231242

232243
await bms.disconnect()
233244

0 commit comments

Comments
 (0)