ESPHome 2026.1.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#define UPDATE_RETURN vTaskDelete(nullptr) // Delete the current update task
15#else
16#define UPDATE_RETURN return
17#endif
18
19static const char *const TAG = "http_request.update";
20
21static const size_t MAX_READ_SIZE = 256;
22
24
25void HttpRequestUpdate::on_ota_state(ota::OTAState state, float progress, uint8_t error) {
28 this->update_info_.has_progress = true;
29 this->update_info_.progress = progress;
30 this->publish_state();
33 this->status_set_error(LOG_STR("Failed to install firmware"));
34 this->publish_state();
35 }
36}
37
39 if (!network::is_connected()) {
40 ESP_LOGD(TAG, "Network not connected, skipping update check");
41 return;
42 }
43#ifdef USE_ESP32
44 xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_);
45#else
46 this->update_task(this);
47#endif
48}
49
51 HttpRequestUpdate *this_update = (HttpRequestUpdate *) params;
52
53 auto container = this_update->request_parent_->get(this_update->source_url_);
54
55 if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
56 ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str());
57 // Defer to main loop to avoid race condition on component_state_ read-modify-write
58 this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to fetch manifest")); });
59 UPDATE_RETURN;
60 }
61
62 RAMAllocator<uint8_t> allocator;
63 uint8_t *data = allocator.allocate(container->content_length);
64 if (data == nullptr) {
65 ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length);
66 // Defer to main loop to avoid race condition on component_state_ read-modify-write
67 this_update->defer(
68 [this_update]() { this_update->status_set_error(LOG_STR("Failed to allocate memory for manifest")); });
69 container->end();
70 UPDATE_RETURN;
71 }
72
73 size_t read_index = 0;
74 while (container->get_bytes_read() < container->content_length) {
75 int read_bytes = container->read(data + read_index, MAX_READ_SIZE);
76
77 yield();
78
79 if (read_bytes <= 0) {
80 // Network error or connection closed - break to avoid infinite loop
81 break;
82 }
83
84 read_index += read_bytes;
85 }
86
87 bool valid = false;
88 { // Ensures the response string falls out of scope and deallocates before the task ends
89 std::string response((char *) data, read_index);
90 allocator.deallocate(data, container->content_length);
91
92 container->end();
93 container.reset(); // Release ownership of the container's shared_ptr
94
95 valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
96 if (!root["name"].is<const char *>() || !root["version"].is<const char *>() || !root["builds"].is<JsonArray>()) {
97 ESP_LOGE(TAG, "Manifest does not contain required fields");
98 return false;
99 }
100 this_update->update_info_.title = root["name"].as<std::string>();
101 this_update->update_info_.latest_version = root["version"].as<std::string>();
102
103 for (auto build : root["builds"].as<JsonArray>()) {
104 if (!build["chipFamily"].is<const char *>()) {
105 ESP_LOGE(TAG, "Manifest does not contain required fields");
106 return false;
107 }
108 if (build["chipFamily"] == ESPHOME_VARIANT) {
109 if (!build["ota"].is<JsonObject>()) {
110 ESP_LOGE(TAG, "Manifest does not contain required fields");
111 return false;
112 }
113 JsonObject ota = build["ota"].as<JsonObject>();
114 if (!ota["path"].is<const char *>() || !ota["md5"].is<const char *>()) {
115 ESP_LOGE(TAG, "Manifest does not contain required fields");
116 return false;
117 }
118 this_update->update_info_.firmware_url = ota["path"].as<std::string>();
119 this_update->update_info_.md5 = ota["md5"].as<std::string>();
120
121 if (ota["summary"].is<const char *>())
122 this_update->update_info_.summary = ota["summary"].as<std::string>();
123 if (ota["release_url"].is<const char *>())
124 this_update->update_info_.release_url = ota["release_url"].as<std::string>();
125
126 return true;
127 }
128 }
129 return false;
130 });
131 }
132
133 if (!valid) {
134 ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
135 // Defer to main loop to avoid race condition on component_state_ read-modify-write
136 this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to parse manifest JSON")); });
137 UPDATE_RETURN;
138 }
139
140 // Merge source_url_ and this_update->update_info_.firmware_url
141 if (this_update->update_info_.firmware_url.find("http") == std::string::npos) {
142 std::string path = this_update->update_info_.firmware_url;
143 if (path[0] == '/') {
144 std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8));
145 this_update->update_info_.firmware_url = domain + path;
146 } else {
147 std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1);
148 this_update->update_info_.firmware_url = domain + path;
149 }
150 }
151
152 { // Ensures the current version string falls out of scope and deallocates before the task ends
153 std::string current_version;
154#ifdef ESPHOME_PROJECT_VERSION
155 current_version = ESPHOME_PROJECT_VERSION;
156#else
157 current_version = ESPHOME_VERSION;
158#endif
159
160 this_update->update_info_.current_version = current_version;
161 }
162
163 bool trigger_update_available = false;
164
165 if (this_update->update_info_.latest_version.empty() ||
166 this_update->update_info_.latest_version == this_update->update_info_.current_version) {
168 } else {
169 if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) {
170 trigger_update_available = true;
171 }
173 }
174
175 // Defer to main loop to ensure thread-safe execution of:
176 // - status_clear_error() performs non-atomic read-modify-write on component_state_
177 // - publish_state() triggers API callbacks that write to the shared protobuf buffer
178 // which can be corrupted if accessed concurrently from task and main loop threads
179 // - update_available trigger to ensure consistent state when the trigger fires
180 this_update->defer([this_update, trigger_update_available]() {
181 this_update->update_info_.has_progress = false;
182 this_update->update_info_.progress = 0.0f;
183
184 this_update->status_clear_error();
185 this_update->publish_state();
186
187 if (trigger_update_available) {
188 this_update->get_update_available_trigger()->trigger(this_update->update_info_);
189 }
190 });
191
192 UPDATE_RETURN;
193}
194
196 if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) {
197 return;
198 }
199
201 this->publish_state();
202
203 this->ota_parent_->set_md5(this->update_info.md5);
205 // Flash in the next loop
206 this->defer([this]() { this->ota_parent_->flash(); });
207}
208
209} // namespace http_request
210} // namespace esphome
void defer(const std::string &name, std::function< void()> &&f)
Defer a callback to the next loop() call.
An STL allocator that uses SPI or internal RAM.
Definition helpers.h:1274
void deallocate(T *p, size_t n)
Definition helpers.h:1332
T * allocate(size_t n)
Definition helpers.h:1294
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:0
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:26
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
void IRAM_ATTR HOT yield()
Definition core.cpp:24