diff --git a/esphome/components/bthome/__init__.py b/esphome/components/bthome/__init__.py new file mode 100644 index 0000000000..5d4df43c00 --- /dev/null +++ b/esphome/components/bthome/__init__.py @@ -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) diff --git a/esphome/components/bthome/bthome.cpp b/esphome/components/bthome/bthome.cpp new file mode 100644 index 0000000000..eea5e9801f --- /dev/null +++ b/esphome/components/bthome/bthome.cpp @@ -0,0 +1,365 @@ +#include "bthome.h" +#include "esphome/core/log.h" + +#ifdef USE_ESP32 + +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID +#include +#endif +#include +#include +#include +#include +#include +#include +#include + +#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(this->min_interval_ / 0.625f), + .adv_int_max = static_cast(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 &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 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 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 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 &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(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(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(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(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(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(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(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(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 &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 &plaintext, std::vector &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 diff --git a/esphome/components/bthome/bthome.h b/esphome/components/bthome/bthome.h new file mode 100644 index 0000000000..c841b79ee7 --- /dev/null +++ b/esphome/components/bthome/bthome.h @@ -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 +#endif +#include + +#include +#include + +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 { + 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 &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 &data, uint8_t object_id, float value); + void encode_binary_measurement_(std::vector &data, uint8_t object_id, bool value); + bool encrypt_payload_(const std::vector &plaintext, std::vector &ciphertext); + + std::vector measurements_; + std::vector 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 encryption_key_{}; + uint32_t counter_{0}; + + // Cached advertisement data + std::vector adv_data_; + bool data_changed_{true}; +}; + +} // namespace bthome +} // namespace esphome + +#endif diff --git a/tests/components/bthome/common.yaml b/tests/components/bthome/common.yaml new file mode 100644 index 0000000000..3be746e26a --- /dev/null +++ b/tests/components/bthome/common.yaml @@ -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 diff --git a/tests/components/bthome/test.esp32-c3-idf.yaml b/tests/components/bthome/test.esp32-c3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/bthome/test.esp32-c3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/bthome/test.esp32-idf.yaml b/tests/components/bthome/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/bthome/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/bthome/test.esp32-s3-idf.yaml b/tests/components/bthome/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/bthome/test.esp32-s3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/bthome/test_encryption.esp32-idf.yaml b/tests/components/bthome/test_encryption.esp32-idf.yaml new file mode 100644 index 0000000000..dd1c654e83 --- /dev/null +++ b/tests/components/bthome/test_encryption.esp32-idf.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