ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
http_request_update.cpp
Go to the documentation of this file.
2
3#include <cstring>
4
7
10
11namespace esphome::http_request {
12
13// The update function runs in a task only on ESP32s.
14#ifdef USE_ESP32
15// vTaskDelete doesn't return, but clang-tidy doesn't know that
16#define UPDATE_RETURN \
17 do { \
18 vTaskDelete(nullptr); \
19 __builtin_unreachable(); \
20 } while (0)
21#else
22#define UPDATE_RETURN return
23#endif
24
25ESPHOME_LOG_TAG(TAG, "http_request.update");
26
27static const size_t MAX_READ_SIZE = 256;
28static constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0;
29static constexpr uint32_t INITIAL_CHECK_INTERVAL_MS = 10000;
30static constexpr uint8_t INITIAL_CHECK_MAX_ATTEMPTS = 6;
31
34
35 // Check periodically until network is ready
36 // Only if update interval is > total retry window to avoid redundant checks
38 this->get_update_interval() > INITIAL_CHECK_INTERVAL_MS * INITIAL_CHECK_MAX_ATTEMPTS) {
39 this->initial_check_remaining_ = INITIAL_CHECK_MAX_ATTEMPTS;
40 this->set_interval(INITIAL_CHECK_INTERVAL_ID, INITIAL_CHECK_INTERVAL_MS, [this]() {
41 bool connected = network::is_connected();
42 if (--this->initial_check_remaining_ == 0 || connected) {
43 this->cancel_interval(INITIAL_CHECK_INTERVAL_ID);
44 if (connected) {
45 this->update();
46 }
47 }
48 });
49 }
50}
51
52void HttpRequestUpdate::on_ota_state(ota::OTAState state, float progress, uint8_t error) {
55 this->update_info_.has_progress = true;
56 this->update_info_.progress = progress;
57 this->publish_state();
60 this->status_set_error(LOG_STR("Failed to install firmware"));
61 this->publish_state();
62 }
63}
64
65void HttpRequestUpdate::set_source_url(const char *source_url, size_t len) {
66 // Copy before releasing, so a caller passing the current URL back in stays valid.
67 char *copy = new char[len + 1];
68 memcpy(copy, source_url, len);
69 copy[len] = '\0';
70 this->release_source_url_();
71 this->source_url_ = copy;
72 this->source_url_owned_ = true;
73}
74
75void HttpRequestUpdate::release_source_url_() {
76 if (this->source_url_owned_)
77 delete[] this->source_url_;
78 this->source_url_owned_ = false;
79}
80
81void HttpRequestUpdate::begin_check_(TaskResult *result) {
82 result->update = this;
83 result->source_url = this->source_url_;
84 result->owns_source_url = this->source_url_owned_;
85 this->source_url_owned_ = false;
86}
87
88void HttpRequestUpdate::end_check_(TaskResult *result) {
89 if (this->source_url_ == result->source_url) {
90 this->source_url_owned_ = result->owns_source_url;
91 } else if (result->owns_source_url) {
92 delete[] result->source_url; // replaced while the check ran
93 }
94}
95
97 if (!network::is_connected()) {
98 ESP_LOGD(TAG, "Network not connected, skipping update check");
99 return;
100 }
101 this->cancel_interval(INITIAL_CHECK_INTERVAL_ID);
102#ifdef USE_ESP32
103 if (this->task_result_ != nullptr) {
104 ESP_LOGW(TAG, "Update check already in progress");
105 return;
106 }
107#endif
108 // Allocated once per check; every path in the task returns via its single defer, which frees it.
109 auto *result = new TaskResult();
110 this->begin_check_(result);
111#ifdef USE_ESP32
112 this->task_result_ = result;
113 if (xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, result, 1, nullptr) != pdPASS) {
114 this->task_result_ = nullptr;
115 this->end_check_(result);
116 delete result;
117 }
118#else
119 this->update_task(result);
120 this->end_check_(result);
121#endif
122}
123
125 auto *result = static_cast<TaskResult *>(params);
126 HttpRequestUpdate *this_update = result->update;
127 auto *info = &result->info;
128 const char *source_url = result->source_url;
129
130 auto container = this_update->request_parent_->get(source_url);
131
132 if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
133 ESP_LOGE(TAG, "Failed to fetch manifest from %s", source_url);
134 if (container != nullptr)
135 container->end();
136 result->error_str = LOG_STR("Failed to fetch manifest");
137 goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
138 }
139
140 {
141 RAMAllocator<uint8_t> allocator;
142 uint8_t *data = allocator.allocate(container->content_length);
143 if (data == nullptr) {
144 ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length);
145 container->end();
146 result->error_str = LOG_STR("Failed to allocate memory for manifest");
147 goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
148 }
149
150 auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE,
151 this_update->request_parent_->get_timeout());
152 if (read_result.status != HttpReadStatus::OK) {
153 if (read_result.status == HttpReadStatus::TIMEOUT) {
154 ESP_LOGE(TAG, "Timeout reading manifest");
155 } else {
156 ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code);
157 }
158 allocator.deallocate(data, container->content_length);
159 container->end();
160 result->error_str = LOG_STR("Failed to read manifest");
161 goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
162 }
163 size_t read_index = container->get_bytes_read();
164 size_t content_length = container->content_length;
165
166 container->end();
167 container.reset(); // Release ownership of the container's shared_ptr
168
169 bool valid = false;
170 { // Scope to ensure JsonDocument is destroyed before deallocating buffer
171 valid = json::parse_json(data, read_index, [info](JsonObject root) -> bool {
172 if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() ||
173 !root[ESPHOME_F("builds")].is<JsonArray>()) {
174 ESP_LOGE(TAG, "Manifest does not contain required fields");
175 return false;
176 }
177 info->title = root[ESPHOME_F("name")].as<std::string>();
178 info->latest_version = root[ESPHOME_F("version")].as<std::string>();
179
180 auto builds_array = root[ESPHOME_F("builds")].as<JsonArray>();
181 for (auto build : builds_array) {
182 if (!build[ESPHOME_F("chipFamily")].is<const char *>()) {
183 ESP_LOGE(TAG, "Manifest does not contain required fields");
184 return false;
185 }
186 if (build[ESPHOME_F("chipFamily")] == ESPHOME_VARIANT) {
187 if (!build[ESPHOME_F("ota")].is<JsonObject>()) {
188 ESP_LOGE(TAG, "Manifest does not contain required fields");
189 return false;
190 }
191 JsonObject ota = build[ESPHOME_F("ota")].as<JsonObject>();
192 if (!ota[ESPHOME_F("path")].is<const char *>() || !ota[ESPHOME_F("md5")].is<const char *>()) {
193 ESP_LOGE(TAG, "Manifest does not contain required fields");
194 return false;
195 }
196 info->firmware_url = ota[ESPHOME_F("path")].as<std::string>();
197 info->md5 = ota[ESPHOME_F("md5")].as<std::string>();
198
199 if (ota[ESPHOME_F("summary")].is<const char *>())
200 info->summary = ota[ESPHOME_F("summary")].as<std::string>();
201 if (ota[ESPHOME_F("release_url")].is<const char *>())
202 info->release_url = ota[ESPHOME_F("release_url")].as<std::string>();
203
204 return true;
205 }
206 }
207 return false;
208 });
209 }
210 allocator.deallocate(data, content_length);
211
212 if (!valid) {
213 ESP_LOGE(TAG, "Failed to parse JSON from %s", source_url);
214 result->error_str = LOG_STR("Failed to parse manifest JSON");
215 goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
216 }
217
218 // Merge source_url_ and firmware_url
219 if (!info->firmware_url.empty() && info->firmware_url.find("http") == std::string::npos) {
220 const char *source = source_url;
221 const size_t source_len = strlen(source);
222 size_t prefix_len;
223 if (info->firmware_url[0] == '/') {
224 // scheme and host, up to the first slash after "https://"
225 const char *host_end = source_len > 8 ? strchr(source + 8, '/') : nullptr;
226 prefix_len = host_end != nullptr ? host_end - source : source_len;
227 } else {
228 // directory of the manifest, up to and including its last slash
229 const char *dir_end = strrchr(source, '/');
230 prefix_len = dir_end != nullptr ? dir_end - source + 1 : 0;
231 }
232 info->firmware_url.insert(0, source, prefix_len);
233 }
234
235#ifdef ESPHOME_PROJECT_VERSION
236 info->current_version = ESPHOME_PROJECT_VERSION;
237#else
238 info->current_version = ESPHOME_VERSION;
239#endif
240 }
241
242defer:
243 // Release container before vTaskDelete (which doesn't call destructors)
244 container.reset();
245
246 // Defer to the main loop so all update_info_ and state_ writes happen on the
247 // same thread as readers (API, MQTT, web server). This is a single defer for
248 // both success and error paths to avoid multiple std::function instantiations.
249 // Lambda captures only 2 pointers (8 bytes) — fits in std::function SBO on supported toolchains.
250 this_update->defer([this_update, result]() {
251#ifdef USE_ESP32
252 this_update->task_result_ = nullptr;
253 this_update->end_check_(result);
254#endif
255 if (result->error_str != nullptr) {
256 this_update->status_set_error(result->error_str);
257 delete result;
258 return;
259 }
260
261 // Determine new state on main loop (avoids extra lambda captures from task)
262 bool trigger_update_available = false;
263 update::UpdateState new_state;
264 if (result->info.latest_version.empty() || result->info.latest_version == result->info.current_version) {
266 } else {
268 if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) {
269 trigger_update_available = true;
270 }
271 }
272
273 this_update->update_info_ = std::move(result->info);
274 this_update->state_ = new_state;
275 delete result; // Safe: moved-from state is valid for destruction
276
277 this_update->status_clear_error();
278 this_update->publish_state();
279
280 if (trigger_update_available) {
281 this_update->get_update_available_trigger()->trigger(this_update->update_info_);
282 }
283 });
284
285 UPDATE_RETURN;
286}
287
289 if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) {
290 return;
291 }
292
294 this->publish_state();
295
296 this->ota_parent_->set_md5(this->update_info.md5);
298 // Flash in the next loop
299 this->defer([this]() { this->ota_parent_->flash(); });
300}
301
302} // namespace esphome::http_request
bool cancel_interval(const char *name)
Cancel an interval function.
Definition component.cpp:92
void status_clear_error()
Definition component.h:302
void defer(const char *name, std::function< void()> &&f)
Defer a callback to the next loop() call with a const char* name.
void set_interval(const char *name, uint32_t interval, std::function< void()> &&f)
Set an interval function with a const char* name.
Definition component.cpp:88
virtual uint32_t get_update_interval() const
Get the update interval in ms of this sensor.
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:2298
void deallocate(T *p, size_t n)
Definition helpers.h:2355
T * allocate(size_t n)
Definition helpers.h:2325
std::shared_ptr< HttpContainer > get(const char *url)
void on_ota_state(ota::OTAState state, float progress, uint8_t error) override
void set_source_url(const char *source_url)
Set the manifest URL at run time; the string is copied, so any storage may be passed.
void add_state_listener(OTAStateListener *listener)
const UpdateState & state
Trigger< const UpdateInfo & > * get_update_available_trigger()
const UpdateInfo & update_info
bool state
Definition fan.h:2
ESPHOME_LOG_TAG(TAG, "http_request")
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.
bool parse_json(const std::string &data, const json_parse_t &f)
Parse a JSON string and run the provided json parse function if it's valid.
Definition json_util.cpp:28
ESPHOME_ALWAYS_INLINE bool is_connected()
Return whether the node is connected to the network (through wifi, eth, ...)
Definition util.h:36
const void size_t len
Definition hal.h:68
constexpr uint32_t SCHEDULER_DONT_RUN
Definition component.h:63
bool valid
static void uint32_t