From d548454dbe2fb94824e8a68e8b68fd44aa82393a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edvard=20Filistovi=C4=8D?= Date: Wed, 5 Aug 2026 17:30:46 +0300 Subject: [PATCH] [ln882h_ble_tracker] Automation triggers and actions (#17778) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .../components/ln882h_ble_tracker/__init__.py | 167 ++++++++++++++++- .../ln882h_ble_tracker/automation.h | 169 ++++++++++++++++++ .../ln882h_ble_tracker/ln882h_ble_tracker.cpp | 40 ++++- .../ln882h_ble_tracker/ln882h_ble_tracker.h | 17 ++ .../config/test_automations.yaml | 42 +++++ .../test_automations_codegen.py | 52 ++++++ .../test-automations.ln882x-ard.yaml | 33 ++++ 7 files changed, 517 insertions(+), 3 deletions(-) create mode 100644 esphome/components/ln882h_ble_tracker/automation.h create mode 100644 tests/component_tests/ln882h_ble_tracker/config/test_automations.yaml create mode 100644 tests/component_tests/ln882h_ble_tracker/test_automations_codegen.py create mode 100644 tests/components/ln882h_ble_tracker/test-automations.ln882x-ard.yaml diff --git a/esphome/components/ln882h_ble_tracker/__init__.py b/esphome/components/ln882h_ble_tracker/__init__.py index 30646dca9a..08d14d0de4 100644 --- a/esphome/components/ln882h_ble_tracker/__init__.py +++ b/esphome/components/ln882h_ble_tracker/__init__.py @@ -2,6 +2,7 @@ top of the ln882h_ble controller. With continuous: false nothing scans until an explicit start_scan() call.""" +from esphome import automation import esphome.codegen as cg from esphome.components import ble_device_base, ln882h_ble, ota from esphome.components.const import CONF_SCAN_PARAMETERS, CONF_WINDOW @@ -12,10 +13,19 @@ from esphome.const import ( CONF_DURATION, CONF_ID, CONF_INTERVAL, + CONF_MAC_ADDRESS, + CONF_MANUFACTURER_ID, + CONF_ON_BLE_ADVERTISE, + CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, + CONF_ON_BLE_SERVICE_DATA_ADVERTISE, + CONF_SERVICE_UUID, + CONF_TRIGGER_ID, ) +from esphome.core import ID from esphome.types import ConfigType CONF_LN882H_BLE_ID = "ln882h_ble_id" +CONF_ON_SCAN_END = "on_scan_end" DEPENDENCIES = ["ln882x"] AUTO_LOAD = ["ble_device_base", "ln882h_ble"] @@ -26,6 +36,31 @@ LN882HBLETracker = ln882h_ble_tracker_ns.class_( "LN882HBLETracker", ble_device_base.BLEHub, cg.Component ) +StartScanAction = ln882h_ble_tracker_ns.class_("StartScanAction", automation.Action) +StopScanAction = ln882h_ble_tracker_ns.class_("StopScanAction", automation.Action) + +ESPBTDeviceConstRef = ( + cg.esphome_ns.namespace("ble_device_base") + .class_("ESPBTDevice") + .operator("ref") + .operator("const") +) +ESPBTAdvertiseTrigger = ln882h_ble_tracker_ns.class_( + "ESPBTAdvertiseTrigger", automation.Trigger.template(ESPBTDeviceConstRef) +) +adv_data_t = cg.std_vector.template(cg.uint8) +adv_data_t_const_ref = adv_data_t.operator("ref").operator("const") +BLEServiceDataAdvertiseTrigger = ln882h_ble_tracker_ns.class_( + "BLEServiceDataAdvertiseTrigger", automation.Trigger.template(adv_data_t_const_ref) +) +BLEManufacturerDataAdvertiseTrigger = ln882h_ble_tracker_ns.class_( + "BLEManufacturerDataAdvertiseTrigger", + automation.Trigger.template(adv_data_t_const_ref), +) +BLEEndOfScanTrigger = ln882h_ble_tracker_ns.class_( + "BLEEndOfScanTrigger", automation.Trigger.template() +) + # LN882H SDK reference scan rate: 100 ms interval / 50 ms window (50 % duty). SCAN_PARAMETERS_SCHEMA = ble_device_base.scan_parameters_schema( @@ -33,15 +68,108 @@ SCAN_PARAMETERS_SCHEMA = ble_device_base.scan_parameters_schema( ) +# UUID string length -> setter width. 16/32-bit go out as plain hex literals, +# 128-bit as a reversed byte array (BLE wire order). Keyed exhaustively so an +# impossible length fails as a KeyError instead of silently picking a width +# (bt_uuid validation upstream only ever produces these three). +_UUID_WIDTHS = { + len(ble_device_base.BT_UUID16_FORMAT): "16", + len(ble_device_base.BT_UUID32_FORMAT): "32", + len(ble_device_base.BT_UUID128_FORMAT): "128", +} + CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(LN882HBLETracker), cv.GenerateID(CONF_LN882H_BLE_ID): cv.use_id(ln882h_ble.LN882HBLE), cv.Optional(CONF_SCAN_PARAMETERS, default={}): SCAN_PARAMETERS_SCHEMA, + cv.Optional(CONF_ON_BLE_ADVERTISE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPBTAdvertiseTrigger), + cv.Optional(CONF_MAC_ADDRESS): cv.ensure_list(cv.mac_address), + } + ), + cv.Optional(CONF_ON_BLE_SERVICE_DATA_ADVERTISE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLEServiceDataAdvertiseTrigger + ), + cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Required(CONF_SERVICE_UUID): ble_device_base.bt_uuid, + } + ), + cv.Optional( + CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE + ): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLEManufacturerDataAdvertiseTrigger + ), + cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Required(CONF_MANUFACTURER_ID): ble_device_base.bt_uuid, + } + ), + cv.Optional(CONF_ON_SCAN_END): automation.validate_automation( + {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BLEEndOfScanTrigger)} + ), } ).extend(cv.COMPONENT_SCHEMA) +# Triggers register as ble_device_base listeners in their constructors; count +# them where they are created so the StaticVector cannot be undersized. Shares +# the define with register_ble_device() via the core slot-counter factory. +_count_listener = cg.slot_counter(ble_device_base.LISTENER_COUNT_DEFINE) + + +@automation.register_action( + "ln882h_ble_tracker.start_scan", + StartScanAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(LN882HBLETracker), + cv.Optional(CONF_CONTINUOUS): cv.templatable(cv.boolean), + } + ), + synchronous=True, +) +async def start_scan_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: list, +) -> cg.MockObj: + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + if (continuous := config.get(CONF_CONTINUOUS)) is not None: + template_ = await cg.templatable(continuous, args, cg.bool_) + cg.add(var.set_continuous(template_)) + return var + + +@automation.register_action( + "ln882h_ble_tracker.stop_scan", + StopScanAction, + automation.maybe_simple_id( + cv.Schema( + { + cv.GenerateID(): cv.use_id(LN882HBLETracker), + } + ) + ), + synchronous=True, +) +async def stop_scan_action_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: list, +) -> cg.MockObj: + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -60,4 +188,41 @@ async def to_code(config: ConfigType) -> None: cg.add(var.set_scan_window(ble_device_base.to_ble_units(scan[CONF_WINDOW]))) cg.add(var.set_scan_duration(scan[CONF_DURATION].total_milliseconds)) cg.add(var.set_scan_active(scan[CONF_ACTIVE])) - cg.add(var.set_scan_continuous(scan[CONF_CONTINUOUS])) + cg.add(var.set_configured_continuous(scan[CONF_CONTINUOUS])) + + for conf in config.get(CONF_ON_BLE_ADVERTISE, []): + 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])) + await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf) + _count_listener() + + for trigger_key, uuid_key, setter_prefix in ( + (CONF_ON_BLE_SERVICE_DATA_ADVERTISE, CONF_SERVICE_UUID, "set_service_uuid"), + ( + CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, + CONF_MANUFACTURER_ID, + "set_manufacturer_uuid", + ), + ): + for conf in config.get(trigger_key, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + uuid = conf[uuid_key] + width = _UUID_WIDTHS[len(uuid)] + value = ( + ble_device_base.as_hex(uuid) + if width != "128" + else ble_device_base.as_reversed_hex_array(uuid) + ) + cg.add(getattr(trigger, f"{setter_prefix}{width}")(value)) + if (mac := conf.get(CONF_MAC_ADDRESS)) is not None: + cg.add(trigger.set_address(mac.as_hex)) + await automation.build_automation( + trigger, [(adv_data_t_const_ref, "x")], conf + ) + _count_listener() + + for conf in config.get(CONF_ON_SCAN_END, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + _count_listener() diff --git a/esphome/components/ln882h_ble_tracker/automation.h b/esphome/components/ln882h_ble_tracker/automation.h new file mode 100644 index 0000000000..43f526e064 --- /dev/null +++ b/esphome/components/ln882h_ble_tracker/automation.h @@ -0,0 +1,169 @@ +// Automation triggers and actions for ln882h_ble_tracker: triggers follow the +// esp32_ble_tracker design; only the scan-control actions are +// platform-specific. + +#pragma once + +#ifdef USE_LIBRETINY + +#include "ln882h_ble_tracker.h" + +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" + +#include +#include + +namespace esphome::ln882h_ble_tracker { + +template class StartScanAction final : public Action, public Parented { + public: + TEMPLATABLE_VALUE(bool, continuous) + void play(const Ts &...x) override { + // With continuous: set, the action wins. Without it, the configured value + // is used - stop_scan() clears the runtime flag permanently, so a bare + // stop_scan/start_scan pair would otherwise never resume continuous mode. + const bool want = + this->continuous_.has_value() ? this->continuous_.value(x...) : this->parent_->configured_continuous(); + if (this->parent_->scan_running()) { + // Same mode on a running scan is a no-op (esp32 parity): re-anchoring + // the duration window here would let a repeated action keep a one-shot + // scan alive forever. A real mode switch re-anchors so a change to + // one-shot runs a full duration from now. + if (want != this->parent_->scan_continuous()) { + this->parent_->set_scan_continuous(want); + this->parent_->restart_scan_duration(); + } + return; + } + this->parent_->set_scan_continuous(want); + this->parent_->start_scan(); + } +}; + +template class StopScanAction final : public Action, public Parented { + public: + void play(const Ts &...x) override { this->parent_->stop_scan(); } +}; + +// --------------------------------------------------------------------------- +// Automation triggers. +// +// Each trigger is a ble_device_base::ESPBTDeviceListener registered on the hub — +// the same design as esp32_ble_tracker, where the triggers sit in the listener +// list and their parse_device() return feeds the "Found device" suppression. +// --------------------------------------------------------------------------- + +// on_ble_advertise: fires on every BLE advertisement, optionally filtered to one or more MACs. +class ESPBTAdvertiseTrigger final : public Trigger, + public ble_device_base::ESPBTDeviceListener { + public: + explicit ESPBTAdvertiseTrigger(LN882HBLETracker *parent) { parent->register_listener(this); } + + void set_addresses(std::initializer_list addresses) { this->addresses_ = addresses; } + + bool parse_device(const ble_device_base::ESPBTDevice &device) override { + if (!this->addresses_.empty() && std::find(this->addresses_.begin(), this->addresses_.end(), + device.address_uint64()) == this->addresses_.end()) { + return false; + } + this->trigger(device); + return true; + } + + protected: + FixedVector addresses_; +}; + +// on_ble_service_data_advertise: fires when an advertisement contains service +// data for the given UUID. Optional single-MAC filter. +class BLEServiceDataAdvertiseTrigger final : public Trigger, + public ble_device_base::ESPBTDeviceListener { + public: + explicit BLEServiceDataAdvertiseTrigger(LN882HBLETracker *parent) { parent->register_listener(this); } + + void set_service_uuid16(uint64_t uuid) { + this->uuid_ = ble_device_base::ESPBTUUID::from_uint16(static_cast(uuid)); + } + void set_service_uuid32(uint64_t uuid) { + this->uuid_ = ble_device_base::ESPBTUUID::from_uint32(static_cast(uuid)); + } + void set_service_uuid128(const uint8_t *uuid) { this->uuid_ = ble_device_base::ESPBTUUID::from_raw(uuid); } + + void set_address(uint64_t address) { + this->address_ = address; + this->has_address_ = true; + } + + bool parse_device(const ble_device_base::ESPBTDevice &device) override { + if (this->has_address_ && device.address_uint64() != this->address_) { + return false; + } + for (const auto &sd : device.get_service_datas()) { + if (sd.uuid == this->uuid_) { + this->trigger(sd.data); + return true; + } + } + return false; + } + + protected: + ble_device_base::ESPBTUUID uuid_{}; + uint64_t address_{0}; + bool has_address_{false}; +}; + +// on_ble_manufacturer_data_advertise: fires when an advertisement contains +// manufacturer data for the given ID. Optional single-MAC filter. +class BLEManufacturerDataAdvertiseTrigger final : public Trigger, + public ble_device_base::ESPBTDeviceListener { + public: + explicit BLEManufacturerDataAdvertiseTrigger(LN882HBLETracker *parent) { parent->register_listener(this); } + + void set_manufacturer_uuid16(uint64_t uuid) { + this->uuid_ = ble_device_base::ESPBTUUID::from_uint16(static_cast(uuid)); + } + void set_manufacturer_uuid32(uint64_t uuid) { + this->uuid_ = ble_device_base::ESPBTUUID::from_uint32(static_cast(uuid)); + } + void set_manufacturer_uuid128(const uint8_t *uuid) { this->uuid_ = ble_device_base::ESPBTUUID::from_raw(uuid); } + + void set_address(uint64_t address) { + this->address_ = address; + this->has_address_ = true; + } + + bool parse_device(const ble_device_base::ESPBTDevice &device) override { + if (this->has_address_ && device.address_uint64() != this->address_) { + return false; + } + for (const auto &md : device.get_manufacturer_datas()) { + if (md.uuid == this->uuid_) { + this->trigger(md.data); + return true; + } + } + return false; + } + + protected: + ble_device_base::ESPBTUUID uuid_{}; + uint64_t address_{0}; + bool has_address_{false}; +}; + +// on_scan_end: fires whenever a scan period ends (duration elapsed or stop_scan +// called). A listener whose on_scan_end() hook fires the trigger — never claims +// devices (parse_device always returns false). +class BLEEndOfScanTrigger final : public Trigger<>, public ble_device_base::ESPBTDeviceListener { + public: + explicit BLEEndOfScanTrigger(LN882HBLETracker *parent) { parent->register_listener(this); } + + bool parse_device(const ble_device_base::ESPBTDevice &device) override { return false; } + void on_scan_end() override { this->trigger(); } +}; + +} // namespace esphome::ln882h_ble_tracker + +#endif // USE_LIBRETINY diff --git a/esphome/components/ln882h_ble_tracker/ln882h_ble_tracker.cpp b/esphome/components/ln882h_ble_tracker/ln882h_ble_tracker.cpp index b98ad228a8..90be341820 100644 --- a/esphome/components/ln882h_ble_tracker/ln882h_ble_tracker.cpp +++ b/esphome/components/ln882h_ble_tracker/ln882h_ble_tracker.cpp @@ -22,7 +22,10 @@ void LN882HBLETracker::setup() { // Receive the controller's scan reports; the controller queues them from the // rw task and delivers here on the main task. this->parent_->register_scan_listener(this); - if (!this->scan_continuous_) { + // scan_running_ check: an on_boot start_scan action (priority 600) runs + // before this setup() (200) and enable_loop() is a no-op pre-setup — parking + // the loop here would strand that already-running scan. + if (!this->scan_continuous_ && !this->scan_running_ && !this->pending_start_) { // Say so once: with continuous: false nothing scans until an explicit // start_scan() — silence here reads as a broken scanner. ESP_LOGD(TAG, "Scanning not started (continuous: false) - waiting for an explicit start_scan()"); @@ -61,6 +64,14 @@ void LN882HBLETracker::on_ota_global_state(ota::OTAState state, float progress, #endif // USE_OTA_STATE_LISTENER void LN882HBLETracker::loop() { + if (this->pending_start_) { + // A start_scan latched before the controller's setup(); safe now — loop() + // only runs after every component set up. + this->pending_start_ = false; + if (!this->scan_running_) { + this->start_scan_(); + } + } // Flush pending scannable advertisements whose scan response never arrived // (device didn't answer / frame lost) — delivered unmerged after the timeout. // Main-task only, like every consumer of pending_adv_. @@ -263,12 +274,34 @@ void LN882HBLETracker::process_adv_(const uint8_t *mac, int8_t rssi, uint8_t add void LN882HBLETracker::start_scan() { // Mirrors esp32_ble_tracker::start_scan(): caller sets scan_continuous_ via // set_scan_continuous() first, then calls start_scan() to begin scanning. + if (!this->parent_->is_ready()) { + // An on_boot automation (priority 600) runs before the controller's + // setup() has resolved the BLE MAC; scan_start() now would rw_init() the + // all-zero address and bring BLE up before WiFi. Latch; loop() applies + // the start once every setup() has run. + this->pending_start_ = true; + return; + } if (!this->scan_running_) { this->start_scan_(); } } +void LN882HBLETracker::restart_scan_duration() { + if (!this->scan_running_) + return; + // Re-anchor only the one-shot duration clock. scan_period_start_ (the + // continuous-mode on_scan_end period) is deliberately left alone: a + // start_scan action fired more often than scan_duration_ would otherwise + // suppress on_scan_end indefinitely — and absence detection (ble_rssi's NAN + // publish) rides on that period. + this->scan_start_time_ = millis(); +} + void LN882HBLETracker::stop_scan() { + // Cancel a start latched before the controller's setup(); without this an + // on_boot start_scan/stop_scan pair would still start at the first loop(). + this->pending_start_ = false; this->scan_continuous_ = false; this->stop_scan_(); } @@ -308,7 +341,10 @@ void LN882HBLETracker::stop_scan_() { // scanner failing to come back up. ESP_LOGD(TAG, "BLE scan stopped"); this->end_scan_period_(millis()); // also resets the period clock so on_scan_end does not double-fire - if (!this->scan_continuous_) { + // scan_running_ re-check: an on_scan_end automation runs synchronously inside + // end_scan_period_() and may have called start_scan() — parking the loop then + // would leave the radio scanning with no period timing or pending-adv sweep. + if (!this->scan_continuous_ && !this->scan_running_) { // Nothing left to time; start_scan_() re-enables the loop. this->disable_loop(); } diff --git a/esphome/components/ln882h_ble_tracker/ln882h_ble_tracker.h b/esphome/components/ln882h_ble_tracker/ln882h_ble_tracker.h index 43328a288d..1ad36c40d4 100644 --- a/esphome/components/ln882h_ble_tracker/ln882h_ble_tracker.h +++ b/esphome/components/ln882h_ble_tracker/ln882h_ble_tracker.h @@ -52,7 +52,22 @@ class LN882HBLETracker : public Component, void set_scan_interval(uint16_t scan_interval) { this->scan_interval_ = scan_interval; } void set_scan_window(uint16_t scan_window) { this->scan_window_ = scan_window; } void set_scan_duration(uint32_t scan_duration) { this->scan_duration_ = scan_duration; } + /// Set from YAML (scan_parameters.continuous); also the value + /// configured_continuous() reports and a bare start_scan action restores. + void set_configured_continuous(bool scan_continuous) { + this->scan_continuous_ = scan_continuous; + this->scan_continuous_configured_ = scan_continuous; + } + /// Runtime control (esp32_ble_tracker lambda parity): does not change the + /// configured value, so configured_continuous() still reports what YAML + /// asked for. void set_scan_continuous(bool scan_continuous) { this->scan_continuous_ = scan_continuous; } + bool scan_continuous() const { return this->scan_continuous_; } + bool configured_continuous() const { return this->scan_continuous_configured_; } + /// Re-anchor the one-shot duration clock of a running scan to now — used + /// when an action changes the scan mode without stopping the radio. The + /// continuous-mode on_scan_end period is deliberately not touched. + void restart_scan_duration(); // ---- Public scan control ---- // Mirrors esp32_ble_tracker: set_scan_continuous() + start_scan() / stop_scan(). @@ -125,6 +140,8 @@ class LN882HBLETracker : public Component, uint16_t scan_window_{80}; // 80 × 0.625 ms = 50 ms (SDK SCAN_WINDOW_DEF; 50/100 = 50 %) uint32_t scan_duration_{300000}; bool scan_continuous_{true}; + bool pending_start_{false}; // start_scan() latched before the controller's setup() + bool scan_continuous_configured_{true}; // YAML value; stop_scan() must not lose it #ifdef USE_OTA_STATE_LISTENER bool scan_continuous_before_ota_{false}; // continuous mode saved at OTA start, restored on OTA failure bool scan_running_before_ota_{false}; // one-shot scan running at OTA start, restarted on OTA failure diff --git a/tests/component_tests/ln882h_ble_tracker/config/test_automations.yaml b/tests/component_tests/ln882h_ble_tracker/config/test_automations.yaml new file mode 100644 index 0000000000..d83e6c0883 --- /dev/null +++ b/tests/component_tests/ln882h_ble_tracker/config/test_automations.yaml @@ -0,0 +1,42 @@ +esphome: + name: ln-trigger-codegen + on_boot: + then: + - ln882h_ble_tracker.start_scan: + continuous: true + # Bare form: restores the configured scan_parameters mode — no + # set_continuous emitted (asserted in the codegen test). + - ln882h_ble_tracker.start_scan: + - ln882h_ble_tracker.stop_scan: + +ln882x: + board: generic-ln882h + +ln882h_ble_tracker: + on_ble_advertise: + - mac_address: + - AC:37:43:77:5F:4C + - 11:22:33:44:55:66 + then: + - lambda: 'ESP_LOGD("t", "%s", x.address_str().c_str());' + on_ble_service_data_advertise: + - service_uuid: ABCDABCD-ABCD-ABCD-ABCD-ABCDABCDABCD + mac_address: AC:37:43:77:5F:4C + then: + - lambda: 'ESP_LOGD("t", "%zu", x.size());' + - service_uuid: ABCDABCD + then: + - lambda: 'ESP_LOGD("t", "%zu", x.size());' + on_ble_manufacturer_data_advertise: + - manufacturer_id: ABCD + then: + - lambda: 'ESP_LOGD("t", "%zu", x.size());' + - manufacturer_id: ABCDABCD + then: + - lambda: 'ESP_LOGD("t", "%zu", x.size());' + - manufacturer_id: ABCDABCD-ABCD-ABCD-ABCD-ABCDABCDABCD + then: + - lambda: 'ESP_LOGD("t", "%zu", x.size());' + on_scan_end: + - then: + - lambda: 'ESP_LOGD("t", "end");' diff --git a/tests/component_tests/ln882h_ble_tracker/test_automations_codegen.py b/tests/component_tests/ln882h_ble_tracker/test_automations_codegen.py new file mode 100644 index 0000000000..43c14c8054 --- /dev/null +++ b/tests/component_tests/ln882h_ble_tracker/test_automations_codegen.py @@ -0,0 +1,52 @@ +"""Codegen tests for the tracker automations: the generated main is the +automated check on the setter calls and the listener accounting (the +test.ln882x-ard.yaml compile fixture proves linkage, not codegen shape).""" + +from collections.abc import Callable +from pathlib import Path +import re + +from esphome.components import ble_device_base +from tests.component_tests.helpers import get_define_value + + +def test_trigger_codegen( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> 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 + # 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," + "0xCD,0xAB,0xCD,0xAB,0xCD,0xAB,0xCD,0xAB,0xCD,0xAB,0xCD,0xAB})" in main_cpp + ) + assert "set_address(0xAC3743775F4CULL)" in main_cpp + # 32-bit middle branch of the width dispatch + assert "set_service_uuid32(0xABCDABCDULL)" in main_cpp + # All three manufacturer widths: getattr() builds these names as strings, + # so a misspelling only ever fails here. + assert "set_manufacturer_uuid16(0xABCDULL)" in main_cpp + assert "set_manufacturer_uuid32(0xABCDABCDULL)" in main_cpp + assert ( + "set_manufacturer_uuid128((uint8_t*)(const uint8_t[16]){0xCD,0xAB,0xCD,0xAB," + "0xCD,0xAB,0xCD,0xAB,0xCD,0xAB,0xCD,0xAB,0xCD,0xAB,0xCD,0xAB})" in main_cpp + ) + # scan-control actions: templatable continuous lambda + parented actions. + # Exactly one set_continuous: the bare start_scan emits none, pinning the + # restore-configured-mode divergence from esp32 against a future default=. + assert main_cpp.count("->set_continuous(") == 1 + assert "startscanaction_id->set_continuous(" in main_cpp + assert "stopscanaction_id->set_parent(" in main_cpp + # Constructor call, not just the declaration: the parent argument is what + # registers the trigger as a listener. + assert re.search( + r"new\(\w+\) ln882h_ble_tracker::BLEEndOfScanTrigger\(\w+\)", main_cpp + ) + + # Seven triggers register as listeners; an undercount silently drops the + # last trigger at runtime (StaticVector::push_back past capacity), so the + # define is the assertion that matters most. + assert get_define_value(ble_device_base.LISTENER_COUNT_DEFINE) == "7" diff --git a/tests/components/ln882h_ble_tracker/test-automations.ln882x-ard.yaml b/tests/components/ln882h_ble_tracker/test-automations.ln882x-ard.yaml new file mode 100644 index 0000000000..3291ef9d8b --- /dev/null +++ b/tests/components/ln882h_ble_tracker/test-automations.ln882x-ard.yaml @@ -0,0 +1,33 @@ +packages: + ln882h_ble_tracker: !include common.yaml + +esphome: + on_boot: + then: + - ln882h_ble_tracker.start_scan + - ln882h_ble_tracker.start_scan: + continuous: true + - ln882h_ble_tracker.start_scan: + continuous: !lambda return false; + - ln882h_ble_tracker.stop_scan + +ln882h_ble_tracker: + on_ble_advertise: + - mac_address: AC:37:43:77:5F:4C + then: + - lambda: |- + ESP_LOGD("main", "The device address is %s", x.address_str().c_str()); + on_ble_service_data_advertise: + - service_uuid: ABCD + then: + - lambda: |- + ESP_LOGD("main", "Length of service data is %zu", x.size()); + on_ble_manufacturer_data_advertise: + - manufacturer_id: ABCD + then: + - lambda: |- + ESP_LOGD("main", "Length of manufacturer data is %zu", x.size()); + on_scan_end: + - then: + - lambda: |- + ESP_LOGD("main", "Scan ended");