From 711de54bf2336b360dbdebe0d242376c6f02ee5a Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Thu, 27 Feb 2025 14:41:34 +0100 Subject: [PATCH 01/11] fix daly problem test case --- custom_components/bms_ble/plugins/daly_bms.py | 2 +- tests/test_daly_bms.py | 22 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/custom_components/bms_ble/plugins/daly_bms.py b/custom_components/bms_ble/plugins/daly_bms.py index cc0c44b..4c84c40 100644 --- a/custom_components/bms_ble/plugins/daly_bms.py +++ b/custom_components/bms_ble/plugins/daly_bms.py @@ -50,7 +50,7 @@ class BMS(BaseBMS): (KEY_TEMP_SENS, 100 + HEAD_LEN, 2, lambda x: min(x, BMS.MAX_TEMP)), (ATTR_CYCLES, 102 + HEAD_LEN, 2, lambda x: x), (ATTR_DELTA_VOLTAGE, 112 + HEAD_LEN, 2, lambda x: float(x / 1000)), - (KEY_PROBLEM, 116 + HEAD_LEN, 8, lambda x: x), + (KEY_PROBLEM, 116 + HEAD_LEN, 8, lambda x: x % 2**64), ] def __init__(self, ble_device: BLEDevice, reconnect: bool = False) -> None: diff --git a/tests/test_daly_bms.py b/tests/test_daly_bms.py index f30e72e..a6861d0 100644 --- a/tests/test_daly_bms.py +++ b/tests/test_daly_bms.py @@ -221,7 +221,6 @@ async def test_invalid_response(monkeypatch, wrong_response) -> None: await bms.disconnect() - @pytest.fixture( name="problem_response", params=[ @@ -232,8 +231,8 @@ async def test_invalid_response(monkeypatch, wrong_response) -> None: b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x3c\x00\x3d\x00\x3e\x00\x3f\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x8c\x75\x4e\x03\x84\x10\x3d\x10\x1f\x00\x00\x00\x00\x00\x00\x0d" - b"\x80\x00\x04\x00\x04\x00\x39\x00\x01\x00\x00\x00\x01\x10\x2e\x01\x41\x00\x2a\x01" - b"\x00\x00\x00\x00\x00\x00\x00\x61\x13" + b"\x80\x00\x04\x00\x04\x00\x39\x00\x01\x00\x00\x00\x01\x10\x2e\x01\x41\x00\x2a\x00" + b"\x00\x00\x00\x00\x00\x00\x01\x61\x1f" ), "first_bit", ), @@ -244,8 +243,8 @@ async def test_invalid_response(monkeypatch, wrong_response) -> None: b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x3c\x00\x3d\x00\x3e\x00\x3f\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x8c\x75\x4e\x03\x84\x10\x3d\x10\x1f\x00\x00\x00\x00\x00\x00\x0d" - b"\x80\x00\x04\x00\x04\x00\x39\x00\x01\x00\x00\x00\x01\x10\x2e\x01\x41\x00\x2a\x00" - b"\x00\x00\x00\x00\x00\x00\x80\xa1\x7f" + b"\x80\x00\x04\x00\x04\x00\x39\x00\x01\x00\x00\x00\x01\x10\x2e\x01\x41\x00\x2a\x80" + b"\x00\x00\x00\x00\x00\x00\x00\xA8\xBF" ), "last_bit", ), @@ -254,15 +253,17 @@ async def test_invalid_response(monkeypatch, wrong_response) -> None: ) def prb_response(request): """Return faulty response frame.""" - return request.param[0] + return request.param -async def test_problem_response(monkeypatch, problem_response) -> None: +async def test_problem_response( + monkeypatch, problem_response: tuple[bytearray, str] +) -> None: """Test data update with BMS returning error flags.""" monkeypatch.setattr( "tests.test_daly_bms.MockDalyBleakClient._response", - lambda _s, _c, _d: problem_response, + lambda _s, _c, _d: problem_response[0], ) monkeypatch.setattr( @@ -273,6 +274,9 @@ async def test_problem_response(monkeypatch, problem_response) -> None: bms = BMS(generate_ble_device("cc:cc:cc:cc:cc:cc", "MockBLEdevice", None, -73)) result: BMSsample = await bms.async_update() - assert result.get("problem", False) # we expect a problem + assert result.get("problem", False) # we expect a problem + assert result.get("problem_code", 0) == ( + 1 << (0 if problem_response[1] == "first_bit" else 63) + ) await bms.disconnect() From 7bed79aa2f7a442cae7319ef625097d6384f3437 Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Thu, 27 Feb 2025 14:41:58 +0100 Subject: [PATCH 02/11] fix problem test cases --- tests/test_ective_bms.py | 2 +- tests/test_ej_bms.py | 6 ++---- tests/test_jikong_bms.py | 2 +- tests/test_redodo_bms.py | 2 +- tests/test_tdt_bms.py | 36 +++++++++++++++++------------------- 5 files changed, 22 insertions(+), 26 deletions(-) diff --git a/tests/test_ective_bms.py b/tests/test_ective_bms.py index 77ec28c..be8b770 100644 --- a/tests/test_ective_bms.py +++ b/tests/test_ective_bms.py @@ -268,7 +268,7 @@ async def test_problem_response(monkeypatch, problem_response) -> None: "runtime": 53961, "battery_charging": False, "problem": True, - "problem_code": 1 if problem_response[1] == "first_bit" else 128, + "problem_code": (1 if problem_response[1] == "first_bit" else 128), } await bms.disconnect() diff --git a/tests/test_ej_bms.py b/tests/test_ej_bms.py index 3c9d95d..0f85000 100644 --- a/tests/test_ej_bms.py +++ b/tests/test_ej_bms.py @@ -277,10 +277,8 @@ async def test_problem_response(monkeypatch, problem_response) -> None: result: BMSsample = await bms.async_update() assert result.get("problem", False) # we expect a problem - assert ( - result.get("problem_code", 0) == 0x4 - if problem_response[0] == "first_bit" - else 0x800 + assert result.get("problem_code", 0) == ( + 0x4 if problem_response[1] == "first_bit" else 0x800 ) await bms.disconnect() diff --git a/tests/test_jikong_bms.py b/tests/test_jikong_bms.py index 7f1d467..5ced866 100644 --- a/tests/test_jikong_bms.py +++ b/tests/test_jikong_bms.py @@ -718,7 +718,7 @@ def frame_update(data: bytearray, update: bytearray, pos: int) -> None: assert await bms.async_update() == _RESULT_DEFS[protocol_type] | { "problem": True, - "problem_code": 1 << 0 if problem_response[1] == "first_bit" else 1 << 15, + "problem_code": 1 << (0 if problem_response[1] == "first_bit" else 15), } await bms.disconnect() diff --git a/tests/test_redodo_bms.py b/tests/test_redodo_bms.py index b6031b3..bdec304 100644 --- a/tests/test_redodo_bms.py +++ b/tests/test_redodo_bms.py @@ -224,7 +224,7 @@ async def test_problem_response(monkeypatch, problem_response) -> None: assert await bms.async_update() == _RESULT_DEFS | { "problem": True, - "problem_code": 1 << 0 if problem_response[1] == "first_bit" else 1 << 31, + "problem_code": 1 << (0 if problem_response[1] == "first_bit" else 31), } await bms.disconnect() diff --git a/tests/test_tdt_bms.py b/tests/test_tdt_bms.py index 0431f39..cee3819 100644 --- a/tests/test_tdt_bms.py +++ b/tests/test_tdt_bms.py @@ -169,7 +169,7 @@ async def read_gatt_char( return bytearray(int.to_bytes(self._char_fffa, 1, "big")) -async def test_update_16S_6T(monkeypatch, reconnect_fixture) -> None: +async def test_update_16s_6t(monkeypatch, reconnect_fixture) -> None: """Test TDT BMS data update.""" monkeypatch.setattr( @@ -347,15 +347,15 @@ async def throw_response(*_args, **_kwargs) -> bytearray: ( { 0x8C: bytearray( # 16 celll message - b"\x7e\x00\x01\x03\x00\x8c\x00\x3c\x10\x0c\xe3\x0c\xe6\x0c\xde\x0c\xde\x0c\xdd\x0c\xde" - b"\x0c\xdd\x0c\xdc\x0c\xdc\x0c\xda\x0c\xde\x0c\xde\x0c\xde\x0c\xdd\x0c\xdf\x0c\xde\x06" - b"\x0b\x5e\x0b\x6f\x0b\x5e\x0b\x5e\x0b\x5e\x0b\x66\xc0\x39\x14\x96\x03\xdf\x04\x3b\x00" - b"\x08\x03\xe8\x00\x5b\x2b\x9c\x0d" + b"\x7e\x00\x01\x03\x00\x8c\x00\x3c\x10\x0c\xe3\x0c\xe6\x0c\xde\x0c\xde\x0c\xdd" + b"\x0c\xde\x0c\xdd\x0c\xdc\x0c\xdc\x0c\xda\x0c\xde\x0c\xde\x0c\xde\x0c\xdd\x0c" + b"\xdf\x0c\xde\x06\x0b\x5e\x0b\x6f\x0b\x5e\x0b\x5e\x0b\x5e\x0b\x66\xc0\x39\x14" + b"\x96\x03\xdf\x04\x3b\x00\x08\x03\xe8\x00\x5b\x2b\x9c\x0d" ), 0x8D: bytearray( - b"\x7e\x00\x01\x03\x00\x8d\x00\x27\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\x0e\x01\x00\x00" - b"\x18\x00\x00\x00\x00\xce\x2a\x0d" # ^^ ^^ problem bits + b"\x7e\x00\x01\x03\x00\x8d\x00\x27\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01" + b"\x0e\x01\x00\x00\x18\x00\x00\x00\x00\xce\x2a\x0d" # problem bits ^^ ^^ ), }, "first_bit_16cell", @@ -363,15 +363,15 @@ async def throw_response(*_args, **_kwargs) -> bytearray: ( { 0x8C: bytearray( # 16 celll message - b"\x7e\x00\x01\x03\x00\x8c\x00\x3c\x10\x0c\xe3\x0c\xe6\x0c\xde\x0c\xde\x0c\xdd\x0c\xde" - b"\x0c\xdd\x0c\xdc\x0c\xdc\x0c\xda\x0c\xde\x0c\xde\x0c\xde\x0c\xdd\x0c\xdf\x0c\xde\x06" - b"\x0b\x5e\x0b\x6f\x0b\x5e\x0b\x5e\x0b\x5e\x0b\x66\xc0\x39\x14\x96\x03\xdf\x04\x3b\x00" - b"\x08\x03\xe8\x00\x5b\x2b\x9c\x0d" + b"\x7e\x00\x01\x03\x00\x8c\x00\x3c\x10\x0c\xe3\x0c\xe6\x0c\xde\x0c\xde\x0c\xdd" + b"\x0c\xde\x0c\xdd\x0c\xdc\x0c\xdc\x0c\xda\x0c\xde\x0c\xde\x0c\xde\x0c\xdd\x0c" + b"\xdf\x0c\xde\x06\x0b\x5e\x0b\x6f\x0b\x5e\x0b\x5e\x0b\x5e\x0b\x66\xc0\x39\x14" + b"\x96\x03\xdf\x04\x3b\x00\x08\x03\xe8\x00\x5b\x2b\x9c\x0d" ), 0x8D: bytearray( - b"\x7e\x00\x01\x03\x00\x8d\x00\x27\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x80\x00\x0e\x01\x00\x00" - b"\x18\x00\x00\x00\x00\xc9\xd2\x0d" # ^^ ^^ problem bits + b"\x7e\x00\x01\x03\x00\x8d\x00\x27\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x80\x00" + b"\x0e\x01\x00\x00\x18\x00\x00\x00\x00\xc9\xd2\x0d" # problem bits ^^ ^^ ), }, "last_bit_16cell", @@ -403,10 +403,8 @@ async def test_problem_response(monkeypatch, problem_response) -> None: result: BMSsample = await bms.async_update() assert result.get("problem", False) # we expect a problem - assert ( - result.get("problem_code", 0) == 0x1 - if problem_response[1].startswith("first_bit") - else 0x8000 + assert result.get("problem_code", 0) == ( + 0x1 if problem_response[1].startswith("first_bit") else 0x8000 ) await bms.disconnect() From 344c4425fa4d10006d9c2dfeb4bdbda623fdbea2 Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Fri, 28 Feb 2025 20:19:56 +0100 Subject: [PATCH 03/11] clean version output --- custom_components/bms_ble/plugins/jikong_bms.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/custom_components/bms_ble/plugins/jikong_bms.py b/custom_components/bms_ble/plugins/jikong_bms.py index 994c042..77372e8 100644 --- a/custom_components/bms_ble/plugins/jikong_bms.py +++ b/custom_components/bms_ble/plugins/jikong_bms.py @@ -216,7 +216,14 @@ def _cmd(cmd: bytes, value: list[int] | None = None) -> bytes: @staticmethod def _dec_devinfo(data: bytearray) -> dict[str, str]: - return {"hw_version": data[22:27].decode(), "sw_version": data[30:35].decode()} + fields: Final[dict[str, int]] = { + "hw_version": 22, + "sw_version": 30, + } + return { + key: data[idx : idx + 8].decode(errors="replace").strip("\x00") + for key, idx in fields.items() + } @staticmethod def _cell_voltages(data: bytearray, cells: int) -> dict[str, float]: From ea027d9530a9f386bcbfa16a7404aceb2268ae55 Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Sat, 1 Mar 2025 10:36:05 +0100 Subject: [PATCH 04/11] Update to HA 2025.2.2 --- hacs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hacs.json b/hacs.json index f329c78..cb4b94e 100644 --- a/hacs.json +++ b/hacs.json @@ -1,7 +1,7 @@ { "name": "BLE Battery Management System (BMS)", "filename": "bms_ble.zip", - "homeassistant": "2024.6.0", + "homeassistant": "2025.2.2", "render_readme": true, "zip_release": true } From 9c44345d1af8bedc984b6b2753aba9b046170181 Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Sat, 1 Mar 2025 23:37:55 +0100 Subject: [PATCH 05/11] assignment expression --- custom_components/bms_ble/plugins/daly_bms.py | 5 +++-- custom_components/bms_ble/plugins/ective_bms.py | 5 +++-- custom_components/bms_ble/plugins/ej_bms.py | 7 +++---- custom_components/bms_ble/plugins/redodo_bms.py | 3 +-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/custom_components/bms_ble/plugins/daly_bms.py b/custom_components/bms_ble/plugins/daly_bms.py index 3f8d1c3..fd578fa 100644 --- a/custom_components/bms_ble/plugins/daly_bms.py +++ b/custom_components/bms_ble/plugins/daly_bms.py @@ -114,8 +114,9 @@ def _notification_handler( self._log.debug("response data is invalid") return - crc: Final = crc_modbus(data[:-2]) - if crc != int.from_bytes(data[-2:], byteorder="little"): + if (crc := crc_modbus(data[:-2])) != int.from_bytes( + data[-2:], byteorder="little" + ): self._log.debug( "invalid checksum 0x%X != 0x%X", int.from_bytes(data[-2:], byteorder="little"), diff --git a/custom_components/bms_ble/plugins/ective_bms.py b/custom_components/bms_ble/plugins/ective_bms.py index 8d6b507..f637058 100644 --- a/custom_components/bms_ble/plugins/ective_bms.py +++ b/custom_components/bms_ble/plugins/ective_bms.py @@ -120,8 +120,9 @@ def _notification_handler( self._data.clear() return - crc: Final[int] = BMS._crc(self._data[1 : -BMS._CRC_LEN]) - if crc != int(self._data[-BMS._CRC_LEN :], 16): + if (crc := BMS._crc(self._data[1 : -BMS._CRC_LEN])) != int( + self._data[-BMS._CRC_LEN :], 16 + ): self._log.debug( "invalid checksum 0x%X != 0x%X", int(self._data[-BMS._CRC_LEN :], 16), diff --git a/custom_components/bms_ble/plugins/ej_bms.py b/custom_components/bms_ble/plugins/ej_bms.py index 6696c87..ea5e6bf 100644 --- a/custom_components/bms_ble/plugins/ej_bms.py +++ b/custom_components/bms_ble/plugins/ej_bms.py @@ -60,8 +60,8 @@ def matcher_dict_list() -> list[dict]: return [ # Fliteboard, Electronix battery {"local_name": "libatt*", "manufacturer_id": 21320, "connectable": True}, {"local_name": "LT-*", "manufacturer_id": 33384, "connectable": True}, - {"local_name": "L-12V???AH-*", "connectable": True}, # Lithtech Energy - {"local_name": "LT-12V-*", "connectable": True}, # Lithtech Energy + {"local_name": "L-12V???AH-*", "connectable": True}, # Lithtech Energy + {"local_name": "LT-12V-*", "connectable": True}, # Lithtech Energy ] @staticmethod @@ -137,8 +137,7 @@ def _notification_handler( self._data.clear() return - crc: Final = BMS._crc(self._data[1:-3]) - if crc != int(self._data[-3:-1], 16): + if (crc := BMS._crc(self._data[1:-3])) != int(self._data[-3:-1], 16): self._log.debug( "invalid checksum 0x%X != 0x%X", int(self._data[-3:-1], 16), crc ) diff --git a/custom_components/bms_ble/plugins/redodo_bms.py b/custom_components/bms_ble/plugins/redodo_bms.py index 4a91f43..4c7e62c 100644 --- a/custom_components/bms_ble/plugins/redodo_bms.py +++ b/custom_components/bms_ble/plugins/redodo_bms.py @@ -103,8 +103,7 @@ def _notification_handler( self._log.debug("incorrect frame length (%i)", len(data)) return - crc: Final[int] = crc_sum(data[: BMS.CRC_POS]) - if crc != data[BMS.CRC_POS]: + if (crc := crc_sum(data[: BMS.CRC_POS])) != data[BMS.CRC_POS]: self._log.debug( "invalid checksum 0x%X != 0x%X", data[len(data) + BMS.CRC_POS], crc ) From 5560aa6ff5f84a9dddeefcc860468e53f8c89400 Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Sun, 2 Mar 2025 13:25:11 +0100 Subject: [PATCH 06/11] skip fuzz tests for coverage --- .vscode/settings.json | 3 --- pyproject.toml | 2 +- tests/test_fuzzing.py | 9 ++++++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 8a25c3f..e137fad 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,4 @@ { - "python.testing.pytestArgs": [ - "tests","--no-cov" - ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 85bcb9b..c7a7bc7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -218,4 +218,4 @@ load-plugins = [ [tool.pylint."MESSAGES CONTROL"] per-file-ignores = [ "/tests/:protected-access", -] \ No newline at end of file +] diff --git a/tests/test_fuzzing.py b/tests/test_fuzzing.py index c2fccc7..1a62f6a 100644 --- a/tests/test_fuzzing.py +++ b/tests/test_fuzzing.py @@ -4,6 +4,7 @@ from types import ModuleType from hypothesis import HealthCheck, given, settings, strategies as st +import pytest from custom_components.bms_ble.plugins.basebms import BaseBMS @@ -18,10 +19,16 @@ max_examples=1000, suppress_health_check=[HealthCheck.function_scoped_fixture] ) async def test_notification_handler( - monkeypatch, plugin_fixture: ModuleType, data: bytearray + monkeypatch, + pytestconfig: pytest.Config, + plugin_fixture: ModuleType, + data: bytearray, ) -> None: """Test the notification handler.""" + if pytestconfig.getoption("--cov") == ["custom_components.bms_ble"]: + pytest.skip("Skipping fuzzing tests due to coverage generation!") + async def patch_init() -> None: return From cae820b21fa62795e680bc44e48c3a4e1fcc42a5 Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Sun, 2 Mar 2025 13:39:43 +0100 Subject: [PATCH 07/11] OGT decode error test --- tests/test_ogt_bms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ogt_bms.py b/tests/test_ogt_bms.py index 9d0a4ff..c58193f 100644 --- a/tests/test_ogt_bms.py +++ b/tests/test_ogt_bms.py @@ -94,7 +94,7 @@ async def _response( if isinstance(char_specifier, str) and normalize_uuid_str( char_specifier ) == normalize_uuid_str("fff6"): - return bytearray(b"invalid_value") + return bytearray(b"invalid\xF0value") return bytearray() From 81c2a7c2e64d9ddb3667719db6acef3fdac7ae90 Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Sun, 2 Mar 2025 17:09:19 +0100 Subject: [PATCH 08/11] inline crc assignment --- custom_components/bms_ble/plugins/cbtpwr_bms.py | 5 +++-- custom_components/bms_ble/plugins/dpwrcore_bms.py | 6 +++--- custom_components/bms_ble/plugins/ecoworthy_bms.py | 3 +-- custom_components/bms_ble/plugins/jbd_bms.py | 5 +++-- custom_components/bms_ble/plugins/jikong_bms.py | 3 +-- custom_components/bms_ble/plugins/seplos_bms.py | 5 +++-- custom_components/bms_ble/plugins/seplos_v2_bms.py | 3 +-- custom_components/bms_ble/plugins/tdt_bms.py | 5 +++-- 8 files changed, 18 insertions(+), 17 deletions(-) diff --git a/custom_components/bms_ble/plugins/cbtpwr_bms.py b/custom_components/bms_ble/plugins/cbtpwr_bms.py index 379fd98..e154f4f 100644 --- a/custom_components/bms_ble/plugins/cbtpwr_bms.py +++ b/custom_components/bms_ble/plugins/cbtpwr_bms.py @@ -120,8 +120,9 @@ def _notification_handler( self._log.debug("incorrect frame start/end: %s", data) return - crc = crc_sum(data[len(BMS.HEAD) : len(data) + BMS.CRC_POS]) - if data[BMS.CRC_POS] != crc: + if (crc := crc_sum(data[len(BMS.HEAD) : len(data) + BMS.CRC_POS])) != data[ + BMS.CRC_POS + ]: self._log.debug( "invalid checksum 0x%X != 0x%X", data[len(data) + BMS.CRC_POS], diff --git a/custom_components/bms_ble/plugins/dpwrcore_bms.py b/custom_components/bms_ble/plugins/dpwrcore_bms.py index 3fc48b9..58988d9 100644 --- a/custom_components/bms_ble/plugins/dpwrcore_bms.py +++ b/custom_components/bms_ble/plugins/dpwrcore_bms.py @@ -147,13 +147,13 @@ async def _notification_handler( self._log.debug("(%s): %s", "start" if page == 1 else "cnt.", data) if page == maxpg: - if int.from_bytes(self._data[-4:-2], byteorder="big") != BMS._crc( - self._data[3:-4] + if (crc := BMS._crc(self._data[3:-4])) != int.from_bytes( + self._data[-4:-2], byteorder="big" ): self._log.debug( "incorrect checksum: 0x%X != 0x%X", int.from_bytes(self._data[-4:-2], byteorder="big"), - self._crc(self._data[3:-4]), + crc, ) self._data = bytearray() self._data_final = bytearray() # reset invalid data diff --git a/custom_components/bms_ble/plugins/ecoworthy_bms.py b/custom_components/bms_ble/plugins/ecoworthy_bms.py index 69c99eb..ad6c61d 100644 --- a/custom_components/bms_ble/plugins/ecoworthy_bms.py +++ b/custom_components/bms_ble/plugins/ecoworthy_bms.py @@ -109,8 +109,7 @@ def _notification_handler( self._log.debug("Invalid frame type: 0x%X", data[0:1]) return - crc: Final[int] = crc_modbus(data[:-2]) - if int.from_bytes(data[-2:], "little") != crc: + if (crc := crc_modbus(data[:-2])) != int.from_bytes(data[-2:], "little"): self._log.debug( "invalid checksum 0x%X != 0x%X", int.from_bytes(data[-2:], "little"), diff --git a/custom_components/bms_ble/plugins/jbd_bms.py b/custom_components/bms_ble/plugins/jbd_bms.py index bf5f93c..fb18638 100644 --- a/custom_components/bms_ble/plugins/jbd_bms.py +++ b/custom_components/bms_ble/plugins/jbd_bms.py @@ -134,8 +134,9 @@ def _notification_handler( self._log.debug("incorrect frame end (length: %i).", len(self._data)) return - crc: Final[int] = BMS._crc(self._data[2 : frame_end - 2]) - if int.from_bytes(self._data[frame_end - 2 : frame_end], "big") != crc: + if (crc := BMS._crc(self._data[2 : frame_end - 2])) != int.from_bytes( + self._data[frame_end - 2 : frame_end], "big" + ): self._log.debug( "invalid checksum 0x%X != 0x%X", int.from_bytes(self._data[frame_end - 2 : frame_end], "big"), diff --git a/custom_components/bms_ble/plugins/jikong_bms.py b/custom_components/bms_ble/plugins/jikong_bms.py index a421a93..c739df1 100644 --- a/custom_components/bms_ble/plugins/jikong_bms.py +++ b/custom_components/bms_ble/plugins/jikong_bms.py @@ -151,8 +151,7 @@ def _notification_handler( self._log.debug("wrong data length (%i): %s", len(self._data), self._data) self._data = self._data[: BMS.INFO_LEN] - crc: Final[int] = crc_sum(self._data[:-1]) - if self._data[-1] != crc: + if (crc := crc_sum(self._data[:-1])) != self._data[-1]: self._log.debug("invalid checksum 0x%X != 0x%X", self._data[-1], crc) return diff --git a/custom_components/bms_ble/plugins/seplos_bms.py b/custom_components/bms_ble/plugins/seplos_bms.py index c30e7f0..2c0a63c 100644 --- a/custom_components/bms_ble/plugins/seplos_bms.py +++ b/custom_components/bms_ble/plugins/seplos_bms.py @@ -158,8 +158,9 @@ def _notification_handler( if len(self._data) < self._pkglen: return - crc: Final[int] = crc_modbus(self._data[: self._pkglen - 2]) - if int.from_bytes(self._data[self._pkglen - 2 : self._pkglen], "little") != crc: + if (crc := crc_modbus(self._data[: self._pkglen - 2])) != int.from_bytes( + self._data[self._pkglen - 2 : self._pkglen], "little" + ): self._log.debug( "invalid checksum 0x%X != 0x%X", int.from_bytes(self._data[self._pkglen - 2 : self._pkglen], "little"), diff --git a/custom_components/bms_ble/plugins/seplos_v2_bms.py b/custom_components/bms_ble/plugins/seplos_v2_bms.py index bb79521..7eaff73 100644 --- a/custom_components/bms_ble/plugins/seplos_v2_bms.py +++ b/custom_components/bms_ble/plugins/seplos_v2_bms.py @@ -138,8 +138,7 @@ def _notification_handler( self._log.debug("BMS reported error code: 0x%X", self._data[4]) return - crc: Final[int] = crc_xmodem(self._data[1:-3]) - if int.from_bytes(self._data[-3:-1]) != crc: + if (crc := crc_xmodem(self._data[1:-3])) != int.from_bytes(self._data[-3:-1]): self._log.debug( "invalid checksum 0x%X != 0x%X", int.from_bytes(self._data[-3:-1]), diff --git a/custom_components/bms_ble/plugins/tdt_bms.py b/custom_components/bms_ble/plugins/tdt_bms.py index 8a224c2..14903a1 100644 --- a/custom_components/bms_ble/plugins/tdt_bms.py +++ b/custom_components/bms_ble/plugins/tdt_bms.py @@ -147,8 +147,9 @@ def _notification_handler( self._log.debug("BMS reported error code: 0x%X", self._data[4]) return - crc: Final[int] = crc_modbus(self._data[:-3]) - if int.from_bytes(self._data[-3:-1], "big") != crc: + if (crc := crc_modbus(self._data[:-3])) != int.from_bytes( + self._data[-3:-1], "big" + ): self._log.debug( "invalid checksum 0x%X != 0x%X", int.from_bytes(self._data[-3:-1], "big"), From 48020aa0643d110f465774d2665d788bc5abd26c Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Sun, 2 Mar 2025 17:34:00 +0100 Subject: [PATCH 09/11] run fuzz tests only without coverage --- tests/test_fuzzing.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_fuzzing.py b/tests/test_fuzzing.py index 1a62f6a..6deeff2 100644 --- a/tests/test_fuzzing.py +++ b/tests/test_fuzzing.py @@ -26,7 +26,13 @@ async def test_notification_handler( ) -> None: """Test the notification handler.""" - if pytestconfig.getoption("--cov") == ["custom_components.bms_ble"]: + # fuzzing can run from VScode (no coverage) or command line with option --no-cov + if {"vscode_pytest", "--cov=."}.issubset( + set(pytestconfig.invocation_params.args) + ) or ( + "vscode_pytest" not in pytestconfig.invocation_params.args + and not pytestconfig.getoption("--no-cov") + ): pytest.skip("Skipping fuzzing tests due to coverage generation!") async def patch_init() -> None: From 45591653fb0632b775581f53aff4df3afabf3a16 Mon Sep 17 00:00:00 2001 From: patman15 <14628713+patman15@users.noreply.github.com> Date: Sun, 2 Mar 2025 18:03:45 +0100 Subject: [PATCH 10/11] add GHA for fuzz testing --- .github/workflows/fuzzing.yaml | 26 ++++++++++++++++++++++++++ tests/test_fuzzing.py | 4 +++- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/fuzzing.yaml diff --git a/.github/workflows/fuzzing.yaml b/.github/workflows/fuzzing.yaml new file mode 100644 index 0000000..e319cc8 --- /dev/null +++ b/.github/workflows/fuzzing.yaml @@ -0,0 +1,26 @@ +name: Run fuzz tests + +on: + pull_request: + workflow_dispatch: + schedule: + - cron: '1 5 * * *' + +jobs: + validate: + runs-on: "ubuntu-latest" + steps: + - name: "Checkout the repository" + uses: "actions/checkout@v4" + + - name: "Set up Python" + uses: actions/setup-python@main + with: + python-version: "3.13" + cache: "pip" + + - name: Install dependencies + run: pip install -r requirements_test.txt + + - name: Run fuzz tests + run: pytest tests/test_fuzzing.py --no-cov diff --git a/tests/test_fuzzing.py b/tests/test_fuzzing.py index 6deeff2..de34e92 100644 --- a/tests/test_fuzzing.py +++ b/tests/test_fuzzing.py @@ -6,6 +6,7 @@ from hypothesis import HealthCheck, given, settings, strategies as st import pytest +from custom_components.bms_ble.const import BMS_TYPES from custom_components.bms_ble.plugins.basebms import BaseBMS from .bluetooth import generate_ble_device @@ -16,8 +17,9 @@ data=st.binary(min_size=0, max_size=513) ) # ATT is not allowed larger than 512 bytes @settings( - max_examples=1000, suppress_health_check=[HealthCheck.function_scoped_fixture] + max_examples=5000, suppress_health_check=[HealthCheck.function_scoped_fixture] ) +@pytest.mark.parametrize("plugin_fixture", BMS_TYPES, indirect=True) async def test_notification_handler( monkeypatch, pytestconfig: pytest.Config, From 456c84587b3d5c3d7e8ad67bd0b88347880be2cd Mon Sep 17 00:00:00 2001 From: Patrick <14628713+patman15@users.noreply.github.com> Date: Sun, 2 Mar 2025 18:09:35 +0100 Subject: [PATCH 11/11] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f1e6132..a8f7d3e 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ This integration allows to monitor Bluetooth Low Energy (BLE) battery management - Any number of batteries in parallel - Native Home Assistant integration (works with all [HA installation methods](https://www.home-assistant.io/installation/#advanced-installation-methods)) - Readout of individual cell voltages to be able to judge battery health -- 100% test coverage +- 100% test coverage plus fuzz tests for BLE data ### Supported Devices - CBT Power BMS, Creabest batteries @@ -203,6 +203,6 @@ for helping with making the integration better. [license-shield]: https://img.shields.io/github/license/patman15/BMS_BLE-HA.svg?style=for-the-badge [releases-shield]: https://img.shields.io/github/release/patman15/BMS_BLE-HA.svg?style=for-the-badge [releases]: https://github.com//patman15/BMS_BLE-HA/releases -[effort-shield]: https://img.shields.io/badge/Effort%20spent-391_hours-gold?style=for-the-badge&cacheSeconds=86400 +[effort-shield]: https://img.shields.io/badge/Effort%20spent-395_hours-gold?style=for-the-badge&cacheSeconds=86400 [install-shield]: https://img.shields.io/badge/dynamic/json?style=for-the-badge&color=green&label=Analytics&suffix=%20Installs&cacheSeconds=15600&url=https://analytics.home-assistant.io/custom_integrations.json&query=$.bms_ble.total [btproxy-url]: https://esphome.io/components/bluetooth_proxy