ESPHome 2026.2.4
Loading...
Searching...
No Matches
ota_web_server.cpp
Go to the documentation of this file.
1#include "ota_web_server.h"
2#ifdef USE_WEBSERVER_OTA
3
6#include "esphome/core/log.h"
7
8#ifdef USE_CAPTIVE_PORTAL
10#endif
11
12#ifdef USE_ARDUINO
13#if defined(USE_LIBRETINY)
14#include <Update.h>
15#endif
16#endif // USE_ARDUINO
17
18#if USE_ESP32
19using PlatformString = std::string;
20#elif USE_ARDUINO
21using PlatformString = String;
22#endif
23
24namespace esphome::web_server {
25
26static const char *const TAG = "web_server.ota";
27
28class OTARequestHandler : public AsyncWebHandler {
29 public:
30 OTARequestHandler(WebServerOTAComponent *parent) : parent_(parent) {}
31 void handleRequest(AsyncWebServerRequest *request) override;
32 void handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index, uint8_t *data,
33 size_t len, bool final) override;
34 bool canHandle(AsyncWebServerRequest *request) const override {
35 if (request->method() != HTTP_POST)
36 return false;
37 // Check if this is an OTA update request
38#ifdef USE_ESP32
39 char url_buf[AsyncWebServerRequest::URL_BUF_SIZE];
40 bool is_ota_request = request->url_to(url_buf) == "/update";
41#else
42 bool is_ota_request = request->url() == ESPHOME_F("/update");
43#endif
44
45#if defined(USE_WEBSERVER_OTA_DISABLED) && defined(USE_CAPTIVE_PORTAL)
46 // IMPORTANT: USE_WEBSERVER_OTA_DISABLED only disables OTA for the web_server component
47 // Captive portal can still perform OTA updates - check if request is from active captive portal
48 // Note: global_captive_portal is the standard way components communicate in ESPHome
49 return is_ota_request && captive_portal::global_captive_portal != nullptr &&
51#elif defined(USE_WEBSERVER_OTA_DISABLED)
52 // OTA disabled for web_server and no captive portal compiled in
53 return false;
54#else
55 // OTA enabled for web_server
56 return is_ota_request;
57#endif
58 }
59
60 // NOLINTNEXTLINE(readability-identifier-naming)
61 bool isRequestHandlerTrivial() const override { return false; }
62
63 protected:
64 void report_ota_progress_(AsyncWebServerRequest *request);
65 void schedule_ota_reboot_();
66 void ota_init_(const char *filename);
67
68 uint32_t last_ota_progress_{0};
69 uint32_t ota_read_length_{0};
70 WebServerOTAComponent *parent_;
71 bool ota_success_{false};
72
73 private:
74 std::unique_ptr<ota::OTABackend> ota_backend_{nullptr};
75};
76
77void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
78 const uint32_t now = millis();
79 if (now - this->last_ota_progress_ > 1000) {
80 float percentage = 0.0f;
81 if (request->contentLength() != 0) {
82 // Note: Using contentLength() for progress calculation is technically wrong as it includes
83 // multipart headers/boundaries, but it's only off by a small amount and we don't have
84 // access to the actual firmware size until the upload is complete. This is intentional
85 // as it still gives the user a reasonable progress indication.
86 percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
87 ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
88 } else {
89 ESP_LOGD(TAG, "OTA in progress: %" PRIu32 " bytes read", this->ota_read_length_);
90 }
91#ifdef USE_OTA_STATE_LISTENER
92 // Report progress - use notify_state_deferred_ since we're in web server task
93 this->parent_->notify_state_deferred_(ota::OTA_IN_PROGRESS, percentage, 0);
94#endif
95 this->last_ota_progress_ = now;
96 }
97}
98
99void OTARequestHandler::schedule_ota_reboot_() {
100 ESP_LOGI(TAG, "OTA update successful!");
101 this->parent_->set_timeout(100, []() {
102 ESP_LOGI(TAG, "Performing OTA reboot now");
104 });
105}
106
107void OTARequestHandler::ota_init_(const char *filename) {
108 ESP_LOGI(TAG, "OTA Update Start: %s", filename);
109 this->ota_read_length_ = 0;
110 this->ota_success_ = false;
111}
112
113void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index,
114 uint8_t *data, size_t len, bool final) {
116
117 if (index == 0 && !this->ota_backend_) {
118 // Initialize OTA on first call
119 this->ota_init_(filename.c_str());
120
121#ifdef USE_OTA_STATE_LISTENER
122 // Notify OTA started - use notify_state_deferred_ since we're in web server task
123 this->parent_->notify_state_deferred_(ota::OTA_STARTED, 0.0f, 0);
124#endif
125
126 // Platform-specific pre-initialization
127#ifdef USE_ARDUINO
128#if defined(USE_LIBRETINY)
129 if (Update.isRunning()) {
130 Update.abort();
131 }
132#endif
133#endif // USE_ARDUINO
134
135 this->ota_backend_ = ota::make_ota_backend();
136 if (!this->ota_backend_) {
137 ESP_LOGE(TAG, "Failed to create OTA backend");
138#ifdef USE_OTA_STATE_LISTENER
139 this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f,
140 static_cast<uint8_t>(ota::OTA_RESPONSE_ERROR_UNKNOWN));
141#endif
142 return;
143 }
144
145 // Web server OTA uses multipart uploads where the actual firmware size
146 // is unknown (contentLength includes multipart overhead)
147 // Pass 0 to indicate unknown size
148 error_code = this->ota_backend_->begin(0);
149 if (error_code != ota::OTA_RESPONSE_OK) {
150 ESP_LOGE(TAG, "OTA begin failed: %d", error_code);
151 this->ota_backend_.reset();
152#ifdef USE_OTA_STATE_LISTENER
153 this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
154#endif
155 return;
156 }
157 }
158
159 if (!this->ota_backend_) {
160 return;
161 }
162
163 // Process data
164 if (len > 0) {
165 error_code = this->ota_backend_->write(data, len);
166 if (error_code != ota::OTA_RESPONSE_OK) {
167 ESP_LOGE(TAG, "OTA write failed: %d", error_code);
168 this->ota_backend_->abort();
169 this->ota_backend_.reset();
170#ifdef USE_OTA_STATE_LISTENER
171 this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
172#endif
173 return;
174 }
175 this->ota_read_length_ += len;
176 this->report_ota_progress_(request);
177 }
178
179 // Finalize
180 if (final) {
181 ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%" PRIu32 ", contentLength=%zu", index, len,
182 this->ota_read_length_, request->contentLength());
183
184 // For Arduino framework, the Update library tracks expected size from firmware header
185 // If we haven't received enough data, calling end() will fail
186 // This can happen if the upload is interrupted or the client disconnects
187 error_code = this->ota_backend_->end();
188 if (error_code == ota::OTA_RESPONSE_OK) {
189 this->ota_success_ = true;
190#ifdef USE_OTA_STATE_LISTENER
191 // Report completion before reboot - use notify_state_deferred_ since we're in web server task
192 this->parent_->notify_state_deferred_(ota::OTA_COMPLETED, 100.0f, 0);
193#endif
194 this->schedule_ota_reboot_();
195 } else {
196 ESP_LOGE(TAG, "OTA end failed: %d", error_code);
197#ifdef USE_OTA_STATE_LISTENER
198 this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
199#endif
200 }
201 this->ota_backend_.reset();
202 }
203}
204
205void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) {
206 AsyncWebServerResponse *response;
207 // Use the ota_success_ flag to determine the actual result
208#ifdef USE_ESP8266
209 static const char UPDATE_SUCCESS[] PROGMEM = "Update Successful!";
210 static const char UPDATE_FAILED[] PROGMEM = "Update Failed!";
211 static const char TEXT_PLAIN[] PROGMEM = "text/plain";
212 static const char CONNECTION_STR[] PROGMEM = "Connection";
213 static const char CLOSE_STR[] PROGMEM = "close";
214 const char *msg = this->ota_success_ ? UPDATE_SUCCESS : UPDATE_FAILED;
215 response = request->beginResponse_P(200, TEXT_PLAIN, msg);
216 response->addHeader(CONNECTION_STR, CLOSE_STR);
217#else
218 const char *msg = this->ota_success_ ? "Update Successful!" : "Update Failed!";
219 response = request->beginResponse(200, "text/plain", msg);
220 response->addHeader("Connection", "close");
221#endif
222 request->send(response);
223}
224
226 // Get the global web server base instance and register our handler
228 if (base == nullptr) {
229 ESP_LOGE(TAG, "WebServerBase not found");
230 this->mark_failed();
231 return;
232 }
233
234 // AsyncWebServer takes ownership of the handler and will delete it when the server is destroyed
235 base->add_handler(new OTARequestHandler(this)); // NOLINT
236}
237
238void WebServerOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, "Web Server OTA"); }
239
240} // namespace esphome::web_server
241
242#endif // USE_WEBSERVER_OTA
virtual void mark_failed()
Mark this component as failed.
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_timeout(const std voi set_timeout)(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a unique name.
Definition component.h:429
void notify_state_deferred_(OTAState state, float progress, uint8_t error)
Notify state with deferral to main loop (for thread safety).
Definition ota_backend.h:87
void add_handler(AsyncWebHandler *handler)
CaptivePortal * global_captive_portal
std::unique_ptr< ota::OTABackend > make_ota_backend()
@ OTA_RESPONSE_ERROR_UNKNOWN
Definition ota_backend.h:41
WebServerBase * global_web_server_base
const uint8_t INDEX_GZ[] PROGMEM
std::string size_t len
Definition helpers.h:692
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:25
Application App
Global storage of Application pointer - only one Application can exist.
std::string PlatformString