ESPHome 2026.1.4
Loading...
Searching...
No Matches
ota_http_request.cpp
Go to the documentation of this file.
1#include "ota_http_request.h"
2
5#include "esphome/core/log.h"
6
13
14namespace esphome {
15namespace http_request {
16
17static const char *const TAG = "http_request.ota";
18
19void OtaHttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, "Over-The-Air updates via HTTP request"); };
20
21void OtaHttpRequestComponent::set_md5_url(const std::string &url) {
22 if (!this->validate_url_(url)) {
23 this->md5_url_.clear(); // URL was not valid; prevent flashing until it is
24 return;
25 }
26 this->md5_url_ = url;
27 this->md5_expected_.clear(); // to be retrieved later
28}
29
30void OtaHttpRequestComponent::set_url(const std::string &url) {
31 if (!this->validate_url_(url)) {
32 this->url_.clear(); // URL was not valid; prevent flashing until it is
33 return;
34 }
35 this->url_ = url;
36}
37
39 if (this->url_.empty()) {
40 ESP_LOGE(TAG, "URL not set; cannot start update");
41 return;
42 }
43
44 ESP_LOGI(TAG, "Starting update");
45#ifdef USE_OTA_STATE_LISTENER
46 this->notify_state_(ota::OTA_STARTED, 0.0f, 0);
47#endif
48
49 auto ota_status = this->do_ota_();
50
51 switch (ota_status) {
53#ifdef USE_OTA_STATE_LISTENER
54 this->notify_state_(ota::OTA_COMPLETED, 100.0f, ota_status);
55#endif
56 delay(10);
58 break;
59
60 default:
61#ifdef USE_OTA_STATE_LISTENER
62 this->notify_state_(ota::OTA_ERROR, 0.0f, ota_status);
63#endif
64 this->md5_computed_.clear(); // will be reset at next attempt
65 this->md5_expected_.clear(); // will be reset at next attempt
66 break;
67 }
68}
69
70void OtaHttpRequestComponent::cleanup_(std::unique_ptr<ota::OTABackend> backend,
71 const std::shared_ptr<HttpContainer> &container) {
72 if (this->update_started_) {
73 ESP_LOGV(TAG, "Aborting OTA backend");
74 backend->abort();
75 }
76 ESP_LOGV(TAG, "Aborting HTTP connection");
77 container->end();
78};
79
82 uint32_t last_progress = 0;
83 uint32_t update_start_time = millis();
84 md5::MD5Digest md5_receive;
85 std::unique_ptr<char[]> md5_receive_str(new char[33]);
86
87 if (this->md5_expected_.empty() && !this->http_get_md5_()) {
88 return OTA_MD5_INVALID;
89 }
90
91 ESP_LOGD(TAG, "MD5 expected: %s", this->md5_expected_.c_str());
92
93 auto url_with_auth = this->get_url_with_auth_(this->url_);
94 if (url_with_auth.empty()) {
95 return OTA_BAD_URL;
96 }
97 ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str());
98 ESP_LOGI(TAG, "Connecting to: %s", this->url_.c_str());
99
100 auto container = this->parent_->get(url_with_auth);
101
102 if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
104 }
105
106 // we will compute MD5 on the fly for verification -- Arduino OTA seems to ignore it
107 md5_receive.init();
108 ESP_LOGV(TAG, "MD5Digest initialized\n"
109 "OTA backend begin");
110 auto backend = ota::make_ota_backend();
111 auto error_code = backend->begin(container->content_length);
112 if (error_code != ota::OTA_RESPONSE_OK) {
113 ESP_LOGW(TAG, "backend->begin error: %d", error_code);
114 this->cleanup_(std::move(backend), container);
115 return error_code;
116 }
117
118 // NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
119 // Use http_read_loop_result() helper instead of checking return values directly
120 uint32_t last_data_time = millis();
121 const uint32_t read_timeout = this->parent_->get_timeout();
122
123 while (container->get_bytes_read() < container->content_length) {
124 // read a maximum of chunk_size bytes into buf. (real read size returned, or negative error code)
125 int bufsize_or_error = container->read(buf, OtaHttpRequestComponent::HTTP_RECV_BUFFER);
126 ESP_LOGVV(TAG, "bytes_read_ = %u, body_length_ = %u, bufsize_or_error = %i", container->get_bytes_read(),
127 container->content_length, bufsize_or_error);
128
129 // feed watchdog and give other tasks a chance to run
130 App.feed_wdt();
131 yield();
132
133 auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout, container->is_read_complete());
134 if (result == HttpReadLoopResult::RETRY)
135 continue;
136 // Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length,
137 // but this is defensive code in case chunked transfer encoding support is added for OTA in the future.
138 if (result == HttpReadLoopResult::COMPLETE)
139 break;
140 if (result != HttpReadLoopResult::DATA) {
141 if (result == HttpReadLoopResult::TIMEOUT) {
142 ESP_LOGE(TAG, "Timeout reading data");
143 } else {
144 ESP_LOGE(TAG, "Error reading data: %d", bufsize_or_error);
145 }
146 this->cleanup_(std::move(backend), container);
148 }
149
150 // At this point bufsize_or_error > 0, so it's a valid size
151 if (bufsize_or_error <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
152 // add read bytes to MD5
153 md5_receive.add(buf, bufsize_or_error);
154
155 // write bytes to OTA backend
156 this->update_started_ = true;
157 error_code = backend->write(buf, bufsize_or_error);
158 if (error_code != ota::OTA_RESPONSE_OK) {
159 // error code explanation available at
160 // https://github.com/esphome/esphome/blob/dev/esphome/components/ota/ota_backend.h
161 ESP_LOGE(TAG, "Error code (%02X) writing binary data to flash at offset %d and size %d", error_code,
162 container->get_bytes_read() - bufsize_or_error, container->content_length);
163 this->cleanup_(std::move(backend), container);
164 return error_code;
165 }
166 }
167
168 uint32_t now = millis();
169 if ((now - last_progress > 1000) or (container->get_bytes_read() == container->content_length)) {
170 last_progress = now;
171 float percentage = container->get_bytes_read() * 100.0f / container->content_length;
172 ESP_LOGD(TAG, "Progress: %0.1f%%", percentage);
173#ifdef USE_OTA_STATE_LISTENER
174 this->notify_state_(ota::OTA_IN_PROGRESS, percentage, 0);
175#endif
176 }
177 } // while
178
179 ESP_LOGI(TAG, "Done in %.0f seconds", float(millis() - update_start_time) / 1000);
180
181 // verify MD5 is as expected and act accordingly
182 md5_receive.calculate();
183 md5_receive.get_hex(md5_receive_str.get());
184 this->md5_computed_ = md5_receive_str.get();
185 if (strncmp(this->md5_computed_.c_str(), this->md5_expected_.c_str(), MD5_SIZE) != 0) {
186 ESP_LOGE(TAG, "MD5 computed: %s - Aborting due to MD5 mismatch", this->md5_computed_.c_str());
187 this->cleanup_(std::move(backend), container);
189 } else {
190 backend->set_update_md5(md5_receive_str.get());
191 }
192
193 container->end();
194
195 // feed watchdog and give other tasks a chance to run
196 App.feed_wdt();
197 yield();
198 delay(100); // NOLINT
199
200 error_code = backend->end();
201 if (error_code != ota::OTA_RESPONSE_OK) {
202 ESP_LOGW(TAG, "Error ending update! error_code: %d", error_code);
203 this->cleanup_(std::move(backend), container);
204 return error_code;
205 }
206
207 ESP_LOGI(TAG, "Update complete");
209}
210
211std::string OtaHttpRequestComponent::get_url_with_auth_(const std::string &url) {
212 if (this->username_.empty() || this->password_.empty()) {
213 return url;
214 }
215
216 auto start_char = url.find("://");
217 if ((start_char == std::string::npos) || (start_char < 4)) {
218 ESP_LOGE(TAG, "Incorrect URL prefix");
219 return {};
220 }
221
222 ESP_LOGD(TAG, "Using basic HTTP authentication");
223
224 start_char += 3; // skip '://' characters
225 auto url_with_auth =
226 url.substr(0, start_char) + this->username_ + ":" + this->password_ + "@" + url.substr(start_char);
227 return url_with_auth;
228}
229
231 if (this->md5_url_.empty()) {
232 return false;
233 }
234
235 auto url_with_auth = this->get_url_with_auth_(this->md5_url_);
236 if (url_with_auth.empty()) {
237 return false;
238 }
239
240 ESP_LOGVV(TAG, "url_with_auth: %s", url_with_auth.c_str());
241 ESP_LOGI(TAG, "Connecting to: %s", this->md5_url_.c_str());
242 auto container = this->parent_->get(url_with_auth);
243 if (container == nullptr) {
244 ESP_LOGE(TAG, "Failed to connect to MD5 URL");
245 return false;
246 }
247 size_t length = container->content_length;
248 if (length == 0) {
249 container->end();
250 return false;
251 }
252 if (length < MD5_SIZE) {
253 ESP_LOGE(TAG, "MD5 file must be %u bytes; %u bytes reported by HTTP server. Aborting", MD5_SIZE, length);
254 container->end();
255 return false;
256 }
257
258 this->md5_expected_.resize(MD5_SIZE);
259 auto result = http_read_fully(container.get(), (uint8_t *) this->md5_expected_.data(), MD5_SIZE, MD5_SIZE,
260 this->parent_->get_timeout());
261 container->end();
262
263 if (result.status != HttpReadStatus::OK) {
264 if (result.status == HttpReadStatus::TIMEOUT) {
265 ESP_LOGE(TAG, "Timeout reading MD5");
266 } else {
267 ESP_LOGE(TAG, "Error reading MD5: %d", result.error_code);
268 }
269 return false;
270 }
271 return true;
272}
273
274bool OtaHttpRequestComponent::validate_url_(const std::string &url) {
275 if ((url.length() < 8) || !url.starts_with("http") || (url.find("://") == std::string::npos)) {
276 ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'");
277 return false;
278 }
279 return true;
280}
281
282} // namespace http_request
283} // namespace esphome
void feed_wdt(uint32_t time=0)
void get_hex(char *output)
Retrieve the hash as hex characters. Output buffer must hold get_size() * 2 + 1 bytes.
Definition hash_base.h:29
void cleanup_(std::unique_ptr< ota::OTABackend > backend, const std::shared_ptr< HttpContainer > &container)
std::string get_url_with_auth_(const std::string &url)
void set_md5_url(const std::string &md5_url)
void calculate() override
Compute the digest, based on the provided data.
Definition md5.cpp:17
void add(const uint8_t *data, size_t len) override
Add bytes of data for the digest.
Definition md5.cpp:15
void init() override
Initialize a new MD5 digest computation.
Definition md5.cpp:10
void notify_state_(OTAState state, float progress, uint8_t error)
@ TIMEOUT
Timeout waiting for data, caller should exit loop.
@ COMPLETE
All content has been read, caller should exit loop.
@ RETRY
No data yet, already delayed, caller should continue loop.
@ DATA
Data was read, process it.
HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, uint32_t timeout_ms, bool is_read_complete)
Process a read result with timeout tracking and delay handling.
HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size, uint32_t timeout_ms)
Read data from HTTP container into buffer with timeout handling Handles feed_wdt, yield,...
@ TIMEOUT
Timeout waiting for data.
@ OK
Read completed successfully.
std::unique_ptr< ota::OTABackend > make_ota_backend()
@ OTA_RESPONSE_ERROR_MD5_MISMATCH
Definition ota_backend.h:39
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
void IRAM_ATTR HOT yield()
Definition core.cpp:24
void IRAM_ATTR HOT delay(uint32_t ms)
Definition core.cpp:26
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:25
Application App
Global storage of Application pointer - only one Application can exist.
uint16_t length
Definition tt21100.cpp:0