From dbf029659f67fbf5ff45dcf5490b4d9481ba73d6 Mon Sep 17 00:00:00 2001 From: Patrick <14628713+patman15@users.noreply.github.com> Date: Sat, 28 Dec 2024 20:45:13 +0100 Subject: [PATCH] Feature/add advertisements (#132) * fixed redundant config_flow for device discovery * add detection tests for all BMS types * check for MAC addr if name is empty --- requirements_test.txt | 2 +- tests/advertisement_data.py | 128 +++++++++++++++++++++++++++++------- tests/test_config_flow.py | 39 +++++------ tests/test_plugins.py | 15 ++++- 4 files changed, 136 insertions(+), 48 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index ef103e2..2ce5da5 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -22,4 +22,4 @@ bluetooth-data-tools pyserial-asyncio pyudev pytest-homeassistant-custom-component==0.13.132 - +kegtron-ble diff --git a/tests/advertisement_data.py b/tests/advertisement_data.py index f5dd6a5..ab7886c 100644 --- a/tests/advertisement_data.py +++ b/tests/advertisement_data.py @@ -5,24 +5,24 @@ from .bluetooth import generate_advertisement_data ADVERTISEMENTS: Final[list] = [ - # ( # conflicting integrated component: https://github.com/patman15/BMS_BLE-HA/issues/123 - # generate_advertisement_data( - # local_name="NWJ20221223010330",#\x11", - # manufacturer_data={65535: b"0UD7\xa2\xd2"}, - # service_uuids=["0000ffe0-0000-1000-8000-00805f9b34fb"], - # rssi=-56, - # ), - # "ective_bms", - # ), - # ( - # generate_advertisement_data( - # local_name="NWJ20221223010388",#\x11", - # manufacturer_data={65535: b"0UD7b\xec"}, - # service_uuids=["0000ffe0-0000-1000-8000-00805f9b34fb"], - # rssi=-47, - # ), - # "ective_bms", - # ), + ( # source LOG + generate_advertisement_data( + local_name="NWJ20221223010330\x11", + manufacturer_data={65535: b"0UD7\xa2\xd2"}, + service_uuids=["0000ffe0-0000-1000-8000-00805f9b34fb"], + rssi=-56, + ), + "ective_bms", + ), + ( # source LOG + generate_advertisement_data( + local_name="NWJ20221223010388\x11", + manufacturer_data={65535: b"0UD7b\xec"}, + service_uuids=["0000ffe0-0000-1000-8000-00805f9b34fb"], + rssi=-47, + ), + "ective_bms", + ), ( generate_advertisement_data( local_name="BatteryOben-00", @@ -33,7 +33,7 @@ ), "jikong_bms", ), - ( + ( # source LOG generate_advertisement_data( local_name="BatterieUnten-01", manufacturer_data={2917: b"\x88\xa0\xc8G\x80\r\x08k"}, @@ -43,7 +43,7 @@ ), "jikong_bms", ), - ( + ( # source LOG generate_advertisement_data( local_name="JK_B2A8S20P", manufacturer_data={2917: b"\x88\xa0\xc8G\x80\x14\x88\xb7"}, @@ -60,7 +60,7 @@ ), "jikong_bms", ), - ( + ( # source LOG generate_advertisement_data( local_name="SP05B2312190075 ", service_uuids=["0000fff0-0000-1000-8000-00805f9b34fb"], @@ -69,7 +69,7 @@ ), "seplos_bms", ), - ( + ( # source LOG generate_advertisement_data( local_name="SP66B2404270002 ", service_uuids=["0000fff0-0000-1000-8000-00805f9b34fb"], @@ -85,7 +85,7 @@ ), "seplos_v2_bms", ), - ( + ( # source LOG generate_advertisement_data( local_name="BP02", service_uuids=[ @@ -97,7 +97,7 @@ ), "seplos_v2_bms", ), - ( + ( # source LOG generate_advertisement_data( local_name="LT-12V-1544", manufacturer_data={33384: b"\x01\x02\x00\x07\x81\xb5N"}, @@ -106,4 +106,84 @@ ), "ej_bms", ), + ( # source LOG + generate_advertisement_data( + local_name="170R000121", + manufacturer_data={ + 21330: b"!4\xba\x03\xec\x11\x0c\xb4\x01\x05\x00\x01\x00\x00" + }, + service_uuids=[ + "00001800-0000-1000-8000-00805f9b34fb", + "00001801-0000-1000-8000-00805f9b34fb", + "0000180a-0000-1000-8000-00805f9b34fb", + "0000fd00-0000-1000-8000-00805f9b34fb", + "0000ff90-0000-1000-8000-00805f9b34fb", + "0000ffb0-0000-1000-8000-00805f9b34fb", + "0000ffc0-0000-1000-8000-00805f9b34fb", + "0000ffd0-0000-1000-8000-00805f9b34fb", + "0000ffe0-0000-1000-8000-00805f9b34fb", + "0000ffe5-0000-1000-8000-00805f9b34fb", + "0000fff0-0000-1000-8000-00805f9b34fb", + ], + tx_power=0, + rssi=-75, + ), + "cbtpwr_bms", + ), + ( # source PCAP + generate_advertisement_data( + manufacturer_data={54976: b"\x3c\x4f\xac\x50\xff"}, + ), + "tdt_bms", + ), + ( # source bluetoothctl (https://github.com/patman15/BMS_BLE-HA/issues/52#issuecomment-2390048120) + generate_advertisement_data( + local_name="TBA-13500277", + service_uuids=[ + "00001800-0000-1000-8000-00805f9b34fb", + "00001801-0000-1000-8000-00805f9b34fb", + "0000180a-0000-1000-8000-00805f9b34fb", + "0000fff0-0000-1000-8000-00805f9b34fb", + ], + rssi=-72, + ), + "dpwrcore_bms", + ), + ( # source LOG + generate_advertisement_data( + local_name="SmartBat-B15051", + service_uuids=["0000fff0-0000-1000-8000-00805f9b34fb"], + tx_power=3, + rssi=-66, + ), + "ogt_bms", + ), + ( # source PCAP + generate_advertisement_data( + local_name="R-24100BNN160-A00643", + service_uuids=[ + "0000ffe0-0000-1000-8000-00805f9b34fb", + ], + manufacturer_data={22618: b"\xc8\x47\x80\x15\xd8\x34"}, + ), + "redodo_bms", + ), + ( # source LOG (https://github.com/patman15/BMS_BLE-HA/issues/89) + generate_advertisement_data( + local_name="DL-46640102XXXX", + manufacturer_data={25670: b"\x01\x02\t\xac"}, + service_uuids=["0000fff0-0000-1000-8000-00805f9b34fb"], + tx_power=-127, + rssi=-58, + ), + "daly_bms", + ), + ( # source nRF (https://github.com/patman15/BMS_BLE-HA/issues/22#issuecomment-2198586195) + generate_advertisement_data( + local_name="SX100P-B230201", # Supervolt Battery + service_uuids=["0000ff00-0000-1000-8000-00805f9b34fb"], + manufacturer_data={31488: "\x02\xFF\xFF\x7D"}, + ), + "jbd_bms", + ), ] diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 2a100c7..43708a7 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -40,7 +40,7 @@ def bms_advertisement(request) -> BluetoothServiceInfoBleak: address: Final[str] = "c0:ff:ee:c0:ff:ee" return BluetoothServiceInfoBleak( name=str(dev.local_name), - address=request.param[1], + address=f"{address}_{request.param[1]}", device=generate_ble_device(address=address, name=dev.local_name), rssi=dev.rssi, service_uuids=dev.service_uuids, @@ -56,46 +56,43 @@ def bms_advertisement(request) -> BluetoothServiceInfoBleak: ) -async def test_device_discovery( - advertisement: BluetoothServiceInfoBleak, hass: HomeAssistant +async def test_bluetooth_discovery( + hass: HomeAssistant, advertisement: BluetoothServiceInfoBleak ) -> None: - """Test discovery via bluetooth with a valid device.""" + """Test bluetooth device discovery.""" - result: ConfigFlowResult = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_BLUETOOTH}, - data=advertisement, - ) + inject_bluetooth_service_info_bleak(hass, advertisement) + await hass.async_block_till_done(wait_background_tasks=True) - assert result.get("type") == FlowResultType.FORM + result: ConfigFlowResult = hass.config_entries.flow.async_progress_by_handler(DOMAIN)[0] assert result.get("step_id") == "bluetooth_confirm" - assert result.get("description_placeholders") == {"name": advertisement.name} - - inject_bluetooth_service_info_bleak(hass, advertisement) + assert result.get("context", {}).get("unique_id") == advertisement.address result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"not": "empty"} ) await hass.async_block_till_done() assert result.get("type") == FlowResultType.CREATE_ENTRY - assert result.get("title") == advertisement.name + assert ( + result.get("title") == advertisement.name or advertisement.address + ) # address is used as name by Bleak if name is not available - # BluetoothServiceInfoBleak contains BMS type in the address, see bms_advertisement + # BluetoothServiceInfoBleak contains BMS type as trailer to the address, see bms_advertisement assert ( hass.config_entries.async_entries()[1].data["type"] - == f"custom_components.bms_ble.plugins.{advertisement.address}" + == f"custom_components.bms_ble.plugins.{advertisement.address.split('_',1)[-1]}" ) async def test_device_setup( monkeypatch, - patch_bleakclient, + patch_bleakclient: None, BTdiscovery: BluetoothServiceInfoBleak, hass: HomeAssistant, ) -> None: """Test discovery via bluetooth with a valid device.""" - result = await hass.config_entries.flow.async_init( + result: ConfigFlowResult = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=BTdiscovery, @@ -154,7 +151,7 @@ async def test_invalid_plugin(monkeypatch, BTdiscovery, hass: HomeAssistant) -> """ monkeypatch.delattr(BaseBMS, "supported") - result = await hass.config_entries.flow.async_init( + result: ConfigFlowResult = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_BLUETOOTH}, data=BTdiscovery, @@ -231,7 +228,7 @@ async def test_user_setup( inject_bluetooth_service_info_bleak(hass, BTdiscovery) - result = await hass.config_entries.flow.async_init( + result: ConfigFlowResult = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result.get("type") == FlowResultType.FORM @@ -297,7 +294,7 @@ def patch_async_current_ids(_self) -> set[str | None]: inject_bluetooth_service_info_bleak(hass, BTdiscovery) - result = await hass.config_entries.flow.async_init( + result: ConfigFlowResult = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result.get("type") == FlowResultType.ABORT diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 050d4bb..fb0ef3c 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -1,9 +1,12 @@ """Test the BLE Battery Management System base class functions.""" +from custom_components.bms_ble.const import BMS_TYPES from custom_components.bms_ble.plugins.basebms import BaseBMS +from .advertisement_data import ADVERTISEMENTS -def test_device_info(plugin_fixture) -> None: + +def test_device_info(plugin_fixture: BaseBMS) -> None: """Test that the BMS returns valid device information.""" bms_instance: BaseBMS = plugin_fixture result = bms_instance.device_info() @@ -11,7 +14,15 @@ def test_device_info(plugin_fixture) -> None: assert "model" in result -def test_matcher_dict(plugin_fixture) -> None: +def test_matcher_dict(plugin_fixture: BaseBMS) -> None: """Test that the BMS returns BT matcher.""" bms_instance: BaseBMS = plugin_fixture assert len(bms_instance.matcher_dict_list()) + +def test_advertisements_complete() -> None: + """Check that each BMS has at least one advertisement.""" + bms_unchecked: list[str] = BMS_TYPES + for _adv, bms in ADVERTISEMENTS: + if bms in bms_unchecked: + bms_unchecked.remove(bms) + assert not bms_unchecked, f"{len(bms_unchecked)} missing BMS type advertisements: {bms_unchecked}"