From 1c67a619459c58fd855771defec3389ae41603f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 09:10:24 -1000 Subject: [PATCH 1/4] [ci] Fix WiFi testing mode validation and component splitter for variant-only tests (#11481) --- esphome/components/wifi/__init__.py | 14 +++++++++----- script/split_components_for_ci.py | 9 +++++++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 29d33bfc76..ba488728b7 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -213,11 +213,15 @@ def _validate(config): if CONF_EAP in config: network[CONF_EAP] = config.pop(CONF_EAP) if CONF_NETWORKS in config: - raise cv.Invalid( - "You cannot use the 'ssid:' option together with 'networks:'. Please " - "copy your network into the 'networks:' key" - ) - config[CONF_NETWORKS] = cv.ensure_list(WIFI_NETWORK_STA)(network) + # In testing mode, merged component tests may have both ssid and networks + # Just use the networks list and ignore the single ssid + if not CORE.testing_mode: + raise cv.Invalid( + "You cannot use the 'ssid:' option together with 'networks:'. Please " + "copy your network into the 'networks:' key" + ) + else: + config[CONF_NETWORKS] = cv.ensure_list(WIFI_NETWORK_STA)(network) if (CONF_NETWORKS not in config) and (CONF_AP not in config): config = config.copy() diff --git a/script/split_components_for_ci.py b/script/split_components_for_ci.py index c58dfd218f..87da540d43 100755 --- a/script/split_components_for_ci.py +++ b/script/split_components_for_ci.py @@ -118,8 +118,13 @@ def create_intelligent_batches( continue # Get signature from any platform (they should all have the same buses) - # Components not in component_buses were filtered out by has_test_files check - comp_platforms = component_buses[component] + # Components not in component_buses may only have variant-specific tests + comp_platforms = component_buses.get(component) + if not comp_platforms: + # Component has tests but no analyzable base config - treat as no buses + signature_groups[(ALL_PLATFORMS, NO_BUSES_SIGNATURE)].append(component) + continue + for platform, buses in comp_platforms.items(): if buses: signature = create_grouping_signature({platform: buses}, platform) From f2de8df556d01b9faba6ab7f732df39bfe3c4ea9 Mon Sep 17 00:00:00 2001 From: Daniel Stiner Date: Wed, 22 Oct 2025 14:07:01 -0700 Subject: [PATCH 2/4] [openthread] Fix OTA by populating CORE.address with device's mDNS address (#11095) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/network/util.cpp | 6 +++++- esphome/components/openthread/__init__.py | 13 ++++++++++++- esphome/components/openthread/openthread.cpp | 6 ++++++ esphome/components/openthread/openthread.h | 4 ++++ esphome/core/__init__.py | 8 +++----- tests/unit_tests/test_core.py | 6 ++++-- 6 files changed, 34 insertions(+), 9 deletions(-) diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index 27ad9448a4..cb8f8569ad 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -99,7 +99,11 @@ const std::string &get_use_address() { return wifi::global_wifi_component->get_use_address(); #endif -#if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) +#ifdef USE_OPENTHREAD + return openthread::global_openthread_component->get_use_address(); +#endif + +#if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) && !defined(USE_OPENTHREAD) // Fallback when no network component is defined (e.g., host platform) static const std::string empty; return empty; diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 4865399d02..572ec144d4 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -8,8 +8,10 @@ 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 +from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID, CONF_USE_ADDRESS +from esphome.core import CORE import esphome.final_validate as fv +from esphome.types import ConfigType from .const import ( CONF_DEVICE_TYPE, @@ -108,6 +110,12 @@ _CONNECTION_SCHEMA = cv.Schema( ) +def _validate(config: ConfigType) -> ConfigType: + if CONF_USE_ADDRESS not in config: + config[CONF_USE_ADDRESS] = f"{CORE.name}.local" + return config + + def _require_vfs_select(config): """Register VFS select requirement during config validation.""" # OpenThread uses esp_vfs_eventfd which requires VFS select support @@ -126,11 +134,13 @@ 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, } ).extend(_CONNECTION_SCHEMA), cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), cv.only_with_esp_idf, only_on_variant(supported=[VARIANT_ESP32C6, VARIANT_ESP32H2]), + _validate, _require_vfs_select, ) @@ -155,6 +165,7 @@ async def to_code(config): enable_mdns_storage() ot = cg.new_Pvariable(config[CONF_ID]) + cg.add(ot.set_use_address(config[CONF_USE_ADDRESS])) await cg.register_component(ot, config) srp = cg.new_Pvariable(config[CONF_SRP_ID]) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index b2c2519c08..db909e6b1f 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -252,6 +252,12 @@ void OpenThreadComponent::on_factory_reset(std::function callback) { ESP_LOGD(TAG, "Waiting on Confirmation Removal SRP Host and Services"); } +// set_use_address() is guaranteed to be called during component setup by Python code generation, +// so use_address_ will always be valid when get_use_address() is called - no fallback needed. +const std::string &OpenThreadComponent::get_use_address() const { return this->use_address_; } + +void OpenThreadComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } + } // namespace openthread } // namespace esphome diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index 5d139c633d..19dbeb4628 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -33,11 +33,15 @@ class OpenThreadComponent : public Component { void on_factory_reset(std::function callback); void defer_factory_reset_external_callback(); + const std::string &get_use_address() const; + void set_use_address(const std::string &use_address); + protected: std::optional get_omr_address_(InstanceLock &lock); bool teardown_started_{false}; bool teardown_complete_{false}; std::function factory_reset_external_callback_; + std::string use_address_; }; extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 2d49d29c5e..fed5265d6b 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -636,11 +636,9 @@ class EsphomeCore: if self.config is None: raise ValueError("Config has not been loaded yet") - if CONF_WIFI in self.config: - return self.config[CONF_WIFI][CONF_USE_ADDRESS] - - if CONF_ETHERNET in self.config: - return self.config[CONF_ETHERNET][CONF_USE_ADDRESS] + for network_type in (CONF_WIFI, CONF_ETHERNET, CONF_OPENTHREAD): + if network_type in self.config: + return self.config[network_type][CONF_USE_ADDRESS] if CONF_OPENTHREAD in self.config: return f"{self.name}.local" diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 41114ae18b..92b60efd93 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -571,9 +571,11 @@ class TestEsphomeCore: assert target.address == "4.3.2.1" def test_address__openthread(self, target): - target.name = "test-device" target.config = {} - target.config[const.CONF_OPENTHREAD] = {} + target.config[const.CONF_OPENTHREAD] = { + const.CONF_USE_ADDRESS: "test-device.local" + } + target.name = "test-device" assert target.address == "test-device.local" From 7f567bdfbe172b3fc0c60a7bd5b1d6eec4104746 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 11:53:15 -1000 Subject: [PATCH 3/4] [fan] Add basic fan compile tests (#11484) --- tests/components/fan/common.yaml | 11 +++++++++++ tests/components/fan/test.esp8266-ard.yaml | 1 + 2 files changed, 12 insertions(+) create mode 100644 tests/components/fan/common.yaml create mode 100644 tests/components/fan/test.esp8266-ard.yaml diff --git a/tests/components/fan/common.yaml b/tests/components/fan/common.yaml new file mode 100644 index 0000000000..55c2a656fd --- /dev/null +++ b/tests/components/fan/common.yaml @@ -0,0 +1,11 @@ +fan: + - platform: template + id: test_fan + name: "Test Fan" + preset_modes: + - Eco + - Sleep + - Turbo + has_oscillating: true + has_direction: true + speed_count: 3 diff --git a/tests/components/fan/test.esp8266-ard.yaml b/tests/components/fan/test.esp8266-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/fan/test.esp8266-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 77f97270d671b6b63a9568db2ca908b0bcfc0c7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 22 Oct 2025 12:20:50 -1000 Subject: [PATCH 4/4] [light] Use std::initializer_list for add_effects to reduce flash overhead --- esphome/components/light/light_state.cpp | 7 ++----- esphome/components/light/light_state.h | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 979dc2f5a1..7b0a698bb8 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -178,12 +178,9 @@ void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore void LightState::set_initial_state(const LightStateRTCState &initial_state) { this->initial_state_ = initial_state; } bool LightState::supports_effects() { return !this->effects_.empty(); } const FixedVector &LightState::get_effects() const { return this->effects_; } -void LightState::add_effects(const std::vector &effects) { +void LightState::add_effects(const std::initializer_list &effects) { // Called once from Python codegen during setup with all effects from YAML config - this->effects_.init(effects.size()); - for (auto *effect : effects) { - this->effects_.push_back(effect); - } + this->effects_ = effects; } void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); } diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index a07aeb6ae5..04449e790d 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -163,7 +163,7 @@ class LightState : public EntityBase, public Component { const FixedVector &get_effects() const; /// Add effects for this light state. - void add_effects(const std::vector &effects); + void add_effects(const std::initializer_list &effects); /// Get the total number of effects available for this light. size_t get_effect_count() const { return this->effects_.size(); }