mirror of
https://github.com/esphome/esphome.git
synced 2026-08-17 19:13:18 +08:00
[bk72xx_ble_tracker] Automation triggers and actions (#17776)
This commit is contained in:
@@ -12,18 +12,30 @@ Scan modes:
|
||||
Use this when the radio is dedicated to BLE.
|
||||
continuous: false — a started scan runs for `duration` ms, then stops. The
|
||||
FIRST start is external too: nothing in this component
|
||||
starts a non-continuous scan on boot, so until the
|
||||
automation actions land (follow-up PR) the radio stays
|
||||
idle. start_scan() is called from code (e.g. an api
|
||||
client-connected automation) so the single-core radio
|
||||
can service WiFi in between scans.
|
||||
starts a non-continuous scan on boot — the radio stays
|
||||
idle until bk72xx_ble_tracker.start_scan fires (e.g.
|
||||
from an api client-connected automation), so the
|
||||
single-core radio can service WiFi in between scans.
|
||||
"""
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import bk72xx_ble, ble_device_base, 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_CONTINUOUS, CONF_DURATION, CONF_ID, CONF_INTERVAL
|
||||
from esphome.const import (
|
||||
CONF_CONTINUOUS,
|
||||
CONF_DURATION,
|
||||
CONF_ID,
|
||||
CONF_INTERVAL,
|
||||
CONF_MANUFACTURER_ID,
|
||||
CONF_ON_BLE_ADVERTISE,
|
||||
CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
|
||||
CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
|
||||
CONF_SERVICE_UUID,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CONF_BK72XX_BLE_ID = "bk72xx_ble_id"
|
||||
@@ -37,6 +49,14 @@ BK72xxBLETracker = bk72xx_ble_tracker_ns.class_(
|
||||
"BK72xxBLETracker", ble_device_base.BLEHub, cg.Component
|
||||
)
|
||||
|
||||
StartScanAction = bk72xx_ble_tracker_ns.class_("StartScanAction", automation.Action)
|
||||
StopScanAction = bk72xx_ble_tracker_ns.class_("StopScanAction", automation.Action)
|
||||
|
||||
ESPBTAdvertiseTrigger = ble_automation.ESPBTAdvertiseTrigger
|
||||
BLEServiceDataAdvertiseTrigger = ble_automation.BLEServiceDataAdvertiseTrigger
|
||||
BLEManufacturerDataAdvertiseTrigger = ble_automation.BLEManufacturerDataAdvertiseTrigger
|
||||
BLEEndOfScanTrigger = ble_automation.BLEEndOfScanTrigger
|
||||
|
||||
|
||||
# interval defaults to the BK reference scan rate — 100 ms with the shared 30 ms
|
||||
# window, a 30 % duty cycle. Converted to the controller's 0.625 ms BLE units in
|
||||
@@ -48,10 +68,79 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.GenerateID(): cv.declare_id(BK72xxBLETracker),
|
||||
cv.GenerateID(CONF_BK72XX_BLE_ID): cv.use_id(bk72xx_ble.BK72xxBLE),
|
||||
cv.Optional(CONF_SCAN_PARAMETERS, default={}): SCAN_PARAMETERS_SCHEMA,
|
||||
cv.Optional(CONF_ON_BLE_ADVERTISE): ble_automation.advertise_trigger_schema(
|
||||
ESPBTAdvertiseTrigger
|
||||
),
|
||||
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
|
||||
): ble_automation.uuid_trigger_schema(
|
||||
BLEManufacturerDataAdvertiseTrigger,
|
||||
{cv.Required(CONF_MANUFACTURER_ID): ble_device_base.bt_uuid},
|
||||
),
|
||||
cv.Optional(CONF_ON_SCAN_END): ble_automation.scan_end_trigger_schema(
|
||||
BLEEndOfScanTrigger
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"bk72xx_ble_tracker.start_scan",
|
||||
StartScanAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(BK72xxBLETracker),
|
||||
# Optional with no default, unlike esp32_ble_tracker: omitting it
|
||||
# keeps whatever scan_parameters.continuous configured, instead of
|
||||
# silently forcing one-shot.
|
||||
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(
|
||||
"bk72xx_ble_tracker.stop_scan",
|
||||
StopScanAction,
|
||||
automation.maybe_simple_id(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(BK72xxBLETracker),
|
||||
}
|
||||
)
|
||||
),
|
||||
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)
|
||||
@@ -69,4 +158,23 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_scan_interval(ble_device_base.to_ble_units(scan[CONF_INTERVAL])))
|
||||
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_continuous(scan[CONF_CONTINUOUS]))
|
||||
cg.add(var.set_configured_continuous(scan[CONF_CONTINUOUS]))
|
||||
|
||||
for conf in config.get(CONF_ON_BLE_ADVERTISE, []):
|
||||
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"),
|
||||
(
|
||||
CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
|
||||
CONF_MANUFACTURER_ID,
|
||||
"set_manufacturer_uuid",
|
||||
),
|
||||
):
|
||||
for conf in config.get(trigger_key, []):
|
||||
await ble_automation.uuid_trigger_to_code(
|
||||
conf, var, uuid_key, setter_prefix
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_SCAN_END, []):
|
||||
await ble_automation.scan_end_trigger_to_code(conf, var)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Automation triggers and actions for bk72xx_ble_tracker: triggers are the
|
||||
// neutral ble_device_base classes; only the scan-control actions are
|
||||
// platform-specific.
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_LIBRETINY
|
||||
|
||||
#include "bk72xx_ble_tracker.h"
|
||||
|
||||
#include "esphome/components/ble_device_base/automation.h"
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
namespace esphome::bk72xx_ble_tracker {
|
||||
|
||||
template<typename... Ts> class StartScanAction final : public Action<Ts...>, public Parented<BK72xxBLETracker> {
|
||||
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<typename... Ts> class StopScanAction final : public Action<Ts...>, public Parented<BK72xxBLETracker> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->stop_scan(); }
|
||||
};
|
||||
|
||||
} // namespace esphome::bk72xx_ble_tracker
|
||||
|
||||
#endif // USE_LIBRETINY
|
||||
@@ -210,6 +210,17 @@ void BK72xxBLETracker::start_scan() {
|
||||
this->try_start_with_backoff_(millis(), /* force= */ true);
|
||||
}
|
||||
|
||||
void BK72xxBLETracker::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 BK72xxBLETracker::stop_scan() {
|
||||
this->scan_continuous_ = false;
|
||||
this->scan_requested_ = false; // also cancels a pending (not yet successful) start
|
||||
|
||||
@@ -70,7 +70,22 @@ class BK72xxBLETracker : public Component,
|
||||
void set_scan_interval(uint32_t scan_interval) { this->scan_interval_ = scan_interval; }
|
||||
void set_scan_window(uint32_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().
|
||||
@@ -134,6 +149,7 @@ class BK72xxBLETracker : public Component,
|
||||
uint32_t scan_window_{48}; // 48 × 0.625 ms = 30 ms (30/100 = 30 %)
|
||||
uint32_t scan_duration_{300000};
|
||||
bool scan_continuous_{true};
|
||||
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_requested_before_ota_{false}; // pending one-shot latch saved at OTA start, re-latched on OTA failure
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// Platform-neutral BLE advertisement triggers: ESPBTDeviceListener subclasses
|
||||
// registered on a BLEHub, exposed by each tracker under its own automation
|
||||
// names. parse_device()'s return feeds the "Found device" suppression.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ble_device.h"
|
||||
#include "ble_hub.h"
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <initializer_list>
|
||||
|
||||
namespace esphome::ble_device_base {
|
||||
|
||||
// on_ble_advertise: fires on every BLE advertisement, optionally filtered to one or more MACs.
|
||||
class ESPBTAdvertiseTrigger final : public Trigger<const ESPBTDevice &>, public ESPBTDeviceListener {
|
||||
public:
|
||||
explicit ESPBTAdvertiseTrigger(BLEHub *parent) { parent->register_listener(this); }
|
||||
|
||||
void set_addresses(std::initializer_list<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()) {
|
||||
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 adv_data_t &>, public ESPBTDeviceListener {
|
||||
public:
|
||||
explicit BLEServiceDataAdvertiseTrigger(BLEHub *parent) { parent->register_listener(this); }
|
||||
|
||||
void set_service_uuid16(uint64_t uuid) { this->uuid_ = ESPBTUUID::from_uint16(static_cast<uint16_t>(uuid)); }
|
||||
void set_service_uuid32(uint64_t uuid) { this->uuid_ = ESPBTUUID::from_uint32(static_cast<uint32_t>(uuid)); }
|
||||
void set_service_uuid128(const uint8_t *uuid) { this->uuid_ = ESPBTUUID::from_raw(uuid); }
|
||||
|
||||
void set_address(uint64_t address) {
|
||||
this->address_ = address;
|
||||
this->has_address_ = true;
|
||||
}
|
||||
|
||||
bool parse_device(const 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:
|
||||
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 adv_data_t &>, public ESPBTDeviceListener {
|
||||
public:
|
||||
explicit BLEManufacturerDataAdvertiseTrigger(BLEHub *parent) { parent->register_listener(this); }
|
||||
|
||||
void set_manufacturer_uuid16(uint64_t uuid) { this->uuid_ = ESPBTUUID::from_uint16(static_cast<uint16_t>(uuid)); }
|
||||
void set_manufacturer_uuid32(uint64_t uuid) { this->uuid_ = ESPBTUUID::from_uint32(static_cast<uint32_t>(uuid)); }
|
||||
void set_manufacturer_uuid128(const uint8_t *uuid) { this->uuid_ = ESPBTUUID::from_raw(uuid); }
|
||||
|
||||
void set_address(uint64_t address) {
|
||||
this->address_ = address;
|
||||
this->has_address_ = true;
|
||||
}
|
||||
|
||||
bool parse_device(const 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:
|
||||
ESPBTUUID uuid_{};
|
||||
uint64_t address_{0};
|
||||
bool has_address_{false};
|
||||
};
|
||||
|
||||
// on_scan_end: fires whenever a scan period ends (duration elapsed or stop
|
||||
// requested). A listener whose on_scan_end() hook fires the trigger — never
|
||||
// claims devices (parse_device always returns false).
|
||||
class BLEEndOfScanTrigger final : public Trigger<>, public ESPBTDeviceListener {
|
||||
public:
|
||||
explicit BLEEndOfScanTrigger(BLEHub *parent) { parent->register_listener(this); }
|
||||
|
||||
bool parse_device(const ESPBTDevice &device) override { return false; }
|
||||
void on_scan_end() override { this->trigger(); }
|
||||
};
|
||||
|
||||
} // namespace esphome::ble_device_base
|
||||
@@ -0,0 +1,128 @@
|
||||
"""Shared codegen for the neutral BLE advertisement triggers (automation.h)."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_MAC_ADDRESS, CONF_TRIGGER_ID
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import (
|
||||
BT_UUID16_FORMAT,
|
||||
BT_UUID32_FORMAT,
|
||||
BT_UUID128_FORMAT,
|
||||
LISTENER_COUNT_DEFINE,
|
||||
as_hex,
|
||||
as_reversed_hex_array,
|
||||
ble_device_base_ns,
|
||||
)
|
||||
|
||||
adv_data_t = cg.std_vector.template(cg.uint8)
|
||||
adv_data_t_const_ref = adv_data_t.operator("ref").operator("const")
|
||||
ESPBTDeviceConstRef = (
|
||||
ble_device_base_ns.class_("ESPBTDevice").operator("ref").operator("const")
|
||||
)
|
||||
|
||||
ESPBTAdvertiseTrigger = ble_device_base_ns.class_(
|
||||
"ESPBTAdvertiseTrigger", automation.Trigger.template(ESPBTDeviceConstRef)
|
||||
)
|
||||
BLEServiceDataAdvertiseTrigger = ble_device_base_ns.class_(
|
||||
"BLEServiceDataAdvertiseTrigger", automation.Trigger.template(adv_data_t_const_ref)
|
||||
)
|
||||
BLEManufacturerDataAdvertiseTrigger = ble_device_base_ns.class_(
|
||||
"BLEManufacturerDataAdvertiseTrigger",
|
||||
automation.Trigger.template(adv_data_t_const_ref),
|
||||
)
|
||||
BLEEndOfScanTrigger = ble_device_base_ns.class_(
|
||||
"BLEEndOfScanTrigger", automation.Trigger.template()
|
||||
)
|
||||
|
||||
# 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(BT_UUID16_FORMAT): "16",
|
||||
len(BT_UUID32_FORMAT): "32",
|
||||
len(BT_UUID128_FORMAT): "128",
|
||||
}
|
||||
|
||||
|
||||
def uuid_trigger_schema(
|
||||
trigger_class: MockObjClass, extra: dict[Any, Any] | None = None
|
||||
):
|
||||
"""Schema for a UUID-filtered trigger — pairs with uuid_trigger_to_code().
|
||||
|
||||
`extra` carries the required UUID key (a cv marker, so a dict rather than
|
||||
**kwargs); the optional single-mac filter is what uuid_trigger_to_code()
|
||||
reads back.
|
||||
"""
|
||||
return automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(trigger_class),
|
||||
cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
**(extra or {}),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def advertise_trigger_schema(trigger_class: MockObjClass):
|
||||
"""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),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def scan_end_trigger_schema(trigger_class: MockObjClass):
|
||||
"""on_scan_end schema: id only — pairs with scan_end_trigger_to_code()."""
|
||||
return automation.validate_automation(
|
||||
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(trigger_class)}
|
||||
)
|
||||
|
||||
|
||||
# Triggers register as ble_device_base listeners in their constructors; count
|
||||
# them where they are created so no backend can undercount the StaticVector
|
||||
# (push_back past capacity drops silently). Shares the define with
|
||||
# register_ble_device() via the core slot-counter factory.
|
||||
_count_listener = cg.slot_counter(LISTENER_COUNT_DEFINE)
|
||||
|
||||
|
||||
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]))
|
||||
await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf)
|
||||
_count_listener()
|
||||
|
||||
|
||||
async def scan_end_trigger_to_code(conf: ConfigType, var: cg.MockObj) -> None:
|
||||
"""Build an on_scan_end trigger."""
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
_count_listener()
|
||||
|
||||
|
||||
async def uuid_trigger_to_code(
|
||||
conf: ConfigType, var: cg.MockObj, key: str, setter_prefix: str
|
||||
) -> None:
|
||||
"""Build a UUID-filtered advertise trigger.
|
||||
|
||||
The UUID width picks the setter: 16-/32-bit go out as a plain hex literal,
|
||||
128-bit as a reversed byte array (BLE wire order).
|
||||
"""
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
uuid = conf[key]
|
||||
width = _UUID_WIDTHS[len(uuid)]
|
||||
value = as_hex(uuid) if width != "128" else 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()
|
||||
@@ -27,6 +27,7 @@ CONF_LOOP = "loop"
|
||||
CONF_NOX_INDEX = "nox_index"
|
||||
CONF_ON_PACKET = "on_packet"
|
||||
CONF_ON_RECEIVE = "on_receive"
|
||||
CONF_ON_SCAN_END = "on_scan_end"
|
||||
CONF_ON_STATE_CHANGE = "on_state_change"
|
||||
CONF_PARITY = "parity"
|
||||
CONF_RECEIVER_FREQUENCY = "receiver_frequency"
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import ble_device_base, esp32_ble, ota
|
||||
from esphome.components.const import CONF_SCAN_PARAMETERS, CONF_WINDOW
|
||||
from esphome.components.const import CONF_ON_SCAN_END, CONF_SCAN_PARAMETERS, CONF_WINDOW
|
||||
from esphome.components.esp32 import (
|
||||
add_idf_sdkconfig_option,
|
||||
request_bluetooth,
|
||||
@@ -44,7 +44,6 @@ DEPENDENCIES = ["esp32"]
|
||||
CODEOWNERS = ["@bdraco"]
|
||||
|
||||
CONF_ESP32_BLE_ID = "esp32_ble_id"
|
||||
CONF_ON_SCAN_END = "on_scan_end"
|
||||
CONF_SOFTWARE_COEXISTENCE = "software_coexistence"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
esphome:
|
||||
name: bk-trigger-codegen
|
||||
on_boot:
|
||||
then:
|
||||
- bk72xx_ble_tracker.start_scan:
|
||||
continuous: true
|
||||
- bk72xx_ble_tracker.stop_scan
|
||||
|
||||
bk72xx:
|
||||
board: cb2s
|
||||
|
||||
bk72xx_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");'
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Codegen tests for the tracker automations.
|
||||
|
||||
The shared trigger classes (ble_device_base/automation.h) are compiled by every
|
||||
esp32 BLE compile test via AUTO_LOAD, but the BK-specific side — automation.h's
|
||||
action templates and restart_scan_duration() — compiles on no CI board (the
|
||||
bk72xx base board generic-bk7252 is BLE 4.2 and cannot build the tracker), and
|
||||
validate fixtures never run to_code. The generated main is therefore the only
|
||||
automated check on the setter spellings and the listener accounting."""
|
||||
|
||||
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
|
||||
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+\) ble_device_base::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"
|
||||
@@ -0,0 +1,52 @@
|
||||
packages:
|
||||
bk72xx_ble_tracker: !include common.yaml
|
||||
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- bk72xx_ble_tracker.start_scan
|
||||
- bk72xx_ble_tracker.start_scan:
|
||||
continuous: true
|
||||
- bk72xx_ble_tracker.stop_scan
|
||||
- bk72xx_ble_tracker.stop_scan: ble_tracker
|
||||
|
||||
bk72xx_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());
|
||||
- mac_address:
|
||||
- AC:37:43:77:5F:4C
|
||||
- AC:37:43:77:5F:4D
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("main", "The device address is %s", x.address_str().c_str());
|
||||
on_ble_service_data_advertise:
|
||||
- service_uuid: ABCD
|
||||
# mac_address exercises the UUID triggers' set_address() codegen branch.
|
||||
mac_address: AC:37:43:77:5F:4C
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("main", "Length of service data is %zu", x.size());
|
||||
- service_uuid: ABCDABCD
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("main", "32-bit service data is %zu", x.size());
|
||||
- service_uuid: ABCDABCD-ABCD-ABCD-ABCD-ABCDABCDABCD
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("main", "128-bit 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());
|
||||
- manufacturer_id: ABCDABCD-ABCD-ABCD-ABCD-ABCDABCDABCD
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("main", "128-bit manufacturer data is %zu", x.size());
|
||||
on_scan_end:
|
||||
- then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("main", "Scan ended");
|
||||
Reference in New Issue
Block a user