diff --git a/esphome/components/ble_device_base/automation.h b/esphome/components/ble_device_base/automation.h index ba3128c0ee..a50813c585 100644 --- a/esphome/components/ble_device_base/automation.h +++ b/esphome/components/ble_device_base/automation.h @@ -11,29 +11,34 @@ #include "esphome/core/automation.h" #include "esphome/core/helpers.h" -#include -#include - namespace esphome::ble_device_base { +/// True if `address` is in `table`, a list of MACs ended by 0. +inline bool mac_in_table(const uint64_t *table, uint64_t address) { + for (; *table != 0; table++) { + if (*table == address) + return true; + } + return false; +} + // on_ble_advertise: fires on every BLE advertisement, optionally filtered to one or more MACs. class ESPBTAdvertiseTrigger final : public Trigger, public ESPBTDeviceListener { public: template explicit ESPBTAdvertiseTrigger(Hub *parent) { parent->register_listener(this); } - void set_addresses(std::initializer_list addresses) { this->addresses_ = addresses; } + /// Table of MACs ended by 0; must outlive the trigger. + void set_addresses(const uint64_t *addresses) { this->addresses_ = addresses; } bool parse_device(const ESPBTDevice &device) override { - if (!this->addresses_.empty() && std::find(this->addresses_.begin(), this->addresses_.end(), - device.address_uint64()) == this->addresses_.end()) { + if (this->addresses_ != nullptr && !mac_in_table(this->addresses_, device.address_uint64())) return false; - } this->trigger(device); return true; } protected: - FixedVector addresses_; + const uint64_t *addresses_{nullptr}; }; // on_ble_service_data_advertise: fires when an advertisement contains service diff --git a/esphome/components/ble_device_base/automation.py b/esphome/components/ble_device_base/automation.py index 6acc4edb92..7cc87add5a 100644 --- a/esphome/components/ble_device_base/automation.py +++ b/esphome/components/ble_device_base/automation.py @@ -69,13 +69,24 @@ def uuid_trigger_schema( ) +def _filter_mac(value: Any) -> Any: + mac = cv.mac_address(value) + if not any(mac.parts): + # 0 ends the flash MAC table, and it is never a real device address. + raise cv.Invalid("00:00:00:00:00:00 cannot be used as a MAC address filter") + return mac + + +MAC_FILTER_LIST = cv.ensure_list(_filter_mac) + + def advertise_trigger_schema(trigger_class: MockObjClass) -> Callable[[Any], Any]: """on_ble_advertise schema: multi-mac list filter, unlike the single-mac uuid_trigger_schema() — pairs with advertise_trigger_to_code().""" return automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(trigger_class), - cv.Optional(CONF_MAC_ADDRESS): cv.ensure_list(cv.mac_address), + cv.Optional(CONF_MAC_ADDRESS): MAC_FILTER_LIST, } ) @@ -94,11 +105,18 @@ def scan_end_trigger_schema(trigger_class: MockObjClass) -> Callable[[Any], Any] _count_listener = cg.slot_counter(LISTENER_COUNT_DEFINE) +def mac_filter_table(macs: list) -> cg.MockObj: + """Shared flash table of MACs ended by 0 (never a valid address), so triggers store a pointer.""" + return cg.shared_progmem_array( + "ble_mac_filter", cg.uint64, [*(mac.as_hex for mac in macs), 0] + ) + + async def advertise_trigger_to_code(conf: ConfigType, var: cg.MockObj) -> None: """Build an on_ble_advertise trigger (optional multi-mac filter).""" trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - if (macs := conf.get(CONF_MAC_ADDRESS)) is not None: - cg.add(trigger.set_addresses([it.as_hex for it in macs])) + if macs := conf.get(CONF_MAC_ADDRESS): + cg.add(trigger.set_addresses(mac_filter_table(macs))) await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf) _count_listener() diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index fc52b92ee7..59ac445cc0 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -8,6 +8,10 @@ from esphome import automation import esphome.codegen as cg from esphome.components import ble_device_base, esp32_ble, ota from esphome.components.ble_device_base import CONF_CONNECTION_SCAN_WINDOW +from esphome.components.ble_device_base.automation import ( + MAC_FILTER_LIST, + mac_filter_table, +) from esphome.components.const import CONF_ON_SCAN_END, CONF_SCAN_PARAMETERS, CONF_WINDOW from esphome.components.esp32 import ( add_idf_sdkconfig_option, @@ -277,7 +281,7 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( ESPBTAdvertiseTrigger ), - cv.Optional(CONF_MAC_ADDRESS): cv.ensure_list(cv.mac_address), + cv.Optional(CONF_MAC_ADDRESS): MAC_FILTER_LIST, } ), cv.Optional( @@ -381,9 +385,8 @@ async def to_code(config: ConfigType) -> None: for conf in config.get(CONF_ON_BLE_ADVERTISE, []): _request_listener_slot() trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - if CONF_MAC_ADDRESS in conf: - addr_list = [it.as_hex for it in conf[CONF_MAC_ADDRESS]] - cg.add(trigger.set_addresses(addr_list)) + if macs := conf.get(CONF_MAC_ADDRESS): + cg.add(trigger.set_addresses(mac_filter_table(macs))) await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf) for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []): _request_listener_slot() diff --git a/esphome/components/esp32_ble_tracker/automation.h b/esphome/components/esp32_ble_tracker/automation.h index 9b4eb2c3b3..f30f63a5c8 100644 --- a/esphome/components/esp32_ble_tracker/automation.h +++ b/esphome/components/esp32_ble_tracker/automation.h @@ -1,10 +1,9 @@ #pragma once #include "esphome/core/automation.h" +#include "esphome/components/ble_device_base/automation.h" #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" -#include - #ifdef USE_ESP32 namespace esphome::esp32_ble_tracker { @@ -12,22 +11,18 @@ namespace esphome::esp32_ble_tracker { class ESPBTAdvertiseTrigger final : public Trigger, public ESPBTDeviceListener { public: explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } - void set_addresses(std::initializer_list addresses) { this->address_vec_ = addresses; } + /// Table of MACs ended by 0; must outlive the trigger. + void set_addresses(const uint64_t *addresses) { this->addresses_ = addresses; } bool parse_device(const ESPBTDevice &device) override { - uint64_t u64_addr = device.address_uint64(); - if (!address_vec_.empty()) { - if (std::find(address_vec_.begin(), address_vec_.end(), u64_addr) == address_vec_.end()) { - return false; - } - } - + if (this->addresses_ != nullptr && !ble_device_base::mac_in_table(this->addresses_, device.address_uint64())) + return false; this->trigger(device); return true; } protected: - std::vector address_vec_; + const uint64_t *addresses_{nullptr}; }; class BLEServiceDataAdvertiseTrigger final : public Trigger, public ESPBTDeviceListener { diff --git a/tests/component_tests/bk72xx_ble_tracker/test_automations_codegen.py b/tests/component_tests/bk72xx_ble_tracker/test_automations_codegen.py index 3f5280d965..2fb1881d1f 100644 --- a/tests/component_tests/bk72xx_ble_tracker/test_automations_codegen.py +++ b/tests/component_tests/bk72xx_ble_tracker/test_automations_codegen.py @@ -22,7 +22,11 @@ def test_trigger_codegen( main_cpp = generate_main(component_config_path("test_automations.yaml")) # on_ble_advertise: multi-mac filter (two addresses in one initializer list) - assert "set_addresses({0xAC3743775F4CULL, 0x112233445566ULL})" in main_cpp + assert ( + "static constexpr uint64_t ble_mac_filter[] PROGMEM = " + "{0xAC3743775F4CULL, 0x112233445566ULL, 0};" in main_cpp + ) + assert "set_addresses(ble_mac_filter)" in main_cpp # 128-bit service uuid goes out reversed (BLE wire order); single-mac filter assert ( "set_service_uuid128((uint8_t*)(const uint8_t[16]){0xCD,0xAB,0xCD,0xAB," diff --git a/tests/component_tests/esp32_ble_tracker/config/mac_filter.yaml b/tests/component_tests/esp32_ble_tracker/config/mac_filter.yaml new file mode 100644 index 0000000000..309476a509 --- /dev/null +++ b/tests/component_tests/esp32_ble_tracker/config/mac_filter.yaml @@ -0,0 +1,27 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +logger: + +esp32_ble_tracker: + on_ble_advertise: + - mac_address: + - AC:37:43:77:5F:4C + - 11:22:33:44:55:66 + then: + - logger.log: one + - mac_address: + - AC:37:43:77:5F:4C + - 11:22:33:44:55:66 + then: + - logger.log: two + - then: + - logger.log: any + - mac_address: [] + then: + - logger.log: empty diff --git a/tests/component_tests/esp32_ble_tracker/test_mac_filter.py b/tests/component_tests/esp32_ble_tracker/test_mac_filter.py new file mode 100644 index 0000000000..45805debc8 --- /dev/null +++ b/tests/component_tests/esp32_ble_tracker/test_mac_filter.py @@ -0,0 +1,38 @@ +"""Tests for the esp32_ble_tracker on_ble_advertise MAC filter codegen.""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + +from esphome import config_validation as cv +from esphome.components.ble_device_base.automation import MAC_FILTER_LIST + + +def test_mac_filters_share_one_progmem_table( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Identical MAC lists share one flash table ended by 0; no or an empty filter sets none.""" + main_cpp = generate_main(component_config_path("mac_filter.yaml")) + + assert ( + "static constexpr uint64_t ble_mac_filter[] PROGMEM = " + "{0xAC3743775F4CULL, 0x112233445566ULL, 0};" in main_cpp + ) + assert main_cpp.count("set_addresses(ble_mac_filter);") == 2 + assert main_cpp.count("set_addresses(") == 2 + + +def test_zero_mac_is_rejected() -> None: + """00:00:00:00:00:00 ends the flash table, so it cannot be a filter entry.""" + assert MAC_FILTER_LIST(["11:22:33:44:55:66"])[0].parts == ( + 0x11, + 0x22, + 0x33, + 0x44, + 0x55, + 0x66, + ) + with pytest.raises(cv.Invalid): + MAC_FILTER_LIST(["00:00:00:00:00:00"]) diff --git a/tests/component_tests/ln882h_ble_tracker/test_automations_codegen.py b/tests/component_tests/ln882h_ble_tracker/test_automations_codegen.py index c1ef283ea3..3d275e479e 100644 --- a/tests/component_tests/ln882h_ble_tracker/test_automations_codegen.py +++ b/tests/component_tests/ln882h_ble_tracker/test_automations_codegen.py @@ -17,7 +17,11 @@ def test_trigger_codegen( main_cpp = generate_main(component_config_path("test_automations.yaml")) # on_ble_advertise: multi-mac filter (two addresses in one initializer list) - assert "set_addresses({0xAC3743775F4CULL, 0x112233445566ULL})" in main_cpp + assert ( + "static constexpr uint64_t ble_mac_filter[] PROGMEM = " + "{0xAC3743775F4CULL, 0x112233445566ULL, 0};" in main_cpp + ) + assert "set_addresses(ble_mac_filter)" in main_cpp # 128-bit service uuid goes out reversed (BLE wire order); single-mac filter assert ( "set_service_uuid128((uint8_t*)(const uint8_t[16]){0xCD,0xAB,0xCD,0xAB," diff --git a/tests/component_tests/rp2_ble_tracker/test_automations_codegen.py b/tests/component_tests/rp2_ble_tracker/test_automations_codegen.py index 1106fd01eb..6014e9c285 100644 --- a/tests/component_tests/rp2_ble_tracker/test_automations_codegen.py +++ b/tests/component_tests/rp2_ble_tracker/test_automations_codegen.py @@ -19,8 +19,12 @@ def test_trigger_codegen( ) -> None: main_cpp = generate_main(component_config_path("test_automations.yaml")) - # on_ble_advertise: multi-mac filter (two addresses in one initializer list) - assert "set_addresses({0xAC3743775F4CULL, 0x112233445566ULL})" in main_cpp + # on_ble_advertise: multi-mac filter, a shared flash table ended by 0 + assert ( + "static constexpr uint64_t ble_mac_filter[] PROGMEM = " + "{0xAC3743775F4CULL, 0x112233445566ULL, 0};" in main_cpp + ) + assert "set_addresses(ble_mac_filter)" in main_cpp # 128-bit service uuid goes out reversed (BLE wire order); single-mac filter assert ( "set_service_uuid128((uint8_t*)(const uint8_t[16]){0xCD,0xAB,0xCD,0xAB,"