From 7a700ca0779367b6558489af364915f7bfd4eb9f Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 11 Nov 2025 12:15:44 +1000 Subject: [PATCH 1/2] [core] Update clamp functions to allow mixed but comparable types (#11828) --- esphome/core/helpers.h | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 48af7f674a..52a0746057 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1174,12 +1174,18 @@ template using ExternalRAMAllocator = RAMAllocator; * Functions to constrain the range of arithmetic values. */ -template T clamp_at_least(T value, T min) { +template +concept comparable_with = requires(T a, U b) { + { a > b } -> std::convertible_to; + { a < b } -> std::convertible_to; +}; + +template U> T clamp_at_least(T value, U min) { if (value < min) return min; return value; } -template T clamp_at_most(T value, T max) { +template U> T clamp_at_most(T value, U max) { if (value > max) return max; return value; From 48a33611a1e207abe190484077bcf7db4c220a40 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 10 Nov 2025 20:43:32 -0600 Subject: [PATCH 2/2] [wifi] Fix infinite retry loop when no hidden networks and captive portal active --- esphome/components/wifi/wifi_component.cpp | 28 ++++++++++------------ 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 7279e0c783..49e433b468 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1163,11 +1163,9 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { if (this->find_next_hidden_sta_(-1, !this->went_through_explicit_hidden_phase_()) >= 0) { return WiFiRetryPhase::RETRY_HIDDEN; // Found hidden networks to try } - // No hidden networks - skip directly to restart/rescan - if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) { - return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN - : WiFiRetryPhase::SCAN_CONNECTING; - } + // No hidden networks - always go through RESTARTING_ADAPTER phase + // This ensures num_retried_ gets reset and a fresh scan is triggered + // The actual adapter restart will be skipped if captive portal/improv is active return WiFiRetryPhase::RESTARTING_ADAPTER; case WiFiRetryPhase::RETRY_HIDDEN: @@ -1183,16 +1181,9 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { return WiFiRetryPhase::RETRY_HIDDEN; } } - // Exhausted all potentially hidden SSIDs - rescan to try next BSSID - // If captive portal/improv is active, skip adapter restart and go back to start - // Otherwise restart adapter to clear any stuck state - if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) { - // Go back to explicit hidden if we went through it initially, otherwise scan - return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN - : WiFiRetryPhase::SCAN_CONNECTING; - } - - // Restart adapter + // Exhausted all potentially hidden SSIDs - always go through RESTARTING_ADAPTER + // This ensures num_retried_ gets reset and a fresh scan is triggered + // The actual adapter restart will be skipped if captive portal/improv is active return WiFiRetryPhase::RESTARTING_ADAPTER; case WiFiRetryPhase::RESTARTING_ADAPTER: @@ -1280,7 +1271,12 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { break; case WiFiRetryPhase::RESTARTING_ADAPTER: - this->restart_adapter(); + // Skip actual adapter restart if captive portal/improv is active + // This allows state machine to reset num_retried_ and trigger fresh scan + // without disrupting the captive portal/improv connection + if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) { + this->restart_adapter(); + } // Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting return true;