diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 5861c3db3f..5c64cf31dc 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -4,13 +4,20 @@ from esphome.components.esp32 import ( VARIANT_ESP32C6, VARIANT_ESP32H2, add_idf_sdkconfig_option, + get_esp32_variant, include_builtin_idf_component, only_on_variant, require_vfs_select, ) from esphome.components.mdns import MDNSComponent, enable_mdns_storage import esphome.config_validation as cv -from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID, CONF_USE_ADDRESS +from esphome.const import ( + CONF_CHANNEL, + CONF_ENABLE_IPV6, + CONF_ID, + CONF_OUTPUT_POWER, + CONF_USE_ADDRESS, +) from esphome.core import CORE, TimePeriodMilliseconds import esphome.final_validate as fv from esphome.types import ConfigType @@ -45,6 +52,20 @@ CONF_DEVICE_TYPES = [ ] +def _validate_txpower(value): + if CORE.is_esp32: + variant = get_esp32_variant() + + # HW limits: Datasheet section "802.15.4 RF Transmitter (TX) Characteristics" + # Further regulatory/soft limit may apply, e.g. by region + if variant in (VARIANT_ESP32C6, VARIANT_ESP32C5): + return cv.int_range(min=-15, max=20)(value) + if variant == VARIANT_ESP32H2: + return cv.int_range(min=-24, max=20)(value) + + return value # Unsupported, fail later with clear error + + def set_sdkconfig_options(config): # and expose options for using SPI/UART RCPs add_idf_sdkconfig_option("CONFIG_IEEE802154_ENABLED", True) @@ -155,6 +176,10 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_TLV): cv.string_strict, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, cv.Optional(CONF_POLL_PERIOD): cv.positive_time_period_milliseconds, + cv.Optional(CONF_OUTPUT_POWER): cv.All( + cv.decibel, + _validate_txpower, + ), } ).extend(_CONNECTION_SCHEMA), cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), @@ -197,4 +222,7 @@ async def to_code(config): cg.add(srp.set_mdns(mdns_component)) await cg.register_component(srp, config) + if (output_power := config.get(CONF_OUTPUT_POWER)) is not None: + cg.add(ot.set_output_power(output_power)) + set_sdkconfig_options(config) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index d22a14aeae..92897a7e96 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -43,6 +43,9 @@ void OpenThreadComponent::dump_config() { ESP_LOGCONFIG(TAG, " Device is configured as Minimal End Device (MED)"); } #endif + if (this->output_power_.has_value()) { + ESP_LOGCONFIG(TAG, " Output power: %" PRId8 "dBm", *this->output_power_); + } } bool OpenThreadComponent::is_connected() { diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 9e429f289b..728847afa5 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -38,6 +38,7 @@ class OpenThreadComponent : public Component { #if CONFIG_OPENTHREAD_MTD void set_poll_period(uint32_t poll_period) { this->poll_period_ = poll_period; } #endif + void set_output_power(int8_t output_power) { this->output_power_ = output_power; } protected: std::optional get_omr_address_(InstanceLock &lock); @@ -45,6 +46,7 @@ class OpenThreadComponent : public Component { #if CONFIG_OPENTHREAD_MTD uint32_t poll_period_{0}; #endif + std::optional output_power_{}; bool teardown_started_{false}; bool teardown_complete_{false}; diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 9dd68a1ccc..2af78b729f 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -135,6 +135,12 @@ void OpenThreadComponent::ot_main() { TRUEFALSE(link_mode_config.mRxOnWhenIdle)); #endif + if (this->output_power_.has_value()) { + if (const auto err = otPlatRadioSetTransmitPower(instance, *this->output_power_); err != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set power: %s", otThreadErrorToString(err)); + } + } + // Run the main loop #if CONFIG_OPENTHREAD_CLI esp_openthread_cli_create_task(); diff --git a/tests/components/openthread/test.esp32-c6-idf.yaml b/tests/components/openthread/test.esp32-c6-idf.yaml index 9df63b2f29..77abc433c1 100644 --- a/tests/components/openthread/test.esp32-c6-idf.yaml +++ b/tests/components/openthread/test.esp32-c6-idf.yaml @@ -13,3 +13,4 @@ openthread: force_dataset: true use_address: open-thread-test.local poll_period: 20sec + output_power: 1dBm