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