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