ESPHome 2026.11.0-dev
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#if USE_ESP32
13using PlatformString = std::string;
14#elif USE_ARDUINO
15using PlatformString = String;
16#endif
17
18namespace esphome::web_server {
19
20ESPHOME_LOG_TAG(TAG, "web_server.ota");
21
22class OTARequestHandler : public AsyncWebHandler {
23 public:
24 OTARequestHandler(WebServerOTAComponent *parent) : parent_(parent) {}
25 void handleRequest(AsyncWebServerRequest *request) override;
26 void handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index, uint8_t *data,
27 size_t len, bool final) override;
28 bool canHandle(AsyncWebServerRequest *request) const override {
29 if (request->method() != HTTP_POST)
30 return false;
31 // Check if this is an OTA update request
32#ifdef USE_ESP32
33 char url_buf[AsyncWebServerRequest::URL_BUF_SIZE];
34 bool is_ota_request = request->url_to(url_buf) == "/update";
35#else
36 bool is_ota_request = request->url() == ESPHOME_F("/update");
37#endif
38
39#if defined(USE_WEBSERVER_OTA_DISABLED) && defined(USE_CAPTIVE_PORTAL)
40 // IMPORTANT: USE_WEBSERVER_OTA_DISABLED only disables OTA for the web_server component
41 // Captive portal can still perform OTA updates - check if request is from active captive portal
42 // Note: global_captive_portal is the standard way components communicate in ESPHome
43 return is_ota_request && captive_portal::global_captive_portal != nullptr &&
45#elif defined(USE_WEBSERVER_OTA_DISABLED)
46 // OTA disabled for web_server and no captive portal compiled in
47 return false;
48#else
49 // OTA enabled for web_server
50 return is_ota_request;
51#endif
52 }
53
54 // NOLINTNEXTLINE(readability-identifier-naming)
55 bool isRequestHandlerTrivial() const override { return false; }
56
57 protected:
58 void report_ota_progress_(AsyncWebServerRequest *request);
59 void schedule_ota_reboot_();
60 void ota_init_(const char *filename);
62 void abort_session_();
64 void refuse_(AsyncWebServerRequest *request);
65
66 uint32_t last_ota_progress_{0};
67 uint32_t ota_read_length_{0};
68 WebServerOTAComponent *parent_;
69 bool ota_success_{false};
70
71 private:
72 ota::OTABackendPtr ota_backend_{nullptr};
74 const AsyncWebServerRequest *ota_request_{nullptr};
75 const AsyncWebServerRequest *refused_request_{nullptr};
76};
77
78void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
79 const uint32_t now = millis();
80 if (now - this->last_ota_progress_ > 1000) {
81 float percentage = 0.0f;
82 if (request->contentLength() != 0) {
83 // Note: Using contentLength() for progress calculation is technically wrong as it includes
84 // multipart headers/boundaries, but it's only off by a small amount and we don't have
85 // access to the actual firmware size until the upload is complete. This is intentional
86 // as it still gives the user a reasonable progress indication.
87 percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
88 ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
89 } else {
90 ESP_LOGD(TAG, "OTA in progress: %" PRIu32 " bytes read", this->ota_read_length_);
91 }
92#ifdef USE_OTA_STATE_LISTENER
93 // Report progress - use notify_state_deferred_ since we're in web server task
94 this->parent_->notify_state_deferred_(ota::OTA_IN_PROGRESS, percentage, 0);
95#endif
96 this->last_ota_progress_ = now;
97 }
98}
99
100void OTARequestHandler::schedule_ota_reboot_() {
101 ESP_LOGI(TAG, "OTA update successful!");
102 this->parent_->set_timeout(100, []() {
103 ESP_LOGI(TAG, "Performing OTA reboot now");
105 });
106}
107
108void OTARequestHandler::ota_init_(const char *filename) {
109 ESP_LOGI(TAG, "OTA Update Start: %s", filename);
110 this->ota_read_length_ = 0;
111 this->ota_success_ = false;
112}
113
114void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const PlatformString &filename, size_t index,
115 uint8_t *data, size_t len, bool final) {
117
118 // First byte of a new upload: index==0 with actual data. (web_server_idf
119 // fires a separate start-marker call with data==nullptr/len==0 before the
120 // first real chunk; gate on len>0 so we only trigger once per upload.)
121 if (index == 0 && len > 0) {
122 // A stalled upload times out and onDisconnect() frees it, so refusing can't block for long.
123 if (this->ota_backend_) {
124 ESP_LOGW(TAG, "OTA upload refused: another upload is in progress");
125 this->refuse_(request);
126 return;
127 }
128
129 this->ota_backend_ = ota::make_ota_backend();
130 if (!this->ota_backend_) {
131 ESP_LOGE(TAG, "Failed to create OTA backend");
132#ifdef USE_OTA_STATE_LISTENER
133 this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f,
134 static_cast<uint8_t>(ota::OTA_RESPONSE_ERROR_UNKNOWN));
135#endif
136 return;
137 }
138
139 // Web server OTA uses multipart uploads where the actual firmware size
140 // is unknown (contentLength includes multipart overhead)
141 // Pass 0 to indicate unknown size
142 error_code = this->ota_backend_->begin(0);
143 if (error_code == ota::OTA_RESPONSE_ERROR_UPDATE_BUSY) {
144 // Nothing started: no state notifications.
145 ESP_LOGW(TAG, "OTA upload refused: another update is in progress");
146 this->ota_backend_.reset();
147 this->refuse_(request);
148 return;
149 }
150
151 // Initialize OTA on first call
152 this->ota_init_(filename.c_str());
153
154#ifdef USE_OTA_STATE_LISTENER
155 // Notify OTA started - use notify_state_deferred_ since we're in web server task
156 this->parent_->notify_state_deferred_(ota::OTA_STARTED, 0.0f, 0);
157#endif
158
159 if (error_code != ota::OTA_RESPONSE_OK) {
160 ESP_LOGE(TAG, "OTA begin failed: %d", error_code);
161 this->ota_backend_.reset();
162#ifdef USE_OTA_STATE_LISTENER
163 this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
164#endif
165 return;
166 }
167
168 // An interrupted upload gets no final chunk.
169 this->ota_request_ = request;
170 request->onDisconnect([this, request]() {
171 if (this->ota_request_ == request && this->ota_backend_) {
172 ESP_LOGW(TAG, "OTA upload interrupted; aborting");
173 this->abort_session_();
174 }
175 });
176 }
177
178 if (!this->ota_backend_ || request != this->ota_request_) {
179 return;
180 }
181
182 // Process data
183 if (len > 0) {
184 error_code = this->ota_backend_->write(data, len);
185 if (error_code != ota::OTA_RESPONSE_OK) {
186 ESP_LOGE(TAG, "OTA write failed: %d", error_code);
187 this->ota_backend_->abort();
188 this->ota_backend_.reset();
189 this->ota_request_ = nullptr;
190#ifdef USE_OTA_STATE_LISTENER
191 this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
192#endif
193 return;
194 }
195 this->ota_read_length_ += len;
196 this->report_ota_progress_(request);
197 }
198
199 // Finalize
200 if (final) {
201 ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%" PRIu32 ", contentLength=%zu", index, len,
202 this->ota_read_length_, request->contentLength());
203
204 // For Arduino framework, the Update library tracks expected size from firmware header
205 // If we haven't received enough data, calling end() will fail
206 // This can happen if the upload is interrupted or the client disconnects
207 error_code = this->ota_backend_->end();
208 if (error_code == ota::OTA_RESPONSE_OK) {
209 this->ota_success_ = true;
210#ifdef USE_OTA_STATE_LISTENER
211 // Report completion before reboot - use notify_state_deferred_ since we're in web server task
212 this->parent_->notify_state_deferred_(ota::OTA_COMPLETED, 100.0f, 0);
213#endif
214 this->schedule_ota_reboot_();
215 } else {
216 ESP_LOGE(TAG, "OTA end failed: %d", error_code);
217 // Reset the platform updater before the lock is released.
218 this->ota_backend_->abort();
219#ifdef USE_OTA_STATE_LISTENER
220 this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
221#endif
222 }
223 this->ota_backend_.reset();
224 this->ota_request_ = nullptr;
225 }
226}
227
228void OTARequestHandler::abort_session_() {
229 this->ota_backend_->abort();
230#ifdef USE_OTA_STATE_LISTENER
231 this->parent_->notify_state_deferred_(ota::OTA_ABORT, 0.0f, 0);
232#endif
233 this->ota_backend_.reset();
234 this->ota_request_ = nullptr;
235}
236
237void OTARequestHandler::refuse_(AsyncWebServerRequest *request) {
238 this->refused_request_ = request;
239 request->onDisconnect([this, request]() {
240 if (this->refused_request_ == request)
241 this->refused_request_ = nullptr;
242 });
243}
244
245void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) {
246 AsyncWebServerResponse *response;
247 // Use the ota_success_ flag to determine the actual result
248 const bool refused = request == this->refused_request_;
249 if (refused)
250 this->refused_request_ = nullptr;
251#ifdef USE_ESP8266
252 static const char UPDATE_SUCCESS[] PROGMEM = "Update Successful!";
253 static const char UPDATE_FAILED[] PROGMEM = "Update Failed!";
254 static const char UPDATE_BUSY[] PROGMEM = "Update Failed! Another update is in progress, retry later.";
255 static const char TEXT_PLAIN[] PROGMEM = "text/plain";
256 static const char CONNECTION_STR[] PROGMEM = "Connection";
257 static const char CLOSE_STR[] PROGMEM = "close";
258 const char *msg = refused ? UPDATE_BUSY : (this->ota_success_ ? UPDATE_SUCCESS : UPDATE_FAILED);
259 response = request->beginResponse_P(200, TEXT_PLAIN, msg);
260 response->addHeader(CONNECTION_STR, CLOSE_STR);
261#else
262 const char *msg = refused ? "Update Failed! Another update is in progress, retry later."
263 : this->ota_success_ ? "Update Successful!"
264 : "Update Failed!";
265 response = request->beginResponse(200, "text/plain", msg);
266 response->addHeader("Connection", "close");
267#endif
268 request->send(response);
269}
270
272 // Get the global web server base instance and register our handler
274 if (base == nullptr) {
275 ESP_LOGE(TAG, "WebServerBase not found");
276 this->mark_failed();
277 return;
278 }
279
280 // The handler lives for the life of the process; WebServerBase never destroys its server
281 base->add_handler(new OTARequestHandler(this)); // NOLINT
282}
283
284void WebServerOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, "Web Server OTA"); }
285
286} // namespace esphome::web_server
287
288#endif // USE_WEBSERVER_OTA
void mark_failed()
Mark this component as failed.
void set_timeout(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a const char* name.
Definition component.cpp:96
void notify_state_deferred_(OTAState state, float progress, uint8_t error)
Notify state with deferral to main loop (for thread safety).
void add_handler(AsyncWebHandler *handler)
CaptivePortal * global_captive_portal
decltype(make_ota_backend()) OTABackendPtr
@ OTA_RESPONSE_ERROR_UPDATE_BUSY
Definition ota_backend.h:54
@ OTA_RESPONSE_ERROR_UNKNOWN
Definition ota_backend.h:55
std::unique_ptr< ArduinoLibreTinyOTABackend > make_ota_backend()
WebServerBase * global_web_server_base
ESPHOME_LOG_TAG(TAG, "web_server.ota")
constexpr uint8_t INDEX_GZ[] PROGMEM
const void size_t len
Definition hal.h:68
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
Application App
Global storage of Application pointer - only one Application can exist.
std::string PlatformString
static void uint32_t