diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 2dc8a783dfd..b54fe2b2180 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -1,3 +1,4 @@ +from esphome import automation import esphome.codegen as cg from esphome.components.esp32 import ( VARIANT_ESP32C5, @@ -226,11 +227,11 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FORCE_DATASET): cv.boolean, cv.Optional(CONF_TLV): cv.All(cv.string_strict, _validate_tlv_hex), 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, ), + cv.Optional(CONF_POLL_PERIOD): cv.positive_time_period_milliseconds, } ).extend(_CONNECTION_SCHEMA), cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), @@ -309,3 +310,37 @@ async def to_code(config): ) zephyr_add_prj_conf(f"OPENTHREAD_{config.get(CONF_DEVICE_TYPE)}", True) zephyr_add_prj_conf("MAIN_STACK_SIZE", 4096) + + +# Actions +OpenThreadComponentPollPeriodAction = openthread_ns.class_( + "OpenThreadComponentPollPeriodAction", + automation.Action, + cg.Parented.template(OpenThreadComponent), +) + +POLL_PERIOD_ACTION_SCHEMA = automation.maybe_conf( + CONF_POLL_PERIOD, + cv.Schema( + { + cv.GenerateID(): cv.use_id(OpenThreadComponent), + cv.Required(CONF_POLL_PERIOD): cv.templatable( + cv.positive_time_period_milliseconds + ), + } + ), +) + + +@automation.register_action( + "openthread.set_poll_period", + OpenThreadComponentPollPeriodAction, + POLL_PERIOD_ACTION_SCHEMA, + synchronous=True, +) +async def openthread_poll_period_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_POLL_PERIOD], args, cg.uint32) + cg.add(var.set_poll_period(template_)) + return var diff --git a/esphome/components/openthread/automation.cpp b/esphome/components/openthread/automation.cpp new file mode 100644 index 00000000000..770bf124c55 --- /dev/null +++ b/esphome/components/openthread/automation.cpp @@ -0,0 +1,37 @@ +#include "esphome/core/defines.h" + +#ifdef USE_OPENTHREAD + +#include "automation.h" +#include "esphome/core/log.h" + +namespace esphome::openthread { + +static const char *const TAG = "openthread.automation"; + +void OpenThreadComponentBaseAction::warn_ftd_no_op_() { + ESP_LOGW(TAG, "OpenThread action has no effect on FTD devices (MTD only)"); +} + +void OpenThreadComponentBaseAction::lock_and_apply_() { + if (this->parent_->is_ready()) { + if (auto lock = InstanceLock::try_acquire(LOCK_ACQUIRE_TIMEOUT_MS); lock) { + if (auto *instance = lock.get_instance(); instance != nullptr) { + this->apply_locked(instance); + } + } else { + ESP_LOGW(TAG, "Failed to acquire lock in action"); + } + } else { + // Action may trigger early before setup, e.g. due to enabled "restore mode". + // Trying to acquire lock would fail! + // + // But default component values already have been overwritten. + // It is sufficient to let component apply those later during setup. + ESP_LOGD(TAG, "Not (yet) ready to apply"); + } +} + +} // namespace esphome::openthread + +#endif diff --git a/esphome/components/openthread/automation.h b/esphome/components/openthread/automation.h new file mode 100644 index 00000000000..3706499fda0 --- /dev/null +++ b/esphome/components/openthread/automation.h @@ -0,0 +1,61 @@ +#pragma once +#include "esphome/core/defines.h" +#ifdef USE_OPENTHREAD +#include "openthread.h" + +#include "esphome/core/automation.h" +#include "esphome/core/helpers.h" + +namespace esphome::openthread { + +/** Base class allowing to fetch OpenThread lock from parent component + * while applying action + * + * - Nontemplate aspects belong here to avoid template bloat. + * - Subclasses implement virtual action method that is called under lock. + * - Seal leaf subclasses via @a final to support devirtualization. + */ +class OpenThreadComponentBaseAction : public Parented { + public: + // Enforce ctor with parent argument (not without args) + explicit OpenThreadComponentBaseAction(OpenThreadComponent *ot) : Parented(ot) {} + + protected: + /** Handler to implement in subclass for applying action parts that need lock */ + virtual void apply_locked(otInstance *instance) = 0; + + /** Fetch OT lock and then call @a apply_locked */ + void lock_and_apply_(); + + /** Log a warning that this action has no effect on FTD devices */ + void warn_ftd_no_op_(); + + /** Timeout (ms) for acquiring OT lock */ + static constexpr uint32_t LOCK_ACQUIRE_TIMEOUT_MS = 100; +}; + +/** Action to set single poll period parameter */ +template +class OpenThreadComponentPollPeriodAction final : public Action, public OpenThreadComponentBaseAction { + TEMPLATABLE_VALUE(uint32_t, poll_period) + + public: + /* Passthrough ctor */ + using OpenThreadComponentBaseAction::OpenThreadComponentBaseAction; + + protected: + void play(const Ts &...x) override { +#if CONFIG_OPENTHREAD_MTD + this->parent_->set_poll_period(this->poll_period_.value(x...)); + + this->lock_and_apply_(); +#else + this->warn_ftd_no_op_(); +#endif + } + + void apply_locked(otInstance *instance) override { this->parent_->apply_linkmode_(instance); } +}; + +} // namespace esphome::openthread +#endif diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 102424c62e0..8bfc16b2e05 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -266,5 +266,34 @@ void OpenThreadComponent::on_factory_reset(std::function callback) { ESP_LOGD(TAG, "Waiting on Confirmation Removal SRP Host and Services"); } +void OpenThreadComponent::apply_linkmode_(otInstance *instance) { + otLinkModeConfig link_mode_config{}; +#if CONFIG_OPENTHREAD_FTD + link_mode_config.mRxOnWhenIdle = true; + link_mode_config.mDeviceType = true; + link_mode_config.mNetworkData = true; +#elif CONFIG_OPENTHREAD_MTD + if (this->poll_period_ > 0) { + if (otLinkSetPollPeriod(instance, this->poll_period_) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set pollperiod"); + } + ESP_LOGD(TAG, "Link Polling Period: %" PRIu32, otLinkGetPollPeriod(instance)); + } + link_mode_config.mRxOnWhenIdle = this->poll_period_ == 0; + link_mode_config.mDeviceType = false; + link_mode_config.mNetworkData = false; +#endif + + if (otThreadSetLinkMode(instance, link_mode_config) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set linkmode"); + } +#ifdef ESPHOME_LOG_HAS_DEBUG // Fetch link mode from OT only when DEBUG + link_mode_config = otThreadGetLinkMode(instance); + ESP_LOGD(TAG, "Link Mode Device Type: %s, Network Data: %s, RX On When Idle: %s", + TRUEFALSE(link_mode_config.mDeviceType), TRUEFALSE(link_mode_config.mNetworkData), + TRUEFALSE(link_mode_config.mRxOnWhenIdle)); +#endif +} + } // namespace esphome::openthread #endif diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 488aad11662..a96941325cf 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -19,6 +19,8 @@ namespace esphome::openthread { class InstanceLock; +template class OpenThreadComponentPollPeriodAction; + class OpenThreadComponent final : public Component { public: OpenThreadComponent(); @@ -41,12 +43,23 @@ class OpenThreadComponent final : public Component { void set_use_address(const char *use_address) { this->use_address_ = use_address; } #if CONFIG_OPENTHREAD_MTD void set_poll_period(uint32_t poll_period) { this->poll_period_ = poll_period; } + uint32_t get_poll_period() const { return this->poll_period_; } #endif void set_output_power(int8_t output_power) { this->output_power_ = output_power; } void set_connected(bool connected) { this->connected_ = connected; } static void on_state_changed(otChangedFlags flags, void *context); protected: + // Actions re-apply link mode under the OT lock; allow them to call apply_linkmode_() + // without exposing this lock-sensitive, raw-instance method on the public API. + template friend class OpenThreadComponentPollPeriodAction; + + /** Apply Link Mode settings (incl poll period). + * Callers running outside the OpenThread task must hold InstanceLock. + * ot_main() runs on the OpenThread task itself and must not acquire the lock. + */ + void apply_linkmode_(otInstance *instance); + std::optional get_omr_address_(InstanceLock &lock); otInstance *get_openthread_instance_(); int openthread_stop_(); diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index 6edaa98524c..4f6e618f491 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -111,32 +111,7 @@ void OpenThreadComponent::ot_main() { ESP_LOGD(TAG, "Thread Version: %" PRIu16, otThreadGetVersion()); - otLinkModeConfig link_mode_config{}; -#if CONFIG_OPENTHREAD_FTD - link_mode_config.mRxOnWhenIdle = true; - link_mode_config.mDeviceType = true; - link_mode_config.mNetworkData = true; -#elif CONFIG_OPENTHREAD_MTD - if (this->poll_period_ > 0) { - if (otLinkSetPollPeriod(instance, this->poll_period_) != OT_ERROR_NONE) { - ESP_LOGE(TAG, "Failed to set pollperiod"); - } - ESP_LOGD(TAG, "Link Polling Period: %" PRIu32, otLinkGetPollPeriod(instance)); - } - link_mode_config.mRxOnWhenIdle = this->poll_period_ == 0; - link_mode_config.mDeviceType = false; - link_mode_config.mNetworkData = false; -#endif - - if (otThreadSetLinkMode(instance, link_mode_config) != OT_ERROR_NONE) { - ESP_LOGE(TAG, "Failed to set linkmode"); - } -#ifdef ESPHOME_LOG_HAS_DEBUG // Fetch link mode from OT only when DEBUG - link_mode_config = otThreadGetLinkMode(instance); - ESP_LOGD(TAG, "Link Mode Device Type: %s, Network Data: %s, RX On When Idle: %s", - TRUEFALSE(link_mode_config.mDeviceType), TRUEFALSE(link_mode_config.mNetworkData), - TRUEFALSE(link_mode_config.mRxOnWhenIdle)); -#endif + this->apply_linkmode_(instance); if (this->output_power_.has_value()) { if (const auto err = otPlatRadioSetTransmitPower(instance, *this->output_power_); err != OT_ERROR_NONE) { diff --git a/tests/components/openthread/common.yaml b/tests/components/openthread/common.yaml new file mode 100644 index 00000000000..d9eeab89ea1 --- /dev/null +++ b/tests/components/openthread/common.yaml @@ -0,0 +1,2 @@ +network: + enable_ipv6: true diff --git a/tests/components/openthread/test-tlv.esp32-c6-idf.yaml b/tests/components/openthread/test-tlv.esp32-c6-idf.yaml new file mode 100644 index 00000000000..c61efd4d3cf --- /dev/null +++ b/tests/components/openthread/test-tlv.esp32-c6-idf.yaml @@ -0,0 +1,20 @@ +<<: !include common.yaml + +openthread: + device_type: MTD + force_dataset: false + use_address: open-thread-test.local + tlv: 0e080000000000010000000300001035060004001fffe00208e227ac6a7f24052f0708fdb753eb517cb4d3051062b2442a928d9ea3b947a1618fc4085a030f4f70656e5468726561642d393837330102987304105330d857354330133c05e1fd7ae81a910c0402a0f7f8 + poll_period: 5s + +switch: + - platform: template + name: "Radio Always On" + optimistic: true + restore_mode: ALWAYS_OFF + turn_on_action: + then: + - openthread.set_poll_period: 0s + turn_off_action: + then: + - openthread.set_poll_period: 5s diff --git a/tests/components/openthread/test.esp32-c6-idf.yaml b/tests/components/openthread/test.esp32-c6-idf.yaml index 008edd53972..92d120e5d19 100644 --- a/tests/components/openthread/test.esp32-c6-idf.yaml +++ b/tests/components/openthread/test.esp32-c6-idf.yaml @@ -1,14 +1,6 @@ -esp32: - board: esp32-c6-devkitc-1 - framework: - type: esp-idf - log_level: DEBUG - -network: - enable_ipv6: true +<<: !include common.yaml openthread: - device_type: MTD channel: 13 network_name: OpenThread-8f28 network_key: 0xdfd34f0f05cad978ec4e32b0413038ff @@ -16,7 +8,4 @@ openthread: ext_pan_id: 0xd63e8e3e495ebbc3 pskc: 0xc23a76e98f1a6483639b1ac1271e2e27 mesh_local_prefix: fd53:145f:ed22:ad81::/64 - force_dataset: true - use_address: open-thread-test.local - poll_period: 20sec output_power: 1dBm