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