[wifi][web_server] Guard AP portal helpers with USE_WIFI_AP and apply review cleanups

This commit is contained in:
J. Nick Koston
2026-08-19 14:40:35 -05:00
parent 78258dc554
commit 676ecae469
10 changed files with 51 additions and 40 deletions
@@ -74,7 +74,6 @@ def _final_validate(config: ConfigType) -> None:
"Add 'ap:' to your WiFi configuration to enable the captive portal."
)
# The listening socket is registered by web_server_base (shared HTTP server).
web_server_base.consume_captive_dns_sockets(config, "captive_portal")
+10 -3
View File
@@ -378,11 +378,18 @@ def _final_validate_ap_mode(config: ConfigType) -> None:
wifi_config = full_config.get(CONF_WIFI)
if serve_captive(config, full_config):
web_server_base.consume_captive_dns_sockets(config, "web_server")
if CONF_LOCAL not in config:
_LOGGER.info(
"WiFi is AP only: embedding the web interface in the firmware "
"(local: true) and serving it as a captive portal on the access point. "
"Set 'local: false' to load it from the internet instead."
)
elif wifi_is_ap_only(wifi_config) and not serve_local(config, wifi_config):
_LOGGER.warning(
"WiFi is AP only and web_server has local: false, so the web interface is "
"loaded from the internet, which browsers on the access point usually cannot "
"reach; the page stays blank. Remove 'local: false' to embed it in the firmware."
"WiFi is AP only and the web_server interface is loaded from the internet, "
"which browsers on the access point usually cannot reach; the page stays "
"blank. Remove 'local: false', or migrate off version 1, so the interface "
"is embedded in the firmware."
)
+13 -13
View File
@@ -346,12 +346,14 @@ void DeferredUpdateEventSourceList::on_client_disconnect_(DeferredUpdateEventSou
#ifdef USE_WEBSERVER_CAPTIVE
WebServer *global_web_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
WebServer::WebServer(web_server_base::WebServerBase *base) : base_(base) { global_web_server = this; }
#else
WebServer::WebServer(web_server_base::WebServerBase *base) : base_(base) {}
#endif
WebServer::WebServer(web_server_base::WebServerBase *base) : base_(base) {
#ifdef USE_WEBSERVER_CAPTIVE
global_web_server = this;
#endif
}
#ifdef USE_WEBSERVER_CSS_INCLUDE
void WebServer::set_css_include(const char *css_include) { this->css_include_ = css_include; }
#endif
@@ -399,8 +401,7 @@ void WebServer::setup() {
#ifdef USE_WEBSERVER_CAPTIVE
// Not-found fallback (outside the auth middleware): the OS captive portal probes hit
// arbitrary URLs and must get the redirect without credentials.
this->base_->get_server()->onNotFound(
[](AsyncWebServerRequest *request) { global_web_server->handle_not_found_(request); });
this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *request) { this->handle_not_found_(request); });
#endif
// OTA is now handled by the web_server OTA platform
@@ -417,25 +418,24 @@ void WebServer::setup() {
});
}
void WebServer::loop() {
bool has_clients = this->events_.loop();
bool keep_looping = this->events_.loop();
#ifdef USE_WEBSERVER_CAPTIVE
if (this->dns_.is_running()) {
this->dns_.loop();
return;
}
this->dns_.loop();
keep_looping |= this->dns_.is_running();
#endif
// No SSE clients connected; stop looping until a new client connects via
// No SSE clients connected (and no captive DNS to serve); stop looping until a new client connects via
// enable_loop_soon_any_context(). This is safe because:
// - set_interval/set_timeout/defer run via the Scheduler, independent of loop()
// - deferrable_send_state early-outs when no clients are connected
// - try_send_nodefer (log, ping) iterates sessions which are empty
// - REST API handlers use defer() which runs via the Scheduler
if (!has_clients)
if (!keep_looping)
this->disable_loop();
}
#ifdef USE_WEBSERVER_CAPTIVE
void WebServer::start_captive() {
// CaptiveDNS::start() no-ops too; this guard just avoids repeating the log and enable_loop
if (this->dns_.is_running())
return;
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
+3 -2
View File
@@ -282,8 +282,9 @@ class WebServer final : public Controller, public Component, public AsyncWebHand
#ifdef USE_WEBSERVER_CAPTIVE
/** AP mode: run a DNS server that answers every name with the AP address and redirect any
* unknown URL to the interface, so a phone joining the AP opens it through the OS captive
* portal check. Started and ended by the wifi component with the access point; start may
* run before setup(), so it touches nothing but the DNS server.
* portal check. Started and ended by the wifi component with the access point. start may run
* before setup() (wifi sets up first): safe because enable_loop() is a no-op before setup;
* nothing but the DNS server may be touched, in particular not base_ or the handlers.
*/
void start_captive();
void end_captive();
@@ -1619,7 +1619,9 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT;
this->num_retried_ = 0;
if (this->has_ap()) {
#ifdef USE_WIFI_AP
this->end_ap_portal_();
#endif
ESP_LOGD(TAG, "Disabling AP");
this->wifi_mode_({}, false);
}
@@ -2219,6 +2221,9 @@ bool WiFiComponent::is_ap_portal_active_() {
return this->is_captive_portal_active_();
}
#ifdef USE_WIFI_AP
// global_web_server needs no null check: codegen always instantiates WebServer when
// USE_WEBSERVER_CAPTIVE is defined, and the constructor assigns the global.
void WiFiComponent::start_ap_portal_() {
#ifdef USE_CAPTIVE_PORTAL
if (captive_portal::global_captive_portal != nullptr)
@@ -2238,6 +2243,7 @@ void WiFiComponent::end_ap_portal_() {
web_server::global_web_server->end_captive();
#endif
}
#endif // USE_WIFI_AP
bool WiFiComponent::is_esp32_improv_active_() {
#ifdef USE_IMPROV
return esp32_improv::global_improv_component != nullptr && esp32_improv::global_improv_component->is_active();
+2
View File
@@ -791,8 +791,10 @@ class WiFiComponent final : public Component {
bool is_captive_portal_active_();
/// captive_portal or the web_server AP mode is serving a user on the access point
bool is_ap_portal_active_();
#ifdef USE_WIFI_AP
void start_ap_portal_();
void end_ap_portal_();
#endif
bool is_esp32_improv_active_();
#ifdef USE_WIFI_FAST_CONNECT
@@ -1131,10 +1131,11 @@ bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
#if (defined(USE_CAPTIVE_PORTAL) || defined(USE_WEBSERVER_CAPTIVE)) && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0)
// Configure DHCP Option 114 (Captive Portal URI) if captive portal or the web_server AP
// mode is enabled. This provides a standards-compliant way for clients to discover the portal
#ifdef USE_CAPTIVE_PORTAL
const bool has_portal = captive_portal::global_captive_portal != nullptr;
#else
#ifdef USE_WEBSERVER_CAPTIVE
// web_server AP mode always serves the portal when compiled in
const bool has_portal = true;
#else
const bool has_portal = captive_portal::global_captive_portal != nullptr;
#endif
if (has_portal) {
// Buffer must be static - dhcps_set_option_info stores pointer, doesn't copy
@@ -0,0 +1,9 @@
# AP mode: with a WiFi access point and the interface embedded in the firmware, web_server
# runs its own captive portal (DNS server, unknown URLs redirect to the page).
wifi:
ap:
ssid: "ESPHome-Test"
password: "Test1234!"
web_server:
local: true
@@ -1,9 +1,2 @@
# AP mode: with a WiFi access point and the interface embedded in the firmware, web_server
# runs its own captive portal (DNS server, unknown URLs redirect to the page).
wifi:
ap:
ssid: "ESPHome-Test"
password: "Test1234!"
web_server:
local: true
packages:
web_server: !include common-ap-mode.yaml
@@ -1,9 +1,2 @@
# AP mode: with a WiFi access point and the interface embedded in the firmware, web_server
# runs its own captive portal (DNS server, unknown URLs redirect to the page).
wifi:
ap:
ssid: "ESPHome-Test"
password: "Test1234!"
web_server:
local: true
packages:
web_server: !include common-ap-mode.yaml