diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index d2eaa3ce6f..d8cdfe19df 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -3,7 +3,17 @@ import re from esphome import automation import esphome.codegen as cg -from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant +from esphome.components.esp32 import ( + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32H2, + VARIANT_ESP32S3, + add_idf_sdkconfig_option, + const, + get_esp32_variant, +) import esphome.config_validation as cv from esphome.const import ( CONF_ENABLE_ON_BOOT, @@ -11,8 +21,10 @@ from esphome.const import ( CONF_ID, CONF_NAME, CONF_NAME_ADD_MAC_SUFFIX, + CONF_TX_POWER, ) from esphome.core import CORE, TimePeriod +from esphome.cpp_types import MockObj import esphome.final_validate as fv DEPENDENCIES = ["esp32"] @@ -151,7 +163,8 @@ IO_CAPABILITY = { esp_power_level_t = cg.global_ns.enum("esp_power_level_t") -TX_POWER_LEVELS = { +# Power level mappings for code generation - ESP32 classic +TX_POWER_LEVELS_ESP32 = { -12: esp_power_level_t.ESP_PWR_LVL_N12, -9: esp_power_level_t.ESP_PWR_LVL_N9, -6: esp_power_level_t.ESP_PWR_LVL_N6, @@ -162,6 +175,53 @@ TX_POWER_LEVELS = { 9: esp_power_level_t.ESP_PWR_LVL_P9, } +# Power level mappings for code generation - Extended variants +TX_POWER_LEVELS_EXT = { + -24: esp_power_level_t.ESP_PWR_LVL_N24, + -21: esp_power_level_t.ESP_PWR_LVL_N21, + -18: esp_power_level_t.ESP_PWR_LVL_N18, + -15: esp_power_level_t.ESP_PWR_LVL_N15, + -12: esp_power_level_t.ESP_PWR_LVL_N12, + -9: esp_power_level_t.ESP_PWR_LVL_N9, + -6: esp_power_level_t.ESP_PWR_LVL_N6, + -3: esp_power_level_t.ESP_PWR_LVL_N3, + 0: esp_power_level_t.ESP_PWR_LVL_N0, + 3: esp_power_level_t.ESP_PWR_LVL_P3, + 6: esp_power_level_t.ESP_PWR_LVL_P6, + 9: esp_power_level_t.ESP_PWR_LVL_P9, + 12: esp_power_level_t.ESP_PWR_LVL_P12, + 15: esp_power_level_t.ESP_PWR_LVL_P15, + 18: esp_power_level_t.ESP_PWR_LVL_P18, + 20: esp_power_level_t.ESP_PWR_LVL_P20, +} + + +def _get_tx_power_levels() -> dict[str, MockObj]: + variant = get_esp32_variant() + if variant in [ + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32H2, + VARIANT_ESP32S3, + ]: + return TX_POWER_LEVELS_EXT + return TX_POWER_LEVELS_ESP32 + + +def validate_tx_power(value: int) -> int: + value = cv.decibel(value) + power_levels = _get_tx_power_levels() + if value not in power_levels: + raise cv.Invalid( + f"TX power {value}dBm is not valid. " + f"Valid values are: {', '.join(str(v) + 'dBm' for v in sorted(power_levels.keys()))}" + ) + # Return just the dBm value, we'll map it to enum in to_code + return value + + CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(ESP32BLE), @@ -169,6 +229,7 @@ CONFIG_SCHEMA = cv.Schema( cv.Optional(CONF_IO_CAPABILITY, default="none"): cv.enum( IO_CAPABILITY, lower=True ), + cv.Optional(CONF_TX_POWER): validate_tx_power, cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, cv.Optional(CONF_ADVERTISING, default=False): cv.boolean, cv.Optional( @@ -259,6 +320,9 @@ async def to_code(config): cg.add(var.set_advertising_cycle_time(config[CONF_ADVERTISING_CYCLE_TIME])) if (name := config.get(CONF_NAME)) is not None: cg.add(var.set_name(name)) + if (tx_power := config.get(CONF_TX_POWER)) is not None: + # The validation already returned the enum value + cg.add(var.set_tx_power(_get_tx_power_levels()[tx_power])) await cg.register_component(var, config) if CORE.using_esp_idf: diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index e22d43c0cc..7f79e85a8c 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -212,6 +212,15 @@ bool ESP32BLE::ble_setup_() { return false; } + // Set TX power for all BLE operations (advertising, scanning, connections) + err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_DEFAULT, this->tx_power_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "esp_ble_tx_power_set failed: %s", esp_err_to_name(err)); + // Continue anyway as this is not critical + } else { + ESP_LOGD(TAG, "BLE TX power set to level %d", this->tx_power_); + } + // BLE takes some time to be fully set up, 200ms should be more than enough delay(200); // NOLINT @@ -520,11 +529,106 @@ void ESP32BLE::dump_config() { io_capability_s = "invalid"; break; } + // Convert TX power level to dBm for display + int tx_power_dbm = 0; +#if defined(CONFIG_IDF_TARGET_ESP32) + // ESP32 classic power levels (0-7) + switch (this->tx_power_) { + case 0: + tx_power_dbm = -12; + break; // ESP_PWR_LVL_N12 + case 1: + tx_power_dbm = -9; + break; // ESP_PWR_LVL_N9 + case 2: + tx_power_dbm = -6; + break; // ESP_PWR_LVL_N6 + case 3: + tx_power_dbm = -3; + break; // ESP_PWR_LVL_N3 + case 4: + tx_power_dbm = 0; + break; // ESP_PWR_LVL_N0 + case 5: + tx_power_dbm = 3; + break; // ESP_PWR_LVL_P3 + case 6: + tx_power_dbm = 6; + break; // ESP_PWR_LVL_P6 + case 7: + tx_power_dbm = 9; + break; // ESP_PWR_LVL_P9 + default: + tx_power_dbm = 0; + break; + } +#elif defined(CONFIG_IDF_TARGET_ESP32C2) || defined(CONFIG_IDF_TARGET_ESP32C3) || \ + defined(CONFIG_IDF_TARGET_ESP32C5) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) || \ + defined(CONFIG_IDF_TARGET_ESP32S3) + // Extended power levels for C2/C3/C5/C6/H2/S3 (0-15) + switch (this->tx_power_) { + case 0: + tx_power_dbm = -24; + break; // ESP_PWR_LVL_N24 + case 1: + tx_power_dbm = -21; + break; // ESP_PWR_LVL_N21 + case 2: + tx_power_dbm = -18; + break; // ESP_PWR_LVL_N18 + case 3: + tx_power_dbm = -15; + break; // ESP_PWR_LVL_N15 + case 4: + tx_power_dbm = -12; + break; // ESP_PWR_LVL_N12 + case 5: + tx_power_dbm = -9; + break; // ESP_PWR_LVL_N9 + case 6: + tx_power_dbm = -6; + break; // ESP_PWR_LVL_N6 + case 7: + tx_power_dbm = -3; + break; // ESP_PWR_LVL_N3 + case 8: + tx_power_dbm = 0; + break; // ESP_PWR_LVL_N0 + case 9: + tx_power_dbm = 3; + break; // ESP_PWR_LVL_P3 + case 10: + tx_power_dbm = 6; + break; // ESP_PWR_LVL_P6 + case 11: + tx_power_dbm = 9; + break; // ESP_PWR_LVL_P9 + case 12: + tx_power_dbm = 12; + break; // ESP_PWR_LVL_P12 + case 13: + tx_power_dbm = 15; + break; // ESP_PWR_LVL_P15 + case 14: + tx_power_dbm = 18; + break; // ESP_PWR_LVL_P18 + case 15: + tx_power_dbm = 20; + break; // ESP_PWR_LVL_P20 + default: + tx_power_dbm = 0; + break; + } +#else + // Unknown variant + tx_power_dbm = 0; +#endif ESP_LOGCONFIG(TAG, "BLE:\n" " MAC address: %s\n" - " IO Capability: %s", - format_mac_address_pretty(mac_address).c_str(), io_capability_s); + " IO Capability: %s\n" + " TX Power: %d dBm", + format_mac_address_pretty(mac_address).c_str(), io_capability_s, tx_power_dbm); } else { ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled"); } diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 712787fe53..285b70d417 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -20,6 +20,7 @@ #ifdef USE_ESP32 +#include #include #include #include @@ -94,6 +95,7 @@ class BLEStatusEventHandler { class ESP32BLE : public Component { public: void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; } + void set_tx_power(esp_power_level_t tx_power) { this->tx_power_ = tx_power; } void set_advertising_cycle_time(uint32_t advertising_cycle_time) { this->advertising_cycle_time_ = advertising_cycle_time; @@ -172,6 +174,7 @@ class ESP32BLE : public Component { // 1-byte aligned members (grouped together to minimize padding) BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; // 1 byte (uint8_t enum) bool enable_on_boot_{}; // 1 byte + esp_power_level_t tx_power_{ESP_PWR_LVL_P9}; // 1 byte (default: +9 dBm) }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)