[bthome] Refactor to use FixedVector and add features

Major refactoring to address memory efficiency, advertisement cycling,
and immediate advertising support:

**Use FixedVector instead of std::vector:**
- Replace std::vector with FixedVector for measurements storage
- Initialize with exact sizes determined from configuration
- Eliminates STL reallocation overhead and reduces flash usage
- Uses runtime-sized FixedVector allocated once in setup()

**Config key changes:**
- Use `sensors` and `binary_sensors` (plurals) for consistency
- Matches ESPHome conventions for sensor arrays

**Advertisement size management and cycling:**
- Calculate max advertisement size (31 bytes total, minus overhead)
- Split measurements across multiple packets if they don't fit
- Automatically cycle through packets on each advertising interval
- Ensures all sensors get advertised even with many measurements
- Overhead: 8 bytes unencrypted, 16 bytes encrypted

**Immediate advertising support:**
- Add `advertise_immediately` option for sensors/binary_sensors
- When enabled, triggers immediate advertisement on state change
- Interrupts normal advertising cycle to send only that sensor
- Resumes normal cycle after immediate advertisement
- Perfect for motion sensors, door sensors, or critical alerts

**Implementation details:**
- Refactored encode functions to use raw pointers and calculate sizes
- Build multiple advertisement packets as needed
- Track current packet index for cycling
- Handle immediate advertising with separate packet building path
- Proper encryption handling with per-packet counters

Example configuration:
```yaml
bthome:
  sensors:
    - type: temperature
      id: room_temp
    - type: humidity
      id: room_humidity
  binary_sensors:
    - type: motion
      id: pir_sensor
      advertise_immediately: true  # Instant notification
    - type: door
      id: front_door
      advertise_immediately: true
```
This commit is contained in:
Claude
2025-11-17 22:37:55 +00:00
parent da04307240
commit f81a3a8c64
5 changed files with 374 additions and 136 deletions
+29 -12
View File
@@ -4,8 +4,9 @@ from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components.esp32_ble import CONF_BLE_ID
import esphome.config_validation as cv
from esphome.const import (
CONF_BINARY_SENSOR,
CONF_BINARY_SENSORS,
CONF_ID,
CONF_SENSORS,
CONF_TX_POWER,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_BATTERY_CHARGING,
@@ -52,11 +53,11 @@ BTHome = bthome_ns.class_(
)
# Configuration constants
CONF_MEASUREMENT = "measurement"
CONF_ENCRYPTION_KEY = "encryption_key"
CONF_MIN_INTERVAL = "min_interval"
CONF_MAX_INTERVAL = "max_interval"
CONF_SENSOR_TYPE = "type"
CONF_ADVERTISE_IMMEDIATELY = "advertise_immediately"
# BTHome object IDs for sensors (mapping from device class to BTHome object ID)
SENSOR_DEVICE_CLASS_TO_OBJECT_ID = {
@@ -140,23 +141,25 @@ CONFIG_SCHEMA = cv.All(
cv.decibel, cv.enum(esp32_ble.TX_POWER_LEVELS, int=True)
),
cv.Optional(CONF_ENCRYPTION_KEY): validate_encryption_key,
cv.Optional(CONF_MEASUREMENT): cv.ensure_list(
cv.Optional(CONF_SENSORS): cv.ensure_list(
cv.Schema(
{
cv.Required(CONF_SENSOR_TYPE): cv.one_of(
*SENSOR_DEVICE_CLASS_TO_OBJECT_ID.keys(), lower=True
),
cv.Required(CONF_ID): cv.use_id(sensor.Sensor),
cv.Optional(CONF_ADVERTISE_IMMEDIATELY, default=False): cv.boolean,
}
)
),
cv.Optional(CONF_BINARY_SENSOR): cv.ensure_list(
cv.Optional(CONF_BINARY_SENSORS): cv.ensure_list(
cv.Schema(
{
cv.Required(CONF_SENSOR_TYPE): cv.one_of(
*BINARY_SENSOR_DEVICE_CLASS_TO_OBJECT_ID.keys(), lower=True
),
cv.Required(CONF_ID): cv.use_id(binary_sensor.BinarySensor),
cv.Optional(CONF_ADVERTISE_IMMEDIATELY, default=False): cv.boolean,
}
)
),
@@ -179,26 +182,40 @@ async def to_code(config):
cg.add(var.set_max_interval(config[CONF_MAX_INTERVAL]))
cg.add(var.set_tx_power(config[CONF_TX_POWER]))
# Initialize FixedVectors with proper sizes
num_sensors = len(config.get(CONF_SENSORS, []))
num_binary_sensors = len(config.get(CONF_BINARY_SENSORS, []))
max_packets = max(1, num_sensors + num_binary_sensors)
# Initialize the measurements and binary_measurements FixedVectors
cg.add(cg.RawExpression(f"{var}->measurements_.init({num_sensors})"))
cg.add(cg.RawExpression(f"{var}->binary_measurements_.init({num_binary_sensors})"))
cg.add(cg.RawExpression(f"{var}->adv_packets_.init({max_packets})"))
cg.add(cg.RawExpression(f"{var}->adv_packet_sizes_.init({max_packets})"))
if CONF_ENCRYPTION_KEY in config:
key = config[CONF_ENCRYPTION_KEY]
key_bytes = [int(key[i : i + 2], 16) for i in range(0, len(key), 2)]
cg.add(var.set_encryption_key(key_bytes))
key_bytes = [cg.RawExpression(f"0x{key[i:i+2]}") for i in range(0, len(key), 2)]
key_array = cg.RawExpression(f"std::array<uint8_t, 16>{{{', '.join(str(b) for b in key_bytes)}}}")
cg.add(var.set_encryption_key(key_array))
# Add sensor measurements
if CONF_MEASUREMENT in config:
for measurement in config[CONF_MEASUREMENT]:
if CONF_SENSORS in config:
for measurement in config[CONF_SENSORS]:
sensor_type = measurement[CONF_SENSOR_TYPE]
object_id = SENSOR_DEVICE_CLASS_TO_OBJECT_ID[sensor_type]
sens = await cg.get_variable(measurement[CONF_ID])
cg.add(var.add_measurement(sens, object_id))
advertise_immediately = measurement[CONF_ADVERTISE_IMMEDIATELY]
cg.add(var.add_measurement(sens, object_id, advertise_immediately))
# Add binary sensor measurements
if CONF_BINARY_SENSOR in config:
for measurement in config[CONF_BINARY_SENSOR]:
if CONF_BINARY_SENSORS in config:
for measurement in config[CONF_BINARY_SENSORS]:
sensor_type = measurement[CONF_SENSOR_TYPE]
object_id = BINARY_SENSOR_DEVICE_CLASS_TO_OBJECT_ID[sensor_type]
sens = await cg.get_variable(measurement[CONF_ID])
cg.add(var.add_binary_measurement(sens, object_id))
advertise_immediately = measurement[CONF_ADVERTISE_IMMEDIATELY]
cg.add(var.add_binary_measurement(sens, object_id, advertise_immediately))
cg.add_define("USE_ESP32_BLE_ADVERTISING")
File diff suppressed because it is too large Load Diff
+20 -12
View File
@@ -4,6 +4,7 @@
#include "esphome/components/esp32_ble/ble.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#ifdef USE_ESP32
@@ -13,7 +14,6 @@
#include <esp_gap_ble_api.h>
#include <array>
#include <vector>
namespace esphome {
namespace bthome {
@@ -23,11 +23,13 @@ using namespace esp32_ble;
struct SensorMeasurement {
sensor::Sensor *sensor;
uint8_t object_id;
bool advertise_immediately;
};
struct BinarySensorMeasurement {
binary_sensor::BinarySensor *sensor;
uint8_t object_id;
bool advertise_immediately;
};
class BTHome : public Component, public GAPEventHandler, public Parented<ESP32BLE> {
@@ -41,21 +43,22 @@ class BTHome : public Component, public GAPEventHandler, public Parented<ESP32BL
void set_max_interval(uint16_t val) { this->max_interval_ = val; }
void set_tx_power(esp_power_level_t val) { this->tx_power_ = val; }
void set_encryption_key(const std::vector<uint8_t> &key);
void add_measurement(sensor::Sensor *sensor, uint8_t object_id);
void add_binary_measurement(binary_sensor::BinarySensor *sensor, uint8_t object_id);
void set_encryption_key(const std::array<uint8_t, 16> &key);
void add_measurement(sensor::Sensor *sensor, uint8_t object_id, bool advertise_immediately);
void add_binary_measurement(binary_sensor::BinarySensor *sensor, uint8_t object_id, bool advertise_immediately);
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
protected:
void on_advertise_();
void build_advertisement_data_();
void encode_measurement_(std::vector<uint8_t> &data, uint8_t object_id, float value);
void encode_binary_measurement_(std::vector<uint8_t> &data, uint8_t object_id, bool value);
bool encrypt_payload_(const std::vector<uint8_t> &plaintext, std::vector<uint8_t> &ciphertext);
void build_advertisement_packets_();
size_t encode_measurement_(uint8_t *data, size_t max_len, uint8_t object_id, float value);
size_t encode_binary_measurement_(uint8_t *data, size_t max_len, uint8_t object_id, bool value);
bool encrypt_payload_(const uint8_t *plaintext, size_t plaintext_len, uint8_t *ciphertext, size_t *ciphertext_len);
void trigger_immediate_advertising_(uint8_t measurement_index, bool is_binary);
std::vector<SensorMeasurement> measurements_;
std::vector<BinarySensorMeasurement> binary_measurements_;
FixedVector<SensorMeasurement> measurements_;
FixedVector<BinarySensorMeasurement> binary_measurements_;
uint16_t min_interval_{};
uint16_t max_interval_{};
@@ -68,9 +71,14 @@ class BTHome : public Component, public GAPEventHandler, public Parented<ESP32BL
std::array<uint8_t, 16> encryption_key_{};
uint32_t counter_{0};
// Cached advertisement data
std::vector<uint8_t> adv_data_;
// Advertisement cycling support
FixedVector<std::unique_ptr<uint8_t[]>> adv_packets_; // Multiple advertisement packets
FixedVector<uint16_t> adv_packet_sizes_; // Size of each packet
uint8_t current_packet_index_{0};
bool data_changed_{true};
bool immediate_advertising_pending_{false};
uint8_t immediate_adv_measurement_index_{0};
bool immediate_adv_is_binary_{false};
};
} // namespace bthome
+3 -2
View File
@@ -18,15 +18,16 @@ binary_sensor:
name: "Test Door"
bthome:
measurement:
sensors:
- type: temperature
id: test_temperature
- type: humidity
id: test_humidity
- type: battery
id: test_battery
binary_sensor:
binary_sensors:
- type: motion
id: test_motion
advertise_immediately: true
- type: door
id: test_door
@@ -10,9 +10,10 @@ binary_sensor:
bthome:
encryption_key: "231d39c1d7cc1ab1aee224cd096db932"
measurement:
sensors:
- type: temperature
id: test_temperature
binary_sensor:
advertise_immediately: true
binary_sensors:
- type: motion
id: test_motion