ESPHome 2026.3.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
26static const size_t MAX_READ_SIZE = 256;
27static constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0;
28static constexpr uint32_t INITIAL_CHECK_INTERVAL_MS = 10000;
29static constexpr uint8_t INITIAL_CHECK_MAX_ATTEMPTS = 6;
30
33
34 // Check periodically until network is ready
35 // Only if update interval is > total retry window to avoid redundant checks
37 this->get_update_interval() > INITIAL_CHECK_INTERVAL_MS * INITIAL_CHECK_MAX_ATTEMPTS) {
38 this->initial_check_remaining_ = INITIAL_CHECK_MAX_ATTEMPTS;
39 this->set_interval(INITIAL_CHECK_INTERVAL_ID, INITIAL_CHECK_INTERVAL_MS, [this]() {
40 bool connected = network::is_connected();
41 if (--this->initial_check_remaining_ == 0 || connected) {
42 this->cancel_interval(INITIAL_CHECK_INTERVAL_ID);
43 if (connected) {
44 this->update();
45 }
46 }
47 });
48 }
49}
50
51void HttpRequestUpdate::on_ota_state(ota::OTAState state, float progress, uint8_t error) {
54 this->update_info_.has_progress = true;
55 this->update_info_.progress = progress;
56 this->publish_state();
59 this->status_set_error(LOG_STR("Failed to install firmware"));
60 this->publish_state();
61 }
62}
63
65 if (!network::is_connected()) {
66 ESP_LOGD(TAG, "Network not connected, skipping update check");
67 return;
68 }
69 this->cancel_interval(INITIAL_CHECK_INTERVAL_ID);
70#ifdef USE_ESP32
71 xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_);
72#else
73 this->update_task(this);
74#endif
75}
76
78 HttpRequestUpdate *this_update = (HttpRequestUpdate *) params;
79
80 auto container = this_update->request_parent_->get(this_update->source_url_);
81
82 if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
83 ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str());
84 // Defer to main loop to avoid race condition on component_state_ read-modify-write
85 this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to fetch manifest")); });
86 UPDATE_RETURN;
87 }
88
89 RAMAllocator<uint8_t> allocator;
90 uint8_t *data = allocator.allocate(container->content_length);
91 if (data == nullptr) {
92 ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length);
93 // Defer to main loop to avoid race condition on component_state_ read-modify-write
94 this_update->defer(
95 [this_update]() { this_update->status_set_error(LOG_STR("Failed to allocate memory for manifest")); });
96 container->end();
97 UPDATE_RETURN;
98 }
99
100 auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE,
101 this_update->request_parent_->get_timeout());
102 if (read_result.status != HttpReadStatus::OK) {
103 if (read_result.status == HttpReadStatus::TIMEOUT) {
104 ESP_LOGE(TAG, "Timeout reading manifest");
105 } else {
106 ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code);
107 }
108 // Defer to main loop to avoid race condition on component_state_ read-modify-write
109 this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to read manifest")); });
110 allocator.deallocate(data, container->content_length);
111 container->end();
112 UPDATE_RETURN;
113 }
114 size_t read_index = container->get_bytes_read();
115 size_t content_length = container->content_length;
116
117 container->end();
118 container.reset(); // Release ownership of the container's shared_ptr
119
120 bool valid = false;
121 { // Scope to ensure JsonDocument is destroyed before deallocating buffer
122 valid = json::parse_json(data, read_index, [this_update](JsonObject root) -> bool {
123 if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() ||
124 !root[ESPHOME_F("builds")].is<JsonArray>()) {
125 ESP_LOGE(TAG, "Manifest does not contain required fields");
126 return false;
127 }
128 this_update->update_info_.title = root[ESPHOME_F("name")].as<std::string>();
129 this_update->update_info_.latest_version = root[ESPHOME_F("version")].as<std::string>();
130
131 for (auto build : root[ESPHOME_F("builds")].as<JsonArray>()) {
132 if (!build[ESPHOME_F("chipFamily")].is<const char *>()) {
133 ESP_LOGE(TAG, "Manifest does not contain required fields");
134 return false;
135 }
136 if (build[ESPHOME_F("chipFamily")] == ESPHOME_VARIANT) {
137 if (!build[ESPHOME_F("ota")].is<JsonObject>()) {
138 ESP_LOGE(TAG, "Manifest does not contain required fields");
139 return false;
140 }
141 JsonObject ota = build[ESPHOME_F("ota")].as<JsonObject>();
142 if (!ota[ESPHOME_F("path")].is<const char *>() || !ota[ESPHOME_F("md5")].is<const char *>()) {
143 ESP_LOGE(TAG, "Manifest does not contain required fields");
144 return false;
145 }
146 this_update->update_info_.firmware_url = ota[ESPHOME_F("path")].as<std::string>();
147 this_update->update_info_.md5 = ota[ESPHOME_F("md5")].as<std::string>();
148
149 if (ota[ESPHOME_F("summary")].is<const char *>())
150 this_update->update_info_.summary = ota[ESPHOME_F("summary")].as<std::string>();
151 if (ota[ESPHOME_F("release_url")].is<const char *>())
152 this_update->update_info_.release_url = ota[ESPHOME_F("release_url")].as<std::string>();
153
154 return true;
155 }
156 }
157 return false;
158 });
159 }
160 allocator.deallocate(data, content_length);
161
162 if (!valid) {
163 ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
164 // Defer to main loop to avoid race condition on component_state_ read-modify-write
165 this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to parse manifest JSON")); });
166 UPDATE_RETURN;
167 }
168
169 // Merge source_url_ and this_update->update_info_.firmware_url
170 if (this_update->update_info_.firmware_url.find("http") == std::string::npos) {
171 std::string path = this_update->update_info_.firmware_url;
172 if (path[0] == '/') {
173 std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8));
174 this_update->update_info_.firmware_url = domain + path;
175 } else {
176 std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1);
177 this_update->update_info_.firmware_url = domain + path;
178 }
179 }
180
181#ifdef ESPHOME_PROJECT_VERSION
182 this_update->update_info_.current_version = ESPHOME_PROJECT_VERSION;
183#else
184 this_update->update_info_.current_version = ESPHOME_VERSION;
185#endif
186
187 bool trigger_update_available = false;
188
189 if (this_update->update_info_.latest_version.empty() ||
190 this_update->update_info_.latest_version == this_update->update_info_.current_version) {
192 } else {
193 if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) {
194 trigger_update_available = true;
195 }
197 }
198
199 // Defer to main loop to ensure thread-safe execution of:
200 // - status_clear_error() performs non-atomic read-modify-write on component_state_
201 // - publish_state() triggers API callbacks that write to the shared protobuf buffer
202 // which can be corrupted if accessed concurrently from task and main loop threads
203 // - update_available trigger to ensure consistent state when the trigger fires
204 this_update->defer([this_update, trigger_update_available]() {
205 this_update->update_info_.has_progress = false;
206 this_update->update_info_.progress = 0.0f;
207
208 this_update->status_clear_error();
209 this_update->publish_state();
210
211 if (trigger_update_available) {
212 this_update->get_update_available_trigger()->trigger(this_update->update_info_);
213 }
214 });
215
216 UPDATE_RETURN;
217}
218
220 if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) {
221 return;
222 }
223
225 this->publish_state();
226
227 this->ota_parent_->set_md5(this->update_info.md5);
229 // Flash in the next loop
230 this->defer([this]() { this->ota_parent_->flash(); });
231}
232
233} // namespace http_request
234} // 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:493
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:350
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:372
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:1794
void deallocate(T *p, size_t n)
Definition helpers.h:1849
T * allocate(size_t n)
Definition helpers.h:1811
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:77
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
bool is_connected()
Return whether the node is connected to the network (through wifi, eth, ...)
Definition util.cpp:25
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
constexpr uint32_t SCHEDULER_DONT_RUN
Definition component.h:49