ESPHome 2026.5.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#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 ota::OTABackendPtr 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 // First byte of a new upload: index==0 with actual data. (web_server_idf
118 // fires a separate start-marker call with data==nullptr/len==0 before the
119 // first real chunk; gate on len>0 so we only trigger once per upload.)
120 if (index == 0 && len > 0) {
121 // If a previous upload was interrupted (e.g. client closed the tab, TCP
122 // reset) the backend from that session may still be open. Tear it down
123 // so flash state doesn't get concatenated with the new image (which can
124 // produce a technically-valid-sized but corrupted firmware that bricks
125 // the device once it reboots).
126 if (this->ota_backend_) {
127 ESP_LOGW(TAG, "New OTA upload received while previous session was still open; aborting previous session");
128 this->ota_backend_->abort();
129#ifdef USE_OTA_STATE_LISTENER
130 // Notify listeners that the previous session was aborted before the new one starts.
131 this->parent_->notify_state_deferred_(ota::OTA_ABORT, 0.0f, 0);
132#endif
133 this->ota_backend_.reset();
134 }
135
136 // Initialize OTA on first call
137 this->ota_init_(filename.c_str());
138
139#ifdef USE_OTA_STATE_LISTENER
140 // Notify OTA started - use notify_state_deferred_ since we're in web server task
141 this->parent_->notify_state_deferred_(ota::OTA_STARTED, 0.0f, 0);
142#endif
143
144 // Platform-specific pre-initialization
145#ifdef USE_ARDUINO
146#if defined(USE_LIBRETINY)
147 if (Update.isRunning()) {
148 Update.abort();
149 }
150#endif
151#endif // USE_ARDUINO
152
153 this->ota_backend_ = ota::make_ota_backend();
154 if (!this->ota_backend_) {
155 ESP_LOGE(TAG, "Failed to create OTA backend");
156#ifdef USE_OTA_STATE_LISTENER
157 this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f,
158 static_cast<uint8_t>(ota::OTA_RESPONSE_ERROR_UNKNOWN));
159#endif
160 return;
161 }
162
163 // Web server OTA uses multipart uploads where the actual firmware size
164 // is unknown (contentLength includes multipart overhead)
165 // Pass 0 to indicate unknown size
166 error_code = this->ota_backend_->begin(0);
167 if (error_code != ota::OTA_RESPONSE_OK) {
168 ESP_LOGE(TAG, "OTA begin failed: %d", error_code);
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 }
176
177 if (!this->ota_backend_) {
178 return;
179 }
180
181 // Process data
182 if (len > 0) {
183 error_code = this->ota_backend_->write(data, len);
184 if (error_code != ota::OTA_RESPONSE_OK) {
185 ESP_LOGE(TAG, "OTA write failed: %d", error_code);
186 this->ota_backend_->abort();
187 this->ota_backend_.reset();
188#ifdef USE_OTA_STATE_LISTENER
189 this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
190#endif
191 return;
192 }
193 this->ota_read_length_ += len;
194 this->report_ota_progress_(request);
195 }
196
197 // Finalize
198 if (final) {
199 ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%" PRIu32 ", contentLength=%zu", index, len,
200 this->ota_read_length_, request->contentLength());
201
202 // For Arduino framework, the Update library tracks expected size from firmware header
203 // If we haven't received enough data, calling end() will fail
204 // This can happen if the upload is interrupted or the client disconnects
205 error_code = this->ota_backend_->end();
206 if (error_code == ota::OTA_RESPONSE_OK) {
207 this->ota_success_ = true;
208#ifdef USE_OTA_STATE_LISTENER
209 // Report completion before reboot - use notify_state_deferred_ since we're in web server task
210 this->parent_->notify_state_deferred_(ota::OTA_COMPLETED, 100.0f, 0);
211#endif
212 this->schedule_ota_reboot_();
213 } else {
214 ESP_LOGE(TAG, "OTA end failed: %d", error_code);
215#ifdef USE_OTA_STATE_LISTENER
216 this->parent_->notify_state_deferred_(ota::OTA_ERROR, 0.0f, static_cast<uint8_t>(error_code));
217#endif
218 }
219 this->ota_backend_.reset();
220 }
221}
222
223void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) {
224 AsyncWebServerResponse *response;
225 // Use the ota_success_ flag to determine the actual result
226#ifdef USE_ESP8266
227 static const char UPDATE_SUCCESS[] PROGMEM = "Update Successful!";
228 static const char UPDATE_FAILED[] PROGMEM = "Update Failed!";
229 static const char TEXT_PLAIN[] PROGMEM = "text/plain";
230 static const char CONNECTION_STR[] PROGMEM = "Connection";
231 static const char CLOSE_STR[] PROGMEM = "close";
232 const char *msg = this->ota_success_ ? UPDATE_SUCCESS : UPDATE_FAILED;
233 response = request->beginResponse_P(200, TEXT_PLAIN, msg);
234 response->addHeader(CONNECTION_STR, CLOSE_STR);
235#else
236 const char *msg = this->ota_success_ ? "Update Successful!" : "Update Failed!";
237 response = request->beginResponse(200, "text/plain", msg);
238 response->addHeader("Connection", "close");
239#endif
240 request->send(response);
241}
242
244 // Get the global web server base instance and register our handler
246 if (base == nullptr) {
247 ESP_LOGE(TAG, "WebServerBase not found");
248 this->mark_failed();
249 return;
250 }
251
252 // AsyncWebServer takes ownership of the handler and will delete it when the server is destroyed
253 base->add_handler(new OTARequestHandler(this)); // NOLINT
254}
255
256void WebServerOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, "Web Server OTA"); }
257
258} // namespace esphome::web_server
259
260#endif // USE_WEBSERVER_OTA
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:510
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_UNKNOWN
Definition ota_backend.h:41
std::unique_ptr< ArduinoLibreTinyOTABackend > make_ota_backend()
WebServerBase * global_web_server_base
constexpr uint8_t INDEX_GZ[] PROGMEM
std::string size_t len
Definition helpers.h:1045
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:26
Application App
Global storage of Application pointer - only one Application can exist.
std::string PlatformString
static void uint32_t