Skip to content

Commit 8e9c1b7

Browse files
authored
Feature/branch coverage (#49)
* full branch coverage * full branch coverage * detailed error message * full branch coverage * increased branch coverage * fixed branch coverage for OGT https://coverage.readthedocs.io/en/7.5.4/branch.html#generator-expressions nedbat/coveragepy#1701 * cleaned debug messages * added pack to test uncovered branch * update pytest settings for branch coverage * removed redundant debug message * code formatting
1 parent db2ba15 commit 8e9c1b7

File tree

9 files changed

+145
-150
lines changed

9 files changed

+145
-150
lines changed

CONTRIBUTING.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ In case you have troubles, please enable the debug protocol for the integration
1313
6. Add an appropriate [bluetooth device matcher](https://developers.home-assistant.io/docs/creating_integration_manifest#bluetooth) to `manifest.json`. Note that this is required to match the implementation of `match_dict_list()` in the new BMS class.
1414
7. Test and commit the changes to the branch and create a pull request to the main repository.
1515

16-
Note: in order to keep maintainability of this integration, pull requests are required to pass standard Home Assistant checks for integrations, python linting, and 100% line test coverage.
16+
Note: in order to keep maintainability of this integration, pull requests are required to pass standard Home Assistant checks for integrations, Python linting, and 100% [branch test coverage](https://coverage.readthedocs.io/en/latest/branch.html#branch).
1717

1818
### Any contributions you make will be under the LGPL-2.1 License
1919

custom_components/bms_ble/plugins/daly_bms.py

+32-50
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,13 @@ class BMS(BaseBMS):
4545
CMD_INFO: Final = bytearray(b"\x00\x00\x00\x3E\xD7\xB9")
4646
HEAD_LEN: Final = 3
4747
CRC_LEN: Final = 2
48-
INFO_LEN: Final = 124 + HEAD_LEN + CRC_LEN
48+
MAX_CELLS: Final = 32
49+
MAX_TEMP: Final = 8
50+
INFO_LEN: Final = 84 + HEAD_LEN + CRC_LEN + MAX_CELLS + MAX_TEMP
4951

5052
def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
5153
"""Intialize private BMS members."""
52-
self._reconnect = reconnect
54+
self._reconnect: Final[bool] = reconnect
5355
self._ble_device = ble_device
5456
assert self._ble_device.name is not None
5557
self._client: BleakClient | None = None
@@ -60,17 +62,9 @@ def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
6062
(ATTR_CURRENT, 82 + self.HEAD_LEN, lambda x: float((x - 30000) / 10)),
6163
(ATTR_BATTERY_LEVEL, 84 + self.HEAD_LEN, lambda x: float(x / 10)),
6264
(ATTR_CYCLE_CHRG, 96 + self.HEAD_LEN, lambda x: float(x / 10)),
63-
(
64-
KEY_TEMP_SENS,
65-
100 + self.HEAD_LEN,
66-
lambda x: int(x), # pylint: disable=unnecessary-lambda
67-
),
68-
(KEY_CELL_COUNT, 98 + self.HEAD_LEN, lambda x: x),
69-
(
70-
ATTR_CYCLES,
71-
102 + self.HEAD_LEN,
72-
lambda x: int(x), # pylint: disable=unnecessary-lambda
73-
),
65+
(KEY_CELL_COUNT, 98 + self.HEAD_LEN, lambda x: min(x, self.MAX_CELLS)),
66+
(KEY_TEMP_SENS, 100 + self.HEAD_LEN, lambda x: min(x, self.MAX_TEMP)),
67+
(ATTR_CYCLES, 102 + self.HEAD_LEN, lambda x: x),
7468
(ATTR_DELTA_VOLTAGE, 112 + self.HEAD_LEN, lambda x: float(x / 1000)),
7569
]
7670

@@ -159,52 +153,40 @@ async def async_update(self) -> BMSsample:
159153

160154
data = {
161155
key: func(
162-
int.from_bytes(
163-
self._data[idx : idx + 2],
164-
byteorder="big",
165-
signed=True,
166-
)
156+
int.from_bytes(self._data[idx : idx + 2], byteorder="big", signed=True)
167157
)
168158
for key, idx, func in self._FIELDS
169159
}
170160

171161
# calculate average temperature
172-
if data[KEY_TEMP_SENS] and data[KEY_TEMP_SENS] <= 8:
173-
data[ATTR_TEMPERATURE] = (
174-
fmean(
175-
[
176-
int.from_bytes(
177-
self._data[idx : idx + 2],
178-
byteorder="big",
179-
signed=True,
180-
)
181-
for idx in range(
182-
64 + self.HEAD_LEN,
183-
64 + self.HEAD_LEN + int(data[KEY_TEMP_SENS]) * 2,
184-
2,
185-
)
186-
]
187-
)
188-
- 40
162+
data[ATTR_TEMPERATURE] = (
163+
fmean(
164+
[
165+
int.from_bytes(
166+
self._data[idx : idx + 2], byteorder="big", signed=True
167+
)
168+
for idx in range(
169+
64 + self.HEAD_LEN,
170+
64 + self.HEAD_LEN + int(data[KEY_TEMP_SENS]) * 2,
171+
2,
172+
)
173+
]
189174
)
175+
- 40
176+
)
190177

191178
# get cell voltages
192-
if data[KEY_CELL_COUNT] and data[KEY_CELL_COUNT] <= 32:
193-
data.update(
194-
{
195-
f"{KEY_CELL_VOLTAGE}{idx}": float(
196-
int.from_bytes(
197-
self._data[
198-
self.HEAD_LEN + 2 * idx : self.HEAD_LEN + 2 * idx + 2
199-
],
200-
byteorder="big",
201-
signed=True,
202-
)
203-
/ 1000
204-
)
205-
for idx in range(int(data[KEY_CELL_COUNT]))
206-
}
179+
data |= {
180+
f"{KEY_CELL_VOLTAGE}{idx}": float(
181+
int.from_bytes(
182+
self._data[self.HEAD_LEN + 2 * idx : self.HEAD_LEN + 2 * idx + 2],
183+
byteorder="big",
184+
signed=True,
185+
)
186+
/ 1000
207187
)
188+
for idx in range(int(data[KEY_CELL_COUNT]))
189+
}
208190

209191
self.calc_values(
210192
data, {ATTR_CYCLE_CAP, ATTR_POWER, ATTR_BATTERY_CHARGING, ATTR_RUNTIME}

custom_components/bms_ble/plugins/jbd_bms.py

+40-44
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,20 @@ class BMS(BaseBMS):
4444
HEAD_CMD: Final = bytes([0xDD, 0xA5]) # read header for commands
4545

4646
INFO_LEN: Final = 7 # minimum frame size
47+
BASIC_INFO: Final = 23 # basic info data length
4748

4849
def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
4950
"""Intialize private BMS members."""
50-
self._reconnect = reconnect
51+
self._reconnect: Final[bool] = reconnect
5152
self._ble_device = ble_device
5253
assert self._ble_device.name is not None
5354
self._client: BleakClient | None = None
54-
self._data: bytearray | None = None
55+
self._data: bytearray = bytearray()
5556
self._data_final: bytearray | None = None
5657
self._data_event = asyncio.Event()
57-
self._FIELDS: Final[list[tuple[str, int, int, bool, Callable[[int], int | float]]]] = [
58+
self._FIELDS: Final[
59+
list[tuple[str, int, int, bool, Callable[[int], int | float]]]
60+
] = [
5861
(KEY_TEMP_SENS, 26, 1, False, lambda x: x),
5962
(ATTR_VOLTAGE, 4, 2, False, lambda x: float(x / 100)),
6063
(ATTR_CURRENT, 6, 2, True, lambda x: float(x / 100)),
@@ -97,10 +100,9 @@ def _notification_handler(self, _sender, data: bytearray) -> None:
97100
and (data[1] == 0x03 or data[1] == 0x04)
98101
and data[2] == 0x00
99102
):
100-
self._data = data
101-
elif len(data) and self._data is not None:
102-
self._data += data
103+
self._data.clear()
103104

105+
self._data += data
104106
LOGGER.debug(
105107
"(%s) Rx BLE data (%s): %s",
106108
self._ble_device.name,
@@ -116,9 +118,8 @@ def _notification_handler(self, _sender, data: bytearray) -> None:
116118
):
117119
return
118120

119-
frame_end: int = self.INFO_LEN + self._data[3] - 1
120-
121-
crc = self._crc(self._data[2 : frame_end - 2])
121+
frame_end: Final[int] = self.INFO_LEN + self._data[3] - 1
122+
crc: Final[int] = self._crc(self._data[2 : frame_end - 2])
122123
if int.from_bytes(self._data[frame_end - 2 : frame_end], "big") != crc:
123124
LOGGER.debug(
124125
"(%s) Rx data CRC is invalid: %i != %i",
@@ -178,20 +179,15 @@ def _decode_data(self, data: bytearray) -> dict[str, int | float]:
178179
}
179180

180181
# calculate average temperature
181-
if result[KEY_TEMP_SENS]:
182-
result[ATTR_TEMPERATURE] = (
183-
fmean(
184-
[
185-
int.from_bytes(data[idx : idx + 2], byteorder="big")
186-
for idx in range(
187-
27,
188-
27 + int(result[KEY_TEMP_SENS]) * 2,
189-
2,
190-
)
191-
]
192-
)
193-
- 2731
194-
) / 10
182+
result[ATTR_TEMPERATURE] = (
183+
fmean(
184+
[
185+
int.from_bytes(data[idx : idx + 2], byteorder="big")
186+
for idx in range(27, 27 + int(result[KEY_TEMP_SENS]) * 2, 2)
187+
]
188+
)
189+
- 2731
190+
) / 10
195191

196192
return result
197193

@@ -211,28 +207,28 @@ async def async_update(self) -> BMSsample:
211207
await self._connect()
212208
assert self._client is not None
213209

214-
# query basic info
215-
await self._client.write_gatt_char(UUID_TX, data=self._cmd(b"\x03"))
216-
await asyncio.wait_for(self._wait_event(), timeout=BAT_TIMEOUT)
217-
218-
if self._data_final is None:
219-
return {}
220-
if len(self._data_final) != self.INFO_LEN + self._data_final[3]:
221-
LOGGER.debug(
222-
"(%s) Wrong data length (%i): %s",
223-
self._ble_device.name,
224-
len(self._data_final),
225-
self._data_final,
226-
)
227-
228-
data = self._decode_data(self._data_final)
229-
230-
# query cell block info
231-
await self._client.write_gatt_char(UUID_TX, data=self._cmd(b"\x04"))
232-
await asyncio.wait_for(self._wait_event(), timeout=BAT_TIMEOUT)
210+
data = {}
211+
for cmd, exp_len, dec_fct in [
212+
(self._cmd(b"\x03"), self.BASIC_INFO, self._decode_data),
213+
(self._cmd(b"\x04"), 0, self._cell_voltages),
214+
]:
215+
await self._client.write_gatt_char(UUID_TX, data=cmd)
216+
await asyncio.wait_for(self._wait_event(), timeout=BAT_TIMEOUT)
217+
218+
if self._data_final is None:
219+
continue
220+
if (
221+
len(self._data_final) != self.INFO_LEN + self._data_final[3]
222+
or len(self._data_final) < self.INFO_LEN + exp_len
223+
):
224+
LOGGER.debug(
225+
"(%s) Wrong data length (%i): %s",
226+
self._ble_device.name,
227+
len(self._data_final),
228+
self._data_final,
229+
)
233230

234-
if self._data_final is not None:
235-
data.update(self._cell_voltages(self._data_final))
231+
data.update(dec_fct(self._data_final))
236232

237233
self.calc_values(
238234
data,

custom_components/bms_ble/plugins/jikong_bms.py

+8-7
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
5050
self._ble_device = ble_device
5151
assert self._ble_device.name is not None
5252
self._client: BleakClient | None = None
53-
self._data: bytearray | None = None
53+
self._data: bytearray = bytearray()
5454
self._data_final: bytearray | None = None
5555
self._data_event = asyncio.Event()
5656
self._char_write_handle: int | None = None
@@ -103,9 +103,9 @@ def _notification_handler(self, _sender, data: bytearray) -> None:
103103
data = data[len(self.BT_MODULE_MSG) :]
104104

105105
if data[0 : len(self.HEAD_RSP)] == self.HEAD_RSP:
106-
self._data = data
107-
elif len(data) and self._data is not None:
108-
self._data += data
106+
self._data.clear()
107+
108+
self._data += data
109109

110110
LOGGER.debug(
111111
"(%s) Rx BLE data (%s): %s",
@@ -171,7 +171,9 @@ async def _connect(self) -> None:
171171
"(%s) Failed to detect characteristics", self._ble_device.name
172172
)
173173
await self._client.disconnect()
174-
raise ConnectionError(f"Unable to connect to {self._ble_device.name}.")
174+
raise ConnectionError(
175+
f"Failed to detect characteristics from {self._ble_device.name}."
176+
)
175177
LOGGER.debug(
176178
"(%s) Using characteristics handle #%i (notify), #%i (write)",
177179
self._ble_device.name,
@@ -206,8 +208,7 @@ def _crc(self, frame: bytes) -> int:
206208

207209
def _cmd(self, cmd: bytes, value: list[int] | None = None) -> bytes:
208210
"""Assemble a Jikong BMS command."""
209-
if value is None:
210-
value = []
211+
value = [] if value is None else value
211212
assert len(value) <= 13
212213
frame = bytes([*self.HEAD_CMD, cmd[0]])
213214
frame += bytes([len(value), *value])

custom_components/bms_ble/plugins/ogt_bms.py

+5-8
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,10 @@ def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None:
9393
23: (ATTR_CYCLES, 2, None),
9494
}
9595
# add cell voltage registers, note: need to be last!
96-
self._REGISTERS.update(
97-
{
98-
63
99-
- reg: (f"{KEY_CELL_VOLTAGE}{reg+1}", 2, lambda x: float(x) / 1000)
100-
for reg in range(16)
101-
}
102-
)
96+
self._REGISTERS |= { # pragma: no branch
97+
63 - reg: (f"{KEY_CELL_VOLTAGE}{reg+1}", 2, lambda x: float(x) / 1000)
98+
for reg in range(16)
99+
}
103100
self._HEADER = "+R16"
104101
else:
105102
self._REGISTERS = {}
@@ -154,7 +151,7 @@ async def async_update(self) -> BMSsample:
154151
)
155152

156153
# remove remaining runtime if battery is charging
157-
if self._values.get(ATTR_RUNTIME) == 0xFFFF*60:
154+
if self._values.get(ATTR_RUNTIME) == 0xFFFF * 60:
158155
del self._values[ATTR_RUNTIME]
159156

160157
if self._reconnect:

0 commit comments

Comments
 (0)