[openthread] Provide action to control poll_period when device MTD (#11766)

Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
rwrozelle
2026-06-25 12:35:15 -04:00
committed by GitHub
co-authored by pre-commit-ci-lite[bot] Jonathan Swoboda Claude Sonnet 4.6
parent 1d5490fd91
commit 6f36ce6429
9 changed files with 200 additions and 39 deletions
+36 -1
View File
@@ -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
@@ -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
@@ -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<OpenThreadComponent> {
public:
// Enforce ctor with parent argument (not without args)
explicit OpenThreadComponentBaseAction(OpenThreadComponent *ot) : Parented<OpenThreadComponent>(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<typename... Ts>
class OpenThreadComponentPollPeriodAction final : public Action<Ts...>, 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
@@ -266,5 +266,34 @@ void OpenThreadComponent::on_factory_reset(std::function<void()> 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
@@ -19,6 +19,8 @@ namespace esphome::openthread {
class InstanceLock;
template<typename... Ts> 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<typename... Ts> 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<otIp6Address> get_omr_address_(InstanceLock &lock);
otInstance *get_openthread_instance_();
int openthread_stop_();
@@ -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) {
+2
View File
@@ -0,0 +1,2 @@
network:
enable_ipv6: true
@@ -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
@@ -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