ESPHome 2026.5.0-dev
Loading...
Searching...
No Matches
http_request_update.cpp
Go to the documentation of this file.
2
5
8
9namespace esphome {
10namespace http_request {
11
12// The update function runs in a task only on ESP32s.
13#ifdef USE_ESP32
14// vTaskDelete doesn't return, but clang-tidy doesn't know that
15#define UPDATE_RETURN \
16 do { \
17 vTaskDelete(nullptr); \
18 __builtin_unreachable(); \
19 } while (0)
20#else
21#define UPDATE_RETURN return
22#endif
23
24static const char *const TAG = "http_request.update";
25
26// Wraps UpdateInfo + error for the task→main-loop handoff.
27struct TaskResult {
28 update::UpdateInfo info;
29 const LogString *error_str{nullptr};
30};
31
32static const size_t MAX_READ_SIZE = 256;
33static constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0;
34static constexpr uint32_t INITIAL_CHECK_INTERVAL_MS = 10000;
35static constexpr uint8_t INITIAL_CHECK_MAX_ATTEMPTS = 6;
36
39
40 // Check periodically until network is ready
41 // Only if update interval is > total retry window to avoid redundant checks
43 this->get_update_interval() > INITIAL_CHECK_INTERVAL_MS * INITIAL_CHECK_MAX_ATTEMPTS) {
44 this->initial_check_remaining_ = INITIAL_CHECK_MAX_ATTEMPTS;
45 this->set_interval(INITIAL_CHECK_INTERVAL_ID, INITIAL_CHECK_INTERVAL_MS, [this]() {
46 bool connected = network::is_connected();
47 if (--this->initial_check_remaining_ == 0 || connected) {
48 this->cancel_interval(INITIAL_CHECK_INTERVAL_ID);
49 if (connected) {
50 this->update();
51 }
52 }
53 });
54 }
55}
56
57void HttpRequestUpdate::on_ota_state(ota::OTAState state, float progress, uint8_t error) {
60 this->update_info_.has_progress = true;
61 this->update_info_.progress = progress;
62 this->publish_state();
65 this->status_set_error(LOG_STR("Failed to install firmware"));
66 this->publish_state();
67 }
68}
69
71 if (!network::is_connected()) {
72 ESP_LOGD(TAG, "Network not connected, skipping update check");
73 return;
74 }
75 this->cancel_interval(INITIAL_CHECK_INTERVAL_ID);
76#ifdef USE_ESP32
77 if (this->update_task_handle_ != nullptr) {
78 ESP_LOGW(TAG, "Update check already in progress");
79 return;
80 }
81 xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_);
82#else
83 this->update_task(this);
84#endif
85}
86
88 HttpRequestUpdate *this_update = (HttpRequestUpdate *) params;
89
90 // Allocate once — every path below returns via the single defer at the end.
91 // On failure, error_str is set; on success it is nullptr.
92 auto *result = new TaskResult();
93 auto *info = &result->info;
94
95 auto container = this_update->request_parent_->get(this_update->source_url_);
96
97 if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
98 ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str());
99 if (container != nullptr)
100 container->end();
101 result->error_str = LOG_STR("Failed to fetch manifest");
102 goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
103 }
104
105 {
106 RAMAllocator<uint8_t> allocator;
107 uint8_t *data = allocator.allocate(container->content_length);
108 if (data == nullptr) {
109 ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length);
110 container->end();
111 result->error_str = LOG_STR("Failed to allocate memory for manifest");
112 goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
113 }
114
115 auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE,
116 this_update->request_parent_->get_timeout());
117 if (read_result.status != HttpReadStatus::OK) {
118 if (read_result.status == HttpReadStatus::TIMEOUT) {
119 ESP_LOGE(TAG, "Timeout reading manifest");
120 } else {
121 ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code);
122 }
123 allocator.deallocate(data, container->content_length);
124 container->end();
125 result->error_str = LOG_STR("Failed to read manifest");
126 goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
127 }
128 size_t read_index = container->get_bytes_read();
129 size_t content_length = container->content_length;
130
131 container->end();
132 container.reset(); // Release ownership of the container's shared_ptr
133
134 bool valid = false;
135 { // Scope to ensure JsonDocument is destroyed before deallocating buffer
136 valid = json::parse_json(data, read_index, [info](JsonObject root) -> bool {
137 if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() ||
138 !root[ESPHOME_F("builds")].is<JsonArray>()) {
139 ESP_LOGE(TAG, "Manifest does not contain required fields");
140 return false;
141 }
142 info->title = root[ESPHOME_F("name")].as<std::string>();
143 info->latest_version = root[ESPHOME_F("version")].as<std::string>();
144
145 auto builds_array = root[ESPHOME_F("builds")].as<JsonArray>();
146 for (auto build : builds_array) {
147 if (!build[ESPHOME_F("chipFamily")].is<const char *>()) {
148 ESP_LOGE(TAG, "Manifest does not contain required fields");
149 return false;
150 }
151 if (build[ESPHOME_F("chipFamily")] == ESPHOME_VARIANT) {
152 if (!build[ESPHOME_F("ota")].is<JsonObject>()) {
153 ESP_LOGE(TAG, "Manifest does not contain required fields");
154 return false;
155 }
156 JsonObject ota = build[ESPHOME_F("ota")].as<JsonObject>();
157 if (!ota[ESPHOME_F("path")].is<const char *>() || !ota[ESPHOME_F("md5")].is<const char *>()) {
158 ESP_LOGE(TAG, "Manifest does not contain required fields");
159 return false;
160 }
161 info->firmware_url = ota[ESPHOME_F("path")].as<std::string>();
162 info->md5 = ota[ESPHOME_F("md5")].as<std::string>();
163
164 if (ota[ESPHOME_F("summary")].is<const char *>())
165 info->summary = ota[ESPHOME_F("summary")].as<std::string>();
166 if (ota[ESPHOME_F("release_url")].is<const char *>())
167 info->release_url = ota[ESPHOME_F("release_url")].as<std::string>();
168
169 return true;
170 }
171 }
172 return false;
173 });
174 }
175 allocator.deallocate(data, content_length);
176
177 if (!valid) {
178 ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
179 result->error_str = LOG_STR("Failed to parse manifest JSON");
180 goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
181 }
182
183 // Merge source_url_ and firmware_url
184 if (!info->firmware_url.empty() && info->firmware_url.find("http") == std::string::npos) {
185 std::string path = info->firmware_url;
186 if (path[0] == '/') {
187 std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8));
188 info->firmware_url = domain + path;
189 } else {
190 std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1);
191 info->firmware_url = domain + path;
192 }
193 }
194
195#ifdef ESPHOME_PROJECT_VERSION
196 info->current_version = ESPHOME_PROJECT_VERSION;
197#else
198 info->current_version = ESPHOME_VERSION;
199#endif
200 }
201
202defer:
203 // Release container before vTaskDelete (which doesn't call destructors)
204 container.reset();
205
206 // Defer to the main loop so all update_info_ and state_ writes happen on the
207 // same thread as readers (API, MQTT, web server). This is a single defer for
208 // both success and error paths to avoid multiple std::function instantiations.
209 // Lambda captures only 2 pointers (8 bytes) — fits in std::function SBO on supported toolchains.
210 this_update->defer([this_update, result]() {
211#ifdef USE_ESP32
212 this_update->update_task_handle_ = nullptr;
213#endif
214 if (result->error_str != nullptr) {
215 this_update->status_set_error(result->error_str);
216 delete result;
217 return;
218 }
219
220 // Determine new state on main loop (avoids extra lambda captures from task)
221 bool trigger_update_available = false;
222 update::UpdateState new_state;
223 if (result->info.latest_version.empty() || result->info.latest_version == result->info.current_version) {
225 } else {
227 if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) {
228 trigger_update_available = true;
229 }
230 }
231
232 this_update->update_info_ = std::move(result->info);
233 this_update->state_ = new_state;
234 delete result; // Safe: moved-from state is valid for destruction
235
236 this_update->status_clear_error();
237 this_update->publish_state();
238
239 if (trigger_update_available) {
240 this_update->get_update_available_trigger()->trigger(this_update->update_info_);
241 }
242 });
243
244 UPDATE_RETURN;
245}
246
248 if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) {
249 return;
250 }
251
253 this->publish_state();
254
255 this->ota_parent_->set_md5(this->update_info.md5);
257 // Flash in the next loop
258 this->defer([this]() { this->ota_parent_->flash(); });
259}
260
261} // namespace http_request
262} // namespace esphome
ESPDEPRECATED("Use const char* overload instead. Removed in 2026.7.0", "2026.1.0") void defer(const std voi defer)(const char *name, std::function< void()> &&f)
Defer a callback to the next loop() call.
Definition component.h:560
void status_clear_error()
Definition component.h:312
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_interval(const std voi set_interval)(const char *name, uint32_t interval, std::function< void()> &&f)
Set an interval function with a unique name.
Definition component.h:417
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_interval(const std boo cancel_interval)(const char *name)
Cancel an interval function.
Definition component.h:439
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:2212
void deallocate(T *p, size_t n)
Definition helpers.h:2267
T * allocate(size_t n)
Definition helpers.h:2229
std::shared_ptr< HttpContainer > get(const std::string &url)
void on_ota_state(ota::OTAState state, float progress, uint8_t error) override
void add_state_listener(OTAStateListener *listener)
Definition ota_backend.h:66
const UpdateState & state
Trigger< const UpdateInfo & > * get_update_available_trigger()
const UpdateInfo & update_info
bool state
Definition fan.h:2
constexpr uint32_t INITIAL_CHECK_INTERVAL_ID
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:27
ESPHOME_ALWAYS_INLINE bool is_connected()
Return whether the node is connected to the network (through wifi, eth, ...)
Definition util.h:27
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
constexpr uint32_t SCHEDULER_DONT_RUN
Definition component.h:60
bool valid
static void uint32_t