From 26607713bb1ba0848100384b9ddecd6d4345ba4d Mon Sep 17 00:00:00 2001 From: rwrozelle Date: Wed, 5 Nov 2025 22:57:31 -0500 Subject: [PATCH] [openthread] add poll period for mtd devices (#11374) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/openthread/__init__.py | 19 ++++++++++++-- esphome/components/openthread/const.py | 1 + esphome/components/openthread/openthread.cpp | 17 ++++++++++++ esphome/components/openthread/openthread.h | 7 +++++ .../components/openthread/openthread_esp.cpp | 26 +++++++++++++++++++ .../openthread/test.esp32-c6-idf.yaml | 4 ++- 6 files changed, 71 insertions(+), 3 deletions(-) diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 572ec144d..e3ad3ed76 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -9,7 +9,7 @@ from esphome.components.esp32 import ( 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.core import CORE +from esphome.core import CORE, TimePeriodMilliseconds import esphome.final_validate as fv from esphome.types import ConfigType @@ -22,6 +22,7 @@ from .const import ( CONF_NETWORK_KEY, CONF_NETWORK_NAME, CONF_PAN_ID, + CONF_POLL_PERIOD, CONF_PSKC, CONF_SRP_ID, CONF_TLV, @@ -89,7 +90,7 @@ def set_sdkconfig_options(config): add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT", True) add_idf_sdkconfig_option("CONFIG_OPENTHREAD_SRP_CLIENT_MAX_SERVICES", 5) - # TODO: Add suport for sleepy end devices + # TODO: Add suport for synchronized sleepy end devices (SSED) add_idf_sdkconfig_option(f"CONFIG_OPENTHREAD_{config.get(CONF_DEVICE_TYPE)}", True) @@ -113,6 +114,17 @@ _CONNECTION_SCHEMA = cv.Schema( def _validate(config: ConfigType) -> ConfigType: if CONF_USE_ADDRESS not in config: config[CONF_USE_ADDRESS] = f"{CORE.name}.local" + device_type = config.get(CONF_DEVICE_TYPE) + poll_period = config.get(CONF_POLL_PERIOD) + if ( + device_type == "FTD" + and poll_period + and poll_period > TimePeriodMilliseconds(milliseconds=0) + ): + raise cv.Invalid( + f"{CONF_POLL_PERIOD} can only be used with {CONF_DEVICE_TYPE}: MTD" + ) + return config @@ -135,6 +147,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FORCE_DATASET): cv.boolean, 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, } ).extend(_CONNECTION_SCHEMA), cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), @@ -167,6 +180,8 @@ async def to_code(config): ot = cg.new_Pvariable(config[CONF_ID]) cg.add(ot.set_use_address(config[CONF_USE_ADDRESS])) await cg.register_component(ot, config) + if (poll_period := config.get(CONF_POLL_PERIOD)) is not None: + cg.add(ot.set_poll_period(poll_period)) srp = cg.new_Pvariable(config[CONF_SRP_ID]) mdns_component = await cg.get_variable(config[CONF_MDNS_ID]) diff --git a/esphome/components/openthread/const.py b/esphome/components/openthread/const.py index 7a6ffb2df..f0274a8c9 100644 --- a/esphome/components/openthread/const.py +++ b/esphome/components/openthread/const.py @@ -6,6 +6,7 @@ CONF_MESH_LOCAL_PREFIX = "mesh_local_prefix" CONF_NETWORK_NAME = "network_name" CONF_NETWORK_KEY = "network_key" CONF_PAN_ID = "pan_id" +CONF_POLL_PERIOD = "poll_period" CONF_PSKC = "pskc" CONF_SRP_ID = "srp_id" CONF_TLV = "tlv" diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index d7fb1e1d4..721ab8932 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -29,6 +29,23 @@ OpenThreadComponent *global_openthread_component = // NOLINT(cppcoreguidelines- OpenThreadComponent::OpenThreadComponent() { global_openthread_component = this; } +void OpenThreadComponent::dump_config() { + ESP_LOGCONFIG(TAG, "Open Thread:"); +#if CONFIG_OPENTHREAD_FTD + ESP_LOGCONFIG(TAG, " Device Type: FTD"); +#elif CONFIG_OPENTHREAD_MTD + ESP_LOGCONFIG(TAG, " Device Type: MTD"); + // TBD: Synchronized Sleepy End Device + if (this->poll_period > 0) { + ESP_LOGCONFIG(TAG, " Device is configured as Sleepy End Device (SED)"); + uint32_t duration = this->poll_period / 1000; + ESP_LOGCONFIG(TAG, " Poll Period: %" PRIu32 "s", duration); + } else { + ESP_LOGCONFIG(TAG, " Device is configured as Minimal End Device (MED)"); + } +#endif +} + bool OpenThreadComponent::is_connected() { auto lock = InstanceLock::try_acquire(100); if (!lock) { diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 3132e4169..546128b36 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -22,6 +22,7 @@ class OpenThreadComponent : public Component { public: OpenThreadComponent(); ~OpenThreadComponent(); + void dump_config() override; void setup() override; bool teardown() override; float get_setup_priority() const override { return setup_priority::WIFI; } @@ -35,6 +36,9 @@ class OpenThreadComponent : public Component { const char *get_use_address() const; void set_use_address(const char *use_address); +#if CONFIG_OPENTHREAD_MTD + void set_poll_period(uint32_t poll_period) { this->poll_period = poll_period; } +#endif protected: std::optional get_omr_address_(InstanceLock &lock); @@ -46,6 +50,9 @@ class OpenThreadComponent : public Component { // Stores a pointer to a string literal (static storage duration). // ONLY set from Python-generated code with string literals - never dynamic strings. const char *use_address_{""}; +#if CONFIG_OPENTHREAD_MTD + uint32_t poll_period{0}; +#endif }; extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index b11b7ad34..72dc52109 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -105,6 +105,32 @@ void OpenThreadComponent::ot_main() { esp_cli_custom_command_init(); #endif // CONFIG_OPENTHREAD_CLI_ESP_EXTENSION + otLinkModeConfig link_mode_config = {0}; +#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(esp_openthread_get_instance(), this->poll_period) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set OpenThread pollperiod."); + } + uint32_t link_polling_period = otLinkGetPollPeriod(esp_openthread_get_instance()); + ESP_LOGD(TAG, "Link Polling Period: %d", link_polling_period); + } + link_mode_config.mRxOnWhenIdle = this->poll_period == 0; + link_mode_config.mDeviceType = false; + link_mode_config.mNetworkData = false; +#endif + + if (otThreadSetLinkMode(esp_openthread_get_instance(), link_mode_config) != OT_ERROR_NONE) { + ESP_LOGE(TAG, "Failed to set OpenThread linkmode."); + } + link_mode_config = otThreadGetLinkMode(esp_openthread_get_instance()); + ESP_LOGD(TAG, "Link Mode Device Type: %s", link_mode_config.mDeviceType ? "true" : "false"); + ESP_LOGD(TAG, "Link Mode Network Data: %s", link_mode_config.mNetworkData ? "true" : "false"); + ESP_LOGD(TAG, "Link Mode RX On When Idle: %s", link_mode_config.mRxOnWhenIdle ? "true" : "false"); + // 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 da5339fb3..9df63b2f2 100644 --- a/tests/components/openthread/test.esp32-c6-idf.yaml +++ b/tests/components/openthread/test.esp32-c6-idf.yaml @@ -2,7 +2,7 @@ network: enable_ipv6: true openthread: - device_type: FTD + device_type: MTD channel: 13 network_name: OpenThread-8f28 network_key: 0xdfd34f0f05cad978ec4e32b0413038ff @@ -11,3 +11,5 @@ openthread: pskc: 0xc23a76e98f1a6483639b1ac1271e2e27 mesh_local_prefix: fd53:145f:ed22:ad81::/64 force_dataset: true + use_address: open-thread-test.local + poll_period: 20sec