[bthome] Add BTHome v2 BLE protocol support

Implements native BTHome v2 protocol support for broadcasting sensor data
over BLE advertisements. This enables ESP32 devices to act as battery-powered
BLE sensors that are natively compatible with Home Assistant.

Features:
- Support for multiple sensor types (temperature, humidity, battery, etc.)
- Support for binary sensors (motion, door, window, etc.)
- AES-CCM encryption support with 16-byte keys
- Automatic packet encoding per BTHome v2 specification
- Dynamic advertisement updates when sensor values change
- Configurable advertising intervals and TX power
- Proper BTHome v2 device info byte and service UUID

Implementation:
- Python configuration for sensor/binary sensor mapping
- C++ component for BTHome packet encoding and BLE advertising
- mbedtls CCM encryption for secure advertisements
- Leverages existing esp32_ble infrastructure

Test coverage:
- Basic sensor/binary sensor configuration tests
- Encryption test configuration
- Multi-platform support (ESP32, ESP32-C3, ESP32-S3 with IDF)

Closes: https://github.com/orgs/esphome/discussions/3235
This commit is contained in:
Claude
2025-11-17 21:42:08 +00:00
parent 3d6c361037
commit 227c46403b
8 changed files with 730 additions and 0 deletions
+233
View File
@@ -0,0 +1,233 @@
import esphome.codegen as cg
from esphome.components import binary_sensor, esp32_ble, sensor
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_ID,
CONF_BATTERY_LEVEL,
CONF_HUMIDITY,
CONF_ILLUMINANCE,
CONF_PRESSURE,
CONF_TEMPERATURE,
CONF_TX_POWER,
)
from esphome.core import TimePeriod
AUTO_LOAD = ["esp32_ble"]
DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@esphome/core"]
bthome_ns = cg.esphome_ns.namespace("bthome")
BTHome = bthome_ns.class_(
"BTHome",
cg.Component,
esp32_ble.GAPEventHandler,
cg.Parented.template(esp32_ble.ESP32BLE),
)
# Configuration constants
CONF_MEASUREMENT = "measurement"
CONF_ENCRYPTION_KEY = "encryption_key"
CONF_MIN_INTERVAL = "min_interval"
CONF_MAX_INTERVAL = "max_interval"
CONF_SENSOR_TYPE = "type"
# Sensor type constants
CONF_BATTERY = "battery"
CONF_CO2 = "co2"
CONF_COUNT = "count"
CONF_CURRENT = "current"
CONF_DEWPOINT = "dewpoint"
CONF_DISTANCE = "distance"
CONF_ENERGY = "energy"
CONF_GAS = "gas"
CONF_MASS = "mass"
CONF_MOISTURE = "moisture"
CONF_PM25 = "pm2_5"
CONF_PM10 = "pm10"
CONF_POWER = "power"
CONF_ROTATION = "rotation"
CONF_SPEED = "speed"
CONF_TIMESTAMP = "timestamp"
CONF_TVOC = "tvoc"
CONF_VOLTAGE = "voltage"
CONF_VOLUME = "volume"
# Binary sensor types
CONF_BINARY_SENSOR = "binary_sensor"
CONF_BATTERY_LOW = "battery_low"
CONF_BATTERY_CHARGING = "battery_charging"
CONF_CO = "carbon_monoxide"
CONF_COLD = "cold"
CONF_CONNECTIVITY = "connectivity"
CONF_DOOR = "door"
CONF_GARAGE_DOOR = "garage_door"
CONF_GAS_DETECTED = "gas"
CONF_GENERIC_BOOLEAN = "generic_boolean"
CONF_HEAT = "heat"
CONF_LIGHT = "light"
CONF_LOCK = "lock"
CONF_MOISTURE_DETECTED = "moisture"
CONF_MOTION = "motion"
CONF_MOVING = "moving"
CONF_OCCUPANCY = "occupancy"
CONF_OPENING = "opening"
CONF_PLUG = "plug"
CONF_POWER_ON = "power"
CONF_PRESENCE = "presence"
CONF_PROBLEM = "problem"
CONF_RUNNING = "running"
CONF_SAFETY = "safety"
CONF_SMOKE = "smoke"
CONF_SOUND = "sound"
CONF_TAMPER = "tamper"
CONF_VIBRATION = "vibration"
CONF_WINDOW = "window"
# BTHome object IDs for sensors
SENSOR_OBJECT_IDS = {
CONF_BATTERY: 0x01,
CONF_TEMPERATURE: 0x02, # 0.01°C
CONF_HUMIDITY: 0x03,
CONF_PRESSURE: 0x04,
CONF_ILLUMINANCE: 0x05,
CONF_MASS: 0x06,
CONF_DEWPOINT: 0x08,
CONF_ENERGY: 0x0A,
CONF_POWER: 0x0B,
CONF_VOLTAGE: 0x0C,
CONF_PM25: 0x0D,
CONF_PM10: 0x0E,
CONF_CO2: 0x12,
CONF_TVOC: 0x13,
CONF_MOISTURE: 0x14,
CONF_CURRENT: 0x43,
CONF_SPEED: 0x44,
CONF_TIMESTAMP: 0x50,
}
# BTHome object IDs for binary sensors
BINARY_SENSOR_OBJECT_IDS = {
CONF_GENERIC_BOOLEAN: 0x0F,
CONF_POWER_ON: 0x10,
CONF_BATTERY_LOW: 0x15,
CONF_BATTERY_CHARGING: 0x16,
CONF_CO: 0x17,
CONF_COLD: 0x18,
CONF_DOOR: 0x1A,
CONF_GARAGE_DOOR: 0x1B,
CONF_LIGHT: 0x1E,
CONF_LOCK: 0x1F,
CONF_MOTION: 0x21,
CONF_OCCUPANCY: 0x23,
CONF_SMOKE: 0x29,
CONF_WINDOW: 0x2D,
}
def validate_config(config):
if config[CONF_MIN_INTERVAL] > config.get(CONF_MAX_INTERVAL):
raise cv.Invalid("min_interval must be <= max_interval")
return config
def validate_encryption_key(value):
value = cv.string_strict(value)
# Remove any spaces or dashes
value = value.replace(" ", "").replace("-", "")
# Must be 32 hex characters (16 bytes)
if len(value) != 32:
raise cv.Invalid("Encryption key must be 32 hexadecimal characters (16 bytes)")
try:
int(value, 16)
except ValueError as e:
raise cv.Invalid("Encryption key must be valid hexadecimal") from e
return value.lower()
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(BTHome),
cv.GenerateID(CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
cv.Optional(CONF_MIN_INTERVAL, default="1s"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(
min=TimePeriod(milliseconds=20), max=TimePeriod(milliseconds=10240)
),
),
cv.Optional(CONF_MAX_INTERVAL, default="1s"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(
min=TimePeriod(milliseconds=20), max=TimePeriod(milliseconds=10240)
),
),
cv.Optional(CONF_TX_POWER, default="3dBm"): 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.Schema(
{
cv.Required(CONF_SENSOR_TYPE): cv.one_of(
*SENSOR_OBJECT_IDS.keys(), lower=True
),
cv.Required(CONF_ID): cv.use_id(sensor.Sensor),
}
)
),
cv.Optional(CONF_BINARY_SENSOR): cv.ensure_list(
cv.Schema(
{
cv.Required(CONF_SENSOR_TYPE): cv.one_of(
*BINARY_SENSOR_OBJECT_IDS.keys(), lower=True
),
cv.Required(CONF_ID): cv.use_id(binary_sensor.BinarySensor),
}
)
),
}
).extend(cv.COMPONENT_SCHEMA),
validate_config,
)
FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID])
esp32_ble.register_gap_event_handler(parent, var)
await cg.register_component(var, config)
cg.add(var.set_min_interval(config[CONF_MIN_INTERVAL]))
cg.add(var.set_max_interval(config[CONF_MAX_INTERVAL]))
cg.add(var.set_tx_power(config[CONF_TX_POWER]))
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))
# Add sensor measurements
if CONF_MEASUREMENT in config:
for measurement in config[CONF_MEASUREMENT]:
sensor_type = measurement[CONF_SENSOR_TYPE]
object_id = SENSOR_OBJECT_IDS[sensor_type]
sens = await cg.get_variable(measurement[CONF_ID])
cg.add(var.add_measurement(sens, object_id))
# Add binary sensor measurements
if CONF_BINARY_SENSOR in config:
for measurement in config[CONF_BINARY_SENSOR]:
sensor_type = measurement[CONF_SENSOR_TYPE]
object_id = BINARY_SENSOR_OBJECT_IDS[sensor_type]
sens = await cg.get_variable(measurement[CONF_ID])
cg.add(var.add_binary_measurement(sens, object_id))
cg.add_define("USE_ESP32_BLE_ADVERTISING")
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)
+365
View File
@@ -0,0 +1,365 @@
#include "bthome.h"
#include "esphome/core/log.h"
#ifdef USE_ESP32
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h>
#endif
#include <esp_bt_main.h>
#include <esp_gap_ble_api.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <nvs_flash.h>
#include <cstring>
#include <cmath>
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "mbedtls/ccm.h"
namespace esphome {
namespace bthome {
static const char *const TAG = "bthome";
// BTHome v2 service UUID (0xFCD2)
static const uint16_t BTHOME_SERVICE_UUID = 0xFCD2;
// Device info byte for BTHome v2 (bit 5-7: version 010 = v2, bit 0: encryption)
static const uint8_t BTHOME_DEVICE_INFO_UNENCRYPTED = 0x40; // 01000000
static const uint8_t BTHOME_DEVICE_INFO_ENCRYPTED = 0x41; // 01000001
void BTHome::dump_config() {
ESP_LOGCONFIG(TAG, "BTHome:");
ESP_LOGCONFIG(TAG, " Min Interval: %ums", this->min_interval_);
ESP_LOGCONFIG(TAG, " Max Interval: %ums", this->max_interval_);
ESP_LOGCONFIG(TAG, " TX Power: %ddBm", (this->tx_power_ * 3) - 12);
ESP_LOGCONFIG(TAG, " Encryption: %s", this->encryption_enabled_ ? "enabled" : "disabled");
ESP_LOGCONFIG(TAG, " Sensors: %d", this->measurements_.size());
ESP_LOGCONFIG(TAG, " Binary Sensors: %d", this->binary_measurements_.size());
}
float BTHome::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
void BTHome::setup() {
this->ble_adv_params_ = {
.adv_int_min = static_cast<uint16_t>(this->min_interval_ / 0.625f),
.adv_int_max = static_cast<uint16_t>(this->max_interval_ / 0.625f),
.adv_type = ADV_TYPE_NONCONN_IND,
.own_addr_type = BLE_ADDR_TYPE_PUBLIC,
.peer_addr = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
.peer_addr_type = BLE_ADDR_TYPE_PUBLIC,
.channel_map = ADV_CHNL_ALL,
.adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY,
};
global_ble->advertising_register_raw_advertisement_callback([this](bool advertise) {
this->advertising_ = advertise;
if (advertise) {
this->on_advertise_();
}
});
// Register callbacks for sensor state changes
for (auto &measurement : this->measurements_) {
measurement.sensor->add_on_state_callback([this](float) { this->data_changed_ = true; });
}
for (auto &measurement : this->binary_measurements_) {
measurement.sensor->add_on_state_callback([this](bool) { this->data_changed_ = true; });
}
}
void BTHome::loop() {
// Rebuild advertisement if data has changed
if (this->data_changed_ && this->advertising_) {
this->build_advertisement_data_();
this->on_advertise_();
this->data_changed_ = false;
}
}
void BTHome::set_encryption_key(const std::vector<uint8_t> &key) {
if (key.size() != 16) {
ESP_LOGE(TAG, "Encryption key must be 16 bytes");
return;
}
this->encryption_enabled_ = true;
std::copy(key.begin(), key.end(), this->encryption_key_.begin());
}
void BTHome::add_measurement(sensor::Sensor *sensor, uint8_t object_id) {
this->measurements_.push_back({sensor, object_id});
}
void BTHome::add_binary_measurement(binary_sensor::BinarySensor *sensor, uint8_t object_id) {
this->binary_measurements_.push_back({sensor, object_id});
}
void BTHome::build_advertisement_data_() {
std::vector<uint8_t> service_data;
// Add BTHome service UUID (little-endian)
service_data.push_back(BTHOME_SERVICE_UUID & 0xFF);
service_data.push_back((BTHOME_SERVICE_UUID >> 8) & 0xFF);
// Add device info byte
uint8_t device_info = this->encryption_enabled_ ? BTHOME_DEVICE_INFO_ENCRYPTED : BTHOME_DEVICE_INFO_UNENCRYPTED;
service_data.push_back(device_info);
// Build measurement data
std::vector<uint8_t> measurement_data;
// Add all sensor measurements
for (const auto &measurement : this->measurements_) {
if (measurement.sensor->has_state() && !std::isnan(measurement.sensor->state)) {
this->encode_measurement_(measurement_data, measurement.object_id, measurement.sensor->state);
}
}
// Add all binary sensor measurements
for (const auto &measurement : this->binary_measurements_) {
if (measurement.sensor->has_state()) {
this->encode_binary_measurement_(measurement_data, measurement.object_id, measurement.sensor->state);
}
}
if (this->encryption_enabled_) {
// Encrypt the measurement data
std::vector<uint8_t> ciphertext;
if (this->encrypt_payload_(measurement_data, ciphertext)) {
// Add ciphertext to service data
service_data.insert(service_data.end(), ciphertext.begin(), ciphertext.end());
// Add counter (little-endian)
service_data.push_back(this->counter_ & 0xFF);
service_data.push_back((this->counter_ >> 8) & 0xFF);
service_data.push_back((this->counter_ >> 16) & 0xFF);
service_data.push_back((this->counter_ >> 24) & 0xFF);
// Increment counter for next advertisement
this->counter_++;
} else {
ESP_LOGE(TAG, "Encryption failed");
return;
}
} else {
// Add unencrypted measurement data
service_data.insert(service_data.end(), measurement_data.begin(), measurement_data.end());
}
// Build the complete advertisement data
this->adv_data_.clear();
// Flags AD element (required): 0x020106
this->adv_data_.push_back(0x02); // Length
this->adv_data_.push_back(0x01); // Type: Flags
this->adv_data_.push_back(0x06); // LE General Discoverable, BR/EDR not supported
// Service Data AD element
this->adv_data_.push_back(service_data.size() + 1); // Length (data + type byte)
this->adv_data_.push_back(0x16); // Type: Service Data
this->adv_data_.insert(this->adv_data_.end(), service_data.begin(), service_data.end());
}
void BTHome::encode_measurement_(std::vector<uint8_t> &data, uint8_t object_id, float value) {
data.push_back(object_id);
// Encode based on object ID
switch (object_id) {
case 0x01: // battery (uint8, 1%)
{
uint8_t encoded = static_cast<uint8_t>(std::round(value));
data.push_back(encoded);
break;
}
case 0x02: // temperature (sint16, 0.01°C)
case 0x08: // dewpoint (sint16, 0.01°C)
{
int16_t encoded = static_cast<int16_t>(std::round(value * 100.0f));
data.push_back(encoded & 0xFF);
data.push_back((encoded >> 8) & 0xFF);
break;
}
case 0x03: // humidity (uint16, 0.01%)
case 0x14: // moisture (uint16, 0.01%)
{
uint16_t encoded = static_cast<uint16_t>(std::round(value * 100.0f));
data.push_back(encoded & 0xFF);
data.push_back((encoded >> 8) & 0xFF);
break;
}
case 0x04: // pressure (uint24, 0.01 hPa)
case 0x05: // illuminance (uint24, 0.01 lux)
case 0x0A: // energy (uint24, 0.001 kWh)
case 0x0B: // power (uint24, 0.01 W)
{
float factor = (object_id == 0x0A) ? 1000.0f : 100.0f;
uint32_t encoded = static_cast<uint32_t>(std::round(value * factor));
data.push_back(encoded & 0xFF);
data.push_back((encoded >> 8) & 0xFF);
data.push_back((encoded >> 16) & 0xFF);
break;
}
case 0x06: // mass (uint16, 0.01 kg)
case 0x43: // current (uint16, 0.001 A)
case 0x44: // speed (uint16, 0.01 m/s)
{
float factor = (object_id == 0x43) ? 1000.0f : 100.0f;
uint16_t encoded = static_cast<uint16_t>(std::round(value * factor));
data.push_back(encoded & 0xFF);
data.push_back((encoded >> 8) & 0xFF);
break;
}
case 0x0C: // voltage (uint16, 0.001 V)
{
uint16_t encoded = static_cast<uint16_t>(std::round(value * 1000.0f));
data.push_back(encoded & 0xFF);
data.push_back((encoded >> 8) & 0xFF);
break;
}
case 0x0D: // PM2.5 (uint16, 1 µg/m³)
case 0x0E: // PM10 (uint16, 1 µg/m³)
case 0x12: // CO2 (uint16, 1 ppm)
case 0x13: // TVOC (uint16, 1 µg/m³)
{
uint16_t encoded = static_cast<uint16_t>(std::round(value));
data.push_back(encoded & 0xFF);
data.push_back((encoded >> 8) & 0xFF);
break;
}
case 0x50: // timestamp (uint32, seconds)
{
uint32_t encoded = static_cast<uint32_t>(value);
data.push_back(encoded & 0xFF);
data.push_back((encoded >> 8) & 0xFF);
data.push_back((encoded >> 16) & 0xFF);
data.push_back((encoded >> 24) & 0xFF);
break;
}
default:
ESP_LOGW(TAG, "Unsupported sensor object ID: 0x%02X", object_id);
// Remove the object ID we just added
data.pop_back();
break;
}
}
void BTHome::encode_binary_measurement_(std::vector<uint8_t> &data, uint8_t object_id, bool value) {
data.push_back(object_id);
data.push_back(value ? 0x01 : 0x00);
}
bool BTHome::encrypt_payload_(const std::vector<uint8_t> &plaintext, std::vector<uint8_t> &ciphertext) {
if (!this->encryption_enabled_) {
return false;
}
// Get MAC address
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_BT);
// Build nonce according to BTHome spec:
// MAC (6 bytes) + UUID reversed (2 bytes) + device info (1 byte) + counter (4 bytes) = 13 bytes
uint8_t nonce[13];
memcpy(nonce, mac, 6);
nonce[6] = BTHOME_SERVICE_UUID & 0xFF; // UUID byte 1
nonce[7] = (BTHOME_SERVICE_UUID >> 8) & 0xFF; // UUID byte 2 (already little-endian)
nonce[8] = BTHOME_DEVICE_INFO_ENCRYPTED; // Device info byte
nonce[9] = this->counter_ & 0xFF; // Counter byte 0
nonce[10] = (this->counter_ >> 8) & 0xFF; // Counter byte 1
nonce[11] = (this->counter_ >> 16) & 0xFF; // Counter byte 2
nonce[12] = (this->counter_ >> 24) & 0xFF; // Counter byte 3
// Prepare output buffer (ciphertext + 4-byte MIC)
ciphertext.resize(plaintext.size() + 4);
// Initialize mbedtls CCM context
mbedtls_ccm_context ctx;
mbedtls_ccm_init(&ctx);
// Set encryption key
int ret = mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, this->encryption_key_.data(), 128);
if (ret != 0) {
ESP_LOGE(TAG, "mbedtls_ccm_setkey failed: %d", ret);
mbedtls_ccm_free(&ctx);
return false;
}
// Encrypt and generate tag
// BTHome uses no additional authenticated data (AAD)
ret = mbedtls_ccm_encrypt_and_tag(&ctx, plaintext.size(), nonce, sizeof(nonce), nullptr, 0, plaintext.data(),
ciphertext.data(), ciphertext.data() + plaintext.size(), 4);
mbedtls_ccm_free(&ctx);
if (ret != 0) {
ESP_LOGE(TAG, "mbedtls_ccm_encrypt_and_tag failed: %d", ret);
return false;
}
return true;
}
void BTHome::on_advertise_() {
// Build advertisement data if needed
if (this->data_changed_ || this->adv_data_.empty()) {
this->build_advertisement_data_();
this->data_changed_ = false;
}
ESP_LOGD(TAG, "Setting BLE TX power");
esp_err_t err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, this->tx_power_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_ble_tx_power_set failed: %s", esp_err_to_name(err));
}
ESP_LOGD(TAG, "Starting BTHome advertisement (%d bytes)", this->adv_data_.size());
err = esp_ble_gap_config_adv_data_raw(this->adv_data_.data(), this->adv_data_.size());
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_config_adv_data_raw failed: %s", esp_err_to_name(err));
return;
}
}
void BTHome::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
if (!this->advertising_)
return;
esp_err_t err;
switch (event) {
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: {
err = esp_ble_gap_start_advertising(&this->ble_adv_params_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_start_advertising failed: %s", esp_err_to_name(err));
}
break;
}
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: {
err = param->adv_start_cmpl.status;
if (err != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(TAG, "BLE adv start failed: %s", esp_err_to_name(err));
} else {
ESP_LOGD(TAG, "BLE advertising started successfully");
}
break;
}
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: {
err = param->adv_stop_cmpl.status;
if (err != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(TAG, "BLE adv stop failed: %s", esp_err_to_name(err));
} else {
ESP_LOGD(TAG, "BLE stopped advertising successfully");
}
break;
}
default:
break;
}
}
} // namespace bthome
} // namespace esphome
#endif
+79
View File
@@ -0,0 +1,79 @@
#pragma once
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/esp32_ble/ble.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
#ifdef USE_ESP32
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
#include <esp_bt.h>
#endif
#include <esp_gap_ble_api.h>
#include <array>
#include <vector>
namespace esphome {
namespace bthome {
using namespace esp32_ble;
struct SensorMeasurement {
sensor::Sensor *sensor;
uint8_t object_id;
};
struct BinarySensorMeasurement {
binary_sensor::BinarySensor *sensor;
uint8_t object_id;
};
class BTHome : public Component, public GAPEventHandler, public Parented<ESP32BLE> {
public:
void setup() override;
void dump_config() override;
void loop() override;
float get_setup_priority() const override;
void set_min_interval(uint16_t val) { this->min_interval_ = val; }
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 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);
std::vector<SensorMeasurement> measurements_;
std::vector<BinarySensorMeasurement> binary_measurements_;
uint16_t min_interval_{};
uint16_t max_interval_{};
esp_power_level_t tx_power_{};
esp_ble_adv_params_t ble_adv_params_;
bool advertising_{false};
// Encryption
bool encryption_enabled_{false};
std::array<uint8_t, 16> encryption_key_{};
uint32_t counter_{0};
// Cached advertisement data
std::vector<uint8_t> adv_data_;
bool data_changed_{true};
};
} // namespace bthome
} // namespace esphome
#endif
+32
View File
@@ -0,0 +1,32 @@
sensor:
- platform: template
id: test_temperature
name: "Test Temperature"
- platform: template
id: test_humidity
name: "Test Humidity"
- platform: template
id: test_battery
name: "Test Battery"
binary_sensor:
- platform: template
id: test_motion
name: "Test Motion"
- platform: template
id: test_door
name: "Test Door"
bthome:
measurement:
- type: temperature
id: test_temperature
- type: humidity
id: test_humidity
- type: battery
id: test_battery
binary_sensor:
- type: motion
id: test_motion
- type: door
id: test_door
@@ -0,0 +1 @@
<<: !include common.yaml
@@ -0,0 +1 @@
<<: !include common.yaml
@@ -0,0 +1 @@
<<: !include common.yaml
@@ -0,0 +1,18 @@
sensor:
- platform: template
id: test_temperature
name: "Test Temperature"
binary_sensor:
- platform: template
id: test_motion
name: "Test Motion"
bthome:
encryption_key: "231d39c1d7cc1ab1aee224cd096db932"
measurement:
- type: temperature
id: test_temperature
binary_sensor:
- type: motion
id: test_motion