From 66b69859755365f9f9701122469331aa593a2cb8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Jul 2025 13:14:25 -1000 Subject: [PATCH 1/6] Fix format string warnings in Web Server OTA component (#9569) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/web_server/ota/ota_web_server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 4f8f6fda17..adac05cbe5 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -152,7 +152,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin // Finalize if (final) { - ESP_LOGD(TAG, "OTA final chunk: index=%u, len=%u, total_read=%u, contentLength=%u", index, len, + ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%u, contentLength=%zu", index, len, this->ota_read_length_, request->contentLength()); // For Arduino framework, the Update library tracks expected size from firmware header From 8415467dabbecb65892682dd8e5a3251462fc1c0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Jul 2025 13:52:16 -1000 Subject: [PATCH 2/6] Bump aioesphomeapi from 35.0.1 to 36.0.0 (#9567) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f547f47389..b58a836594 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==4.9.0 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==35.0.1 +aioesphomeapi==36.0.0 zeroconf==0.147.0 puremagic==1.30 ruamel.yaml==0.18.14 # dashboard_import From 02999195cdf366f9e5d0a38afbe5eb683c2ca0f2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Jul 2025 14:13:55 -1000 Subject: [PATCH 3/6] Add helpful error message when ESP32+Arduino runs out of flash space (#9580) --- esphome/util.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/esphome/util.py b/esphome/util.py index ba26b8adc1..79cb630200 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -147,6 +147,13 @@ class RedirectText: continue self._write_color_replace(line) + # Check for flash size error and provide helpful guidance + if ( + "Error: The program size" in line + and "is greater than maximum allowed" in line + and (help_msg := get_esp32_arduino_flash_error_help()) + ): + self._write_color_replace(help_msg) else: self._write_color_replace(s) @@ -309,3 +316,34 @@ def get_serial_ports() -> list[SerialPort]: result.sort(key=lambda x: x.path) return result + + +def get_esp32_arduino_flash_error_help() -> str | None: + """Returns helpful message when ESP32 with Arduino runs out of flash space.""" + from esphome.core import CORE + + if not (CORE.is_esp32 and CORE.using_arduino): + return None + + from esphome.log import AnsiFore, color + + return ( + "\n" + + color( + AnsiFore.YELLOW, + "💡 TIP: Your ESP32 with Arduino framework has run out of flash space.\n", + ) + + "\n" + + "To fix this, switch to the ESP-IDF framework which is more memory efficient:\n" + + "\n" + + "1. In your YAML configuration, modify the framework section:\n" + + "\n" + + " esp32:\n" + + " framework:\n" + + " type: esp-idf\n" + + "\n" + + "2. Clean build files and compile again\n" + + "\n" + + "Note: ESP-IDF uses less flash space and provides better performance.\n" + + "Some Arduino-specific libraries may need alternatives.\n\n" + ) From 88323bcca0f7e854f49b5f3db001c453aa3bfc9b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Jul 2025 15:42:48 -1000 Subject: [PATCH 4/6] Allow disabling OTA for web_server while keeping it enabled for captive_portal --- esphome/components/web_server/__init__.py | 23 +++++++++++-------- .../web_server/ota/ota_web_server.cpp | 20 +++++++++++++++- esphome/components/web_server/web_server.cpp | 6 ++--- .../components/web_server/web_server_v1.cpp | 4 +++- .../web_server/test_ota_migration.py | 12 +++++----- 5 files changed, 45 insertions(+), 20 deletions(-) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 6890f60014..572b75a8f1 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -74,13 +74,14 @@ def validate_local(config: ConfigType) -> ConfigType: return config -def validate_ota_removed(config: ConfigType) -> ConfigType: - # Only raise error if OTA is explicitly enabled (True) - # If it's False or not specified, we can safely ignore it - if config.get(CONF_OTA): +def validate_ota(config: ConfigType) -> ConfigType: + # The OTA option only accepts False to explicitly disable OTA for web_server + # IMPORTANT: Setting ota: false ONLY affects the web_server component + # The captive_portal component will still be able to perform OTA updates + if CONF_OTA in config and config[CONF_OTA] is not False: raise cv.Invalid( - f"The '{CONF_OTA}' option has been removed from 'web_server'. " - f"Please use the new OTA platform structure instead:\n\n" + f"The '{CONF_OTA}' option in 'web_server' only accepts 'false' to disable OTA. " + f"To enable OTA, please use the new OTA platform structure instead:\n\n" f"ota:\n" f" - platform: web_server\n\n" f"See https://esphome.io/components/ota for more information." @@ -185,7 +186,7 @@ CONFIG_SCHEMA = cv.All( web_server_base.WebServerBase ), cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean, - cv.Optional(CONF_OTA, default=False): cv.boolean, + cv.Optional(CONF_OTA): cv.boolean, cv.Optional(CONF_LOG, default=True): cv.boolean, cv.Optional(CONF_LOCAL): cv.boolean, cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group), @@ -203,7 +204,7 @@ CONFIG_SCHEMA = cv.All( default_url, validate_local, validate_sorting_groups, - validate_ota_removed, + validate_ota, ) @@ -288,7 +289,11 @@ async def to_code(config): cg.add(var.set_css_url(config[CONF_CSS_URL])) cg.add(var.set_js_url(config[CONF_JS_URL])) # OTA is now handled by the web_server OTA platform - # The CONF_OTA option is kept only for backwards compatibility validation + # The CONF_OTA option is kept to allow explicitly disabling OTA for web_server + # IMPORTANT: This ONLY affects the web_server component, NOT captive_portal + # Captive portal will still be able to perform OTA updates even when this is set + if config.get(CONF_OTA) is False: + cg.add_define("USE_WEBSERVER_OTA_DISABLED") cg.add(var.set_expose_log(config[CONF_LOG])) if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]: cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS") diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index adac05cbe5..26d86ac3cf 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -5,6 +5,10 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" +#ifdef USE_CAPTIVE_PORTAL +#include "esphome/components/captive_portal/captive_portal.h" +#endif + #ifdef USE_ARDUINO #ifdef USE_ESP8266 #include @@ -25,7 +29,21 @@ class OTARequestHandler : public AsyncWebHandler { void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) override; bool canHandle(AsyncWebServerRequest *request) const override { - return request->url() == "/update" && request->method() == HTTP_POST; + if (request->url() != "/update" || request->method() != HTTP_POST) { + return false; + } + +#if defined(USE_WEBSERVER_OTA_DISABLED) && defined(USE_CAPTIVE_PORTAL) + // IMPORTANT: USE_WEBSERVER_OTA_DISABLED only disables OTA for the web_server component + // Captive portal can still perform OTA updates - check if request is from active captive portal + return captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active(); +#elif defined(USE_WEBSERVER_OTA_DISABLED) + // OTA disabled for web_server and no captive portal compiled in + return false; +#else + // OTA enabled for web_server + return true; +#endif } // NOLINTNEXTLINE(readability-identifier-naming) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 9ec667dbc5..14791071e6 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -268,10 +268,10 @@ std::string WebServer::get_config_json() { return json::build_json([this](JsonObject root) { root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name(); root["comment"] = App.get_comment(); -#ifdef USE_WEBSERVER_OTA - root["ota"] = true; // web_server OTA platform is configured +#if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA) + root["ota"] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal #else - root["ota"] = false; + root["ota"] = true; #endif root["log"] = this->expose_log_; root["lang"] = "en"; diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index 5db0f1cae9..0f558f6d81 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -192,7 +192,9 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { stream->print(F("

See ESPHome Web API for " "REST API documentation.

")); -#ifdef USE_WEBSERVER_OTA +#if defined(USE_WEBSERVER_OTA) && !defined(USE_WEBSERVER_OTA_DISABLED) + // Show OTA form only if web_server OTA is not explicitly disabled + // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal stream->print(F("

OTA Update

")); #endif diff --git a/tests/component_tests/web_server/test_ota_migration.py b/tests/component_tests/web_server/test_ota_migration.py index 7f34ec75f6..da25bab0e8 100644 --- a/tests/component_tests/web_server/test_ota_migration.py +++ b/tests/component_tests/web_server/test_ota_migration.py @@ -8,31 +8,31 @@ from esphome.types import ConfigType def test_web_server_ota_true_fails_validation() -> None: """Test that web_server with ota: true fails validation with helpful message.""" - from esphome.components.web_server import validate_ota_removed + from esphome.components.web_server import validate_ota # Config with ota: true should fail config: ConfigType = {"ota": True} with pytest.raises(cv.Invalid) as exc_info: - validate_ota_removed(config) + validate_ota(config) # Check error message contains migration instructions error_msg = str(exc_info.value) - assert "has been removed from 'web_server'" in error_msg + assert "only accepts 'false' to disable OTA" in error_msg assert "platform: web_server" in error_msg assert "ota:" in error_msg def test_web_server_ota_false_passes_validation() -> None: """Test that web_server with ota: false passes validation.""" - from esphome.components.web_server import validate_ota_removed + from esphome.components.web_server import validate_ota # Config with ota: false should pass config: ConfigType = {"ota": False} - result = validate_ota_removed(config) + result = validate_ota(config) assert result == config # Config without ota should also pass config: ConfigType = {} - result = validate_ota_removed(config) + result = validate_ota(config) assert result == config From ce21b992e3f09409803df89ce05373ea68c24c56 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Jul 2025 15:49:30 -1000 Subject: [PATCH 5/6] tidy happy --- esphome/components/web_server/ota/ota_web_server.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 26d86ac3cf..3e566f14bf 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -29,20 +29,20 @@ class OTARequestHandler : public AsyncWebHandler { void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) override; bool canHandle(AsyncWebServerRequest *request) const override { - if (request->url() != "/update" || request->method() != HTTP_POST) { - return false; - } + // Check if this is an OTA update request + bool is_ota_request = request->url() == "/update" && request->method() == HTTP_POST; #if defined(USE_WEBSERVER_OTA_DISABLED) && defined(USE_CAPTIVE_PORTAL) // IMPORTANT: USE_WEBSERVER_OTA_DISABLED only disables OTA for the web_server component // Captive portal can still perform OTA updates - check if request is from active captive portal - return captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active(); + return is_ota_request && captive_portal::global_captive_portal != nullptr && + captive_portal::global_captive_portal->is_active(); #elif defined(USE_WEBSERVER_OTA_DISABLED) // OTA disabled for web_server and no captive portal compiled in return false; #else // OTA enabled for web_server - return true; + return is_ota_request; #endif } From c3da5b7a3f2376e297865f7cc130fa7bce5d44b9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Jul 2025 15:50:42 -1000 Subject: [PATCH 6/6] tell the bot --- esphome/components/web_server/ota/ota_web_server.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 3e566f14bf..966c1c1024 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -35,6 +35,7 @@ class OTARequestHandler : public AsyncWebHandler { #if defined(USE_WEBSERVER_OTA_DISABLED) && defined(USE_CAPTIVE_PORTAL) // IMPORTANT: USE_WEBSERVER_OTA_DISABLED only disables OTA for the web_server component // Captive portal can still perform OTA updates - check if request is from active captive portal + // Note: global_captive_portal is the standard way components communicate in ESPHome return is_ota_request && captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active(); #elif defined(USE_WEBSERVER_OTA_DISABLED)