[openthread] Add core_options for OpenThread compile-time options (#20595)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rwrozelle
2026-10-13 15:00:45 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 0821c84b7a
commit 417ab4d2e9
6 changed files with 274 additions and 5 deletions
@@ -0,0 +1,19 @@
esphome:
name: test
esp32:
variant: esp32c6
framework:
type: esp-idf
network:
enable_ipv6: true
openthread:
channel: 13
network_name: OpenThread-8f28
network_key: 0xdfd34f0f05cad978ec4e32b0413038ff
pan_id: 0x8f28
core_options:
OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE: "1"
OPENTHREAD_CONFIG_MAC_DEFAULT_MAX_FRAME_RETRIES_DIRECT: "5"
@@ -0,0 +1,16 @@
esphome:
name: test
esp32:
variant: esp32c6
framework:
type: esp-idf
network:
enable_ipv6: true
openthread:
channel: 13
network_name: OpenThread-8f28
network_key: 0xdfd34f0f05cad978ec4e32b0413038ff
pan_id: 0x8f28
+166 -5
View File
@@ -1,11 +1,22 @@
"""Tests for the openthread component schema."""
"""Tests for the openthread component."""
from collections.abc import Callable
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from esphome.components.esp32 import KEY_VARIANT, VARIANT_ESP32C6
from esphome.components.openthread import CONFIG_SCHEMA
from esphome.const import PlatformFramework
from esphome.core import HexInt
from esphome.components.esp32 import (
KEY_ESP32,
KEY_SDKCONFIG_OPTIONS,
KEY_VARIANT,
VARIANT_ESP32C6,
)
from esphome.components.openthread import CONFIG_SCHEMA, OT_HEADER_DIR, OT_HEADER_FILE
from esphome.components.openthread.const import OPENTHREAD_CONFIG_PREFIX
import esphome.config_validation as cv
from esphome.const import PlatformFramework, Toolchain
from esphome.core import CORE, HexInt
from esphome.yaml_util import Sensitive
from tests.component_tests.types import SetCoreConfigCallable
@@ -37,3 +48,153 @@ def test_credentials_are_sensitive(
assert isinstance(validated[field], Sensitive)
assert validated[field] == expected
assert isinstance(validated[field], HexInt) == isinstance(expected, int)
def _generate_in(
tmp_path: Path, yaml_text: str, generate_main: Callable[[str | Path], str]
) -> tuple[Path, MagicMock]:
"""Generate from ``yaml_text`` written to tmp_path, so the build directory lands there.
Returns the header path and the mocked ``clean_build``.
"""
config_path = tmp_path / "test.yaml"
config_path.write_text(yaml_text, encoding="utf-8")
with patch("esphome.components.openthread.clean_build") as clean_build:
generate_main(config_path)
return CORE.relative_build_path(OT_HEADER_DIR, OT_HEADER_FILE), clean_build
def test_core_options_written_to_header(
tmp_path: Path,
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
header, _ = _generate_in(
tmp_path,
component_config_path("custom_header.yaml").read_text(encoding="utf-8"),
generate_main,
)
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
assert sdkconfig["CONFIG_OPENTHREAD_HEADER_CUSTOM"] is True
assert sdkconfig["CONFIG_OPENTHREAD_CUSTOM_HEADER_PATH"] == OT_HEADER_DIR
assert sdkconfig["CONFIG_OPENTHREAD_CUSTOM_HEADER_FILE_NAME"] == OT_HEADER_FILE
assert header.read_text(encoding="utf-8") == (
"// Auto-generated by ESPHome from openthread core_options - do not edit\n"
"#pragma once\n\n"
"#define OPENTHREAD_CONFIG_COAP_BLOCKWISE_TRANSFER_ENABLE 1\n"
"#define OPENTHREAD_CONFIG_MAC_DEFAULT_MAX_FRAME_RETRIES_DIRECT 5\n"
)
def test_no_header_options_no_header(
tmp_path: Path,
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
header, clean_build = _generate_in(
tmp_path,
component_config_path("no_custom_header.yaml").read_text(encoding="utf-8"),
generate_main,
)
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
assert "CONFIG_OPENTHREAD_HEADER_CUSTOM" not in sdkconfig
assert not header.exists()
clean_build.assert_not_called()
@pytest.mark.parametrize(
("toolchain", "cleans"),
[(Toolchain.PLATFORMIO, True), (Toolchain.ESP_IDF, False)],
)
def test_header_change_cleans_only_on_platformio(
toolchain: Toolchain,
cleans: bool,
tmp_path: Path,
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
source = component_config_path("custom_header.yaml").read_text(encoding="utf-8")
CORE.toolchain = toolchain
_, clean_build = _generate_in(tmp_path, source, generate_main)
assert clean_build.called is cleans
# Unchanged content: no clean on either toolchain.
CORE.reset()
CORE.toolchain = toolchain
_, clean_build = _generate_in(tmp_path, source, generate_main)
clean_build.assert_not_called()
# A changed value leaves sdkconfig as it was, so only this clean rebuilds OpenThread.
changed = source.replace('RETRIES_DIRECT: "5"', 'RETRIES_DIRECT: "7"')
assert changed != source
CORE.reset()
CORE.toolchain = toolchain
header, clean_build = _generate_in(tmp_path, changed, generate_main)
assert clean_build.called is cleans
assert (
"#define OPENTHREAD_CONFIG_MAC_DEFAULT_MAX_FRAME_RETRIES_DIRECT 7\n"
in header.read_text(encoding="utf-8")
)
@pytest.mark.parametrize(
"name",
[
"CONFIG_OPENTHREAD_JOINER",
"openthread_config_x",
"OT_CONFIG_X",
OPENTHREAD_CONFIG_PREFIX,
],
)
def test_core_option_without_prefix_rejected(
name: str, set_core_config: SetCoreConfigCallable
) -> None:
set_core_config(
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32C6}
)
with pytest.raises(cv.Invalid, match=OPENTHREAD_CONFIG_PREFIX):
CONFIG_SCHEMA({"network_key": hex(NETWORK_KEY), "core_options": {name: "1"}})
@pytest.mark.parametrize(
"name",
["OPENTHREAD_CONFIG_a", "OPENTHREAD_CONFIG_A B", "OPENTHREAD_CONFIG_A-B"],
)
def test_core_option_bad_characters_rejected(
name: str, set_core_config: SetCoreConfigCallable
) -> None:
set_core_config(
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32C6}
)
with pytest.raises(cv.Invalid, match="may only contain A-Z, 0-9 and _"):
CONFIG_SCHEMA({"network_key": hex(NETWORK_KEY), "core_options": {name: "1"}})
@pytest.mark.parametrize(
("value", "message"),
[
("", "must not be empty"),
(" ", "must not be empty"),
("1\n#define X 2", "must be a single line"),
("1\r2", "must be a single line"),
("1 \\", "must not end with"),
("1 \\ ", "must not end with"),
],
)
def test_core_option_bad_value_rejected(
value: str, message: str, set_core_config: SetCoreConfigCallable
) -> None:
set_core_config(
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32C6}
)
with pytest.raises(cv.Invalid, match=message):
CONFIG_SCHEMA(
{
"network_key": hex(NETWORK_KEY),
"core_options": {"OPENTHREAD_CONFIG_X": value},
}
)
def test_core_options_only_on_esp32(set_core_config: SetCoreConfigCallable) -> None:
set_core_config(PlatformFramework.NRF52_ZEPHYR)
with pytest.raises(cv.Invalid, match="ESP32"):
CONFIG_SCHEMA({"tlv": TLV, "core_options": {"OPENTHREAD_CONFIG_X": "1"}})
@@ -9,3 +9,5 @@ openthread:
pskc: 0xc23a76e98f1a6483639b1ac1271e2e27
mesh_local_prefix: fd53:145f:ed22:ad81::/64
output_power: 1dBm
core_options:
OPENTHREAD_CONFIG_MAC_DEFAULT_MAX_FRAME_RETRIES_DIRECT: "5"