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