[ln882h_ble_tracker] Use the shared ble_device_base automation layer (#18089)

This commit is contained in:
Edvard Filistovič
2026-08-05 16:46:01 +00:00
committed by GitHub
parent b93b21eab1
commit 9bfce75bc5
8 changed files with 43 additions and 213 deletions
@@ -8,8 +8,8 @@
#include "bk72xx_ble_tracker.h"
#include "esphome/components/ble_device_base/automation.h"
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
namespace esphome::bk72xx_ble_tracker {
@@ -5,7 +5,8 @@ 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
from esphome.components.ble_device_base import automation as ble_automation
from esphome.components.const import CONF_ON_SCAN_END, CONF_SCAN_PARAMETERS, CONF_WINDOW
import esphome.config_validation as cv
from esphome.const import (
CONF_ACTIVE,
@@ -13,19 +14,16 @@ 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"]
@@ -39,27 +37,10 @@ LN882HBLETracker = ln882h_ble_tracker_ns.class_(
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()
)
ESPBTAdvertiseTrigger = ble_automation.ESPBTAdvertiseTrigger
BLEServiceDataAdvertiseTrigger = ble_automation.BLEServiceDataAdvertiseTrigger
BLEManufacturerDataAdvertiseTrigger = ble_automation.BLEManufacturerDataAdvertiseTrigger
BLEEndOfScanTrigger = ble_automation.BLEEndOfScanTrigger
# LN882H SDK reference scan rate: 100 ms interval / 50 ms window (50 % duty).
@@ -68,60 +49,33 @@ 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_ADVERTISE): ble_automation.advertise_trigger_schema(
ESPBTAdvertiseTrigger
),
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_SERVICE_DATA_ADVERTISE
): ble_automation.uuid_trigger_schema(
BLEServiceDataAdvertiseTrigger,
{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,
}
): ble_automation.uuid_trigger_schema(
BLEManufacturerDataAdvertiseTrigger,
{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)}
cv.Optional(CONF_ON_SCAN_END): ble_automation.scan_end_trigger_schema(
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,
@@ -191,11 +145,7 @@ async def to_code(config: ConfigType) -> None:
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()
await ble_automation.advertise_trigger_to_code(conf, var)
for trigger_key, uuid_key, setter_prefix in (
(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, CONF_SERVICE_UUID, "set_service_uuid"),
@@ -206,23 +156,9 @@ async def to_code(config: ConfigType) -> None:
),
):
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)
await ble_automation.uuid_trigger_to_code(
conf, var, uuid_key, setter_prefix
)
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()
await ble_automation.scan_end_trigger_to_code(conf, var)
@@ -1,6 +1,5 @@
// Automation triggers and actions for ln882h_ble_tracker: triggers follow the
// esp32_ble_tracker design; only the scan-control actions are
// platform-specific.
// Scan-control actions for ln882h_ble_tracker. The automation triggers are the
// neutral ble_device_base classes (ble_device_base/automation.h).
#pragma once
@@ -11,9 +10,6 @@
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
#include <algorithm>
#include <initializer_list>
namespace esphome::ln882h_ble_tracker {
template<typename... Ts> class StartScanAction final : public Action<Ts...>, public Parented<LN882HBLETracker> {
@@ -46,124 +42,6 @@ template<typename... Ts> class StopScanAction final : public Action<Ts...>, publ
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<const ble_device_base::ESPBTDevice &>,
public ble_device_base::ESPBTDeviceListener {
public:
explicit ESPBTAdvertiseTrigger(LN882HBLETracker *parent) { parent->register_listener(this); }
void set_addresses(std::initializer_list<uint64_t> 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<uint64_t> 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<const ble_device_base::adv_data_t &>,
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<uint16_t>(uuid));
}
void set_service_uuid32(uint64_t uuid) {
this->uuid_ = ble_device_base::ESPBTUUID::from_uint32(static_cast<uint32_t>(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<const ble_device_base::adv_data_t &>,
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<uint16_t>(uuid));
}
void set_manufacturer_uuid32(uint64_t uuid) {
this->uuid_ = ble_device_base::ESPBTUUID::from_uint32(static_cast<uint32_t>(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
@@ -4,12 +4,17 @@ esphome:
then:
- bk72xx_ble_tracker.start_scan:
continuous: true
# Bare form: restores the configured scan_parameters mode — no
# set_continuous emitted (asserted in the codegen test).
- bk72xx_ble_tracker.start_scan:
- bk72xx_ble_tracker.stop_scan
bk72xx:
board: cb2s
bk72xx_ble_tracker:
scan_parameters:
continuous: false
on_ble_advertise:
- mac_address:
- AC:37:43:77:5F:4C
@@ -39,9 +39,15 @@ def test_trigger_codegen(
"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
# 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
# scan_parameters continuous: false reaches the YAML-mode setter, not the
# runtime override.
assert "->set_configured_continuous(false)" in main_cpp
# Constructor call, not just the declaration: the parent argument is what
# registers the trigger as a listener.
assert re.search(
@@ -13,6 +13,8 @@ ln882x:
board: generic-ln882h
ln882h_ble_tracker:
scan_parameters:
continuous: false
on_ble_advertise:
- mac_address:
- AC:37:43:77:5F:4C
@@ -40,10 +40,13 @@ def test_trigger_codegen(
assert main_cpp.count("->set_continuous(") == 1
assert "startscanaction_id->set_continuous(" in main_cpp
assert "stopscanaction_id->set_parent(" in main_cpp
# scan_parameters continuous: false reaches the YAML-mode setter, not the
# runtime override.
assert "->set_configured_continuous(false)" 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
r"new\(\w+\) ble_device_base::BLEEndOfScanTrigger\(\w+\)", main_cpp
)
# Seven triggers register as listeners; an undercount silently drops the