diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 54e35e301e..a7e57d6e7d 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -274,8 +274,9 @@ 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])) cg.add(var.set_allow_ota(config[CONF_OTA])) - if config[CONF_OTA] and "ota" in CORE.loaded_integrations: - # Only define USE_WEBSERVER_OTA if OTA component is actually loaded + if config[CONF_OTA]: + # Define USE_WEBSERVER_OTA based only on web_server OTA config + # This allows web server OTA to work without loading the OTA component cg.add_define("USE_WEBSERVER_OTA") cg.add(var.set_expose_log(config[CONF_LOG])) if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]: diff --git a/esphome/components/web_server_base/web_server_base.cpp b/esphome/components/web_server_base/web_server_base.cpp index 3a41c7db3d..78c4b6b57a 100644 --- a/esphome/components/web_server_base/web_server_base.cpp +++ b/esphome/components/web_server_base/web_server_base.cpp @@ -15,7 +15,8 @@ #endif #if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA) -#include "esphome/components/ota/ota_backend.h" +#include +#include #endif namespace esphome { @@ -23,6 +24,101 @@ namespace web_server_base { static const char *const TAG = "web_server_base"; +#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA) +// Minimal OTA backend implementation for web server +// This allows OTA updates via web server without requiring the OTA component +// TODO: In the future, this should be refactored into a common ota_base component +// that both web_server and ota components can depend on, avoiding code duplication +// while keeping the components independent. This would allow both ESP-IDF and Arduino +// implementations to share the base OTA functionality without requiring the full OTA component. +class IDFWebServerOTABackend { + public: + bool begin() { + this->partition_ = esp_ota_get_next_update_partition(nullptr); + if (this->partition_ == nullptr) { + ESP_LOGE(TAG, "No OTA partition available"); + return false; + } + +#if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15 + // The following function takes longer than the default timeout of WDT due to flash erase +#if ESP_IDF_VERSION_MAJOR >= 5 + esp_task_wdt_config_t wdtc; + wdtc.idle_core_mask = 0; +#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0 + wdtc.idle_core_mask |= (1 << 0); +#endif +#if CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1 + wdtc.idle_core_mask |= (1 << 1); +#endif + wdtc.timeout_ms = 15000; + wdtc.trigger_panic = false; + esp_task_wdt_reconfigure(&wdtc); +#else + esp_task_wdt_init(15, false); +#endif +#endif + + esp_err_t err = esp_ota_begin(this->partition_, 0, &this->update_handle_); + +#if CONFIG_ESP_TASK_WDT_TIMEOUT_S < 15 + // Set the WDT back to the configured timeout +#if ESP_IDF_VERSION_MAJOR >= 5 + wdtc.timeout_ms = CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000; + esp_task_wdt_reconfigure(&wdtc); +#else + esp_task_wdt_init(CONFIG_ESP_TASK_WDT_TIMEOUT_S, false); +#endif +#endif + + if (err != ESP_OK) { + esp_ota_abort(this->update_handle_); + this->update_handle_ = 0; + ESP_LOGE(TAG, "esp_ota_begin failed: %s", esp_err_to_name(err)); + return false; + } + return true; + } + + bool write(uint8_t *data, size_t len) { + esp_err_t err = esp_ota_write(this->update_handle_, data, len); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_write failed: %s", esp_err_to_name(err)); + return false; + } + return true; + } + + bool end() { + esp_err_t err = esp_ota_end(this->update_handle_); + this->update_handle_ = 0; + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_end failed: %s", esp_err_to_name(err)); + return false; + } + + err = esp_ota_set_boot_partition(this->partition_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ota_set_boot_partition failed: %s", esp_err_to_name(err)); + return false; + } + + return true; + } + + void abort() { + if (this->update_handle_ != 0) { + esp_ota_abort(this->update_handle_); + this->update_handle_ = 0; + } + } + + private: + esp_ota_handle_t update_handle_{0}; + const esp_partition_t *partition_{nullptr}; +}; +#endif + void WebServerBase::add_handler(AsyncWebHandler *handler) { // remove all handlers @@ -120,22 +216,23 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin this->ota_init_(filename.c_str()); this->ota_success_ = false; - auto backend = ota::make_ota_backend(); - if (backend->begin(0) != ota::OTA_RESPONSE_OK) { + auto *backend = new IDFWebServerOTABackend(); + if (!backend->begin()) { ESP_LOGE(TAG, "OTA begin failed"); + delete backend; return; } - this->ota_backend_ = backend.release(); + this->ota_backend_ = backend; } - auto *backend = static_cast(this->ota_backend_); + auto *backend = static_cast(this->ota_backend_); if (!backend) { return; } // Process data if (len > 0) { - if (backend->write(data, len) != ota::OTA_RESPONSE_OK) { + if (!backend->write(data, len)) { ESP_LOGE(TAG, "OTA write failed"); backend->abort(); delete backend; @@ -148,7 +245,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin // Finalize if (final) { - this->ota_success_ = (backend->end() == ota::OTA_RESPONSE_OK); + this->ota_success_ = backend->end(); if (this->ota_success_) { this->schedule_ota_reboot_(); } else { diff --git a/esphome/components/web_server_idf/__init__.py b/esphome/components/web_server_idf/__init__.py index 4e6f21cd03..fe1c6f2640 100644 --- a/esphome/components/web_server_idf/__init__.py +++ b/esphome/components/web_server_idf/__init__.py @@ -15,7 +15,6 @@ async def to_code(config): # Increase the maximum supported size of headers section in HTTP request packet to be processed by the server add_idf_sdkconfig_option("CONFIG_HTTPD_MAX_REQ_HDR_LEN", 1024) # Check if web_server component has OTA enabled - web_server_config = CORE.config.get(CONF_WEB_SERVER, {}) - if web_server_config.get(CONF_OTA, True) and "ota" in CORE.loaded_integrations: + if CORE.config.get(CONF_WEB_SERVER, {}).get(CONF_OTA, True): # Add multipart parser component for ESP-IDF OTA support add_idf_component(name="zorxx/multipart-parser", ref="1.0.1")