ESPHome 2026.3.0-dev
Loading...
Searching...
No Matches
online_image.cpp
Go to the documentation of this file.
1#include "online_image.h"
2#include "esphome/core/log.h"
3#include <algorithm>
4
5static const char *const TAG = "online_image";
6static const char *const ETAG_HEADER_NAME = "etag";
7static const char *const IF_NONE_MATCH_HEADER_NAME = "if-none-match";
8static const char *const LAST_MODIFIED_HEADER_NAME = "last-modified";
9static const char *const IF_MODIFIED_SINCE_HEADER_NAME = "if-modified-since";
10
11namespace esphome::online_image {
12
13OnlineImage::OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format,
14 image::ImageType type, image::Transparency transparency, image::Image *placeholder,
15 uint32_t buffer_size, bool is_big_endian)
16 : RuntimeImage(format, type, transparency, placeholder, is_big_endian, width, height),
17 download_buffer_(buffer_size),
18 download_buffer_initial_size_(buffer_size) {
19 this->set_url(url);
20}
21
22bool OnlineImage::validate_url_(const std::string &url) {
23 if (url.empty()) {
24 ESP_LOGE(TAG, "URL is empty");
25 return false;
26 }
27 if (url.length() > 2048) {
28 ESP_LOGE(TAG, "URL is too long");
29 return false;
30 }
31 if (url.compare(0, 7, "http://") != 0 && url.compare(0, 8, "https://") != 0) {
32 ESP_LOGE(TAG, "URL must start with http:// or https://");
33 return false;
34 }
35 return true;
36}
37
39 if (this->is_decoding()) {
40 ESP_LOGW(TAG, "Image already being updated.");
41 return;
42 }
43
44 if (!this->validate_url_(this->url_)) {
45 ESP_LOGE(TAG, "Invalid URL: %s", this->url_.c_str());
46 this->download_error_callback_.call();
47 return;
48 }
49
50 ESP_LOGD(TAG, "Updating image from %s", this->url_.c_str());
51
52 std::vector<http_request::Header> headers;
53
54 // Add caching headers if we have them
55 if (!this->etag_.empty()) {
56 headers.push_back({IF_NONE_MATCH_HEADER_NAME, this->etag_});
57 }
58 if (!this->last_modified_.empty()) {
59 headers.push_back({IF_MODIFIED_SINCE_HEADER_NAME, this->last_modified_});
60 }
61
62 // Add Accept header based on image format
63 const char *accept_mime_type;
64 switch (this->get_format()) {
65#ifdef USE_RUNTIME_IMAGE_BMP
67 accept_mime_type = "image/bmp,*/*;q=0.8";
68 break;
69#endif
70#ifdef USE_RUNTIME_IMAGE_JPEG
72 accept_mime_type = "image/jpeg,*/*;q=0.8";
73 break;
74#endif
75#ifdef USE_RUNTIME_IMAGE_PNG
77 accept_mime_type = "image/png,*/*;q=0.8";
78 break;
79#endif
80 default:
81 accept_mime_type = "image/*,*/*;q=0.8";
82 break;
83 }
84 headers.push_back({"Accept", accept_mime_type});
85
86 // User headers last so they can override any of the above
87 for (auto &header : this->request_headers_) {
88 headers.push_back(http_request::Header{header.first, header.second.value()});
89 }
90
91 this->downloader_ = this->parent_->get(this->url_, headers, {ETAG_HEADER_NAME, LAST_MODIFIED_HEADER_NAME});
92
93 if (this->downloader_ == nullptr) {
94 ESP_LOGE(TAG, "Download failed.");
95 this->end_connection_();
96 this->download_error_callback_.call();
97 return;
98 }
99
100 int http_code = this->downloader_->status_code;
101 if (http_code == HTTP_CODE_NOT_MODIFIED) {
102 // Image hasn't changed on server. Skip download.
103 ESP_LOGI(TAG, "Server returned HTTP 304 (Not Modified). Download skipped.");
104 this->end_connection_();
105 this->download_finished_callback_.call(true);
106 return;
107 }
108 if (http_code != HTTP_CODE_OK) {
109 ESP_LOGE(TAG, "HTTP result: %d", http_code);
110 this->end_connection_();
111 this->download_error_callback_.call();
112 return;
113 }
114
115 ESP_LOGD(TAG, "Starting download");
116 size_t total_size = this->downloader_->content_length;
117
118 // Initialize decoder with the known format
119 if (!this->begin_decode(total_size)) {
120 ESP_LOGE(TAG, "Failed to initialize decoder for format %d", this->get_format());
121 this->end_connection_();
122 this->download_error_callback_.call();
123 return;
124 }
125
126 // JPEG requires the complete image in the download buffer before decoding
127 if (this->get_format() == runtime_image::JPEG && total_size > this->download_buffer_.size()) {
128 this->download_buffer_.resize(total_size);
129 }
130
131 ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size);
132 this->start_time_ = ::time(nullptr);
133 this->enable_loop();
134}
135
137 if (!this->is_decoding()) {
138 // Not decoding at the moment => nothing to do.
139 this->disable_loop();
140 return;
141 }
142
143 if (!this->downloader_) {
144 ESP_LOGE(TAG, "Downloader not instantiated; cannot download");
145 this->end_connection_();
146 this->download_error_callback_.call();
147 return;
148 }
149
150 // Check if download is complete — use decoder's format-specific completion check
151 // to handle both known content-length and chunked transfer encoding
152 if (this->is_decode_finished() || (this->downloader_->content_length > 0 &&
153 this->downloader_->get_bytes_read() >= this->downloader_->content_length &&
154 this->download_buffer_.unread() == 0)) {
155 // Finalize decoding
156 this->end_decode();
157
158 ESP_LOGD(TAG, "Image fully downloaded, %zu bytes in %" PRIu32 "s", this->downloader_->get_bytes_read(),
159 (uint32_t) (::time(nullptr) - this->start_time_));
160
161 // Save caching headers
162 this->etag_ = this->downloader_->get_response_header(ETAG_HEADER_NAME);
163 this->last_modified_ = this->downloader_->get_response_header(LAST_MODIFIED_HEADER_NAME);
164
165 this->download_finished_callback_.call(false);
166 this->end_connection_();
167 return;
168 }
169
170 // Download and decode more data
171 size_t available = this->download_buffer_.free_capacity();
172 if (available > 0) {
173 // Download in chunks to avoid blocking
174 available = std::min(available, this->download_buffer_initial_size_);
175 auto len = this->downloader_->read(this->download_buffer_.append(), available);
176
177 if (len > 0) {
179
180 // Feed data to decoder
181 auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread());
182
183 if (consumed < 0) {
184 ESP_LOGE(TAG, "Error decoding image: %d", consumed);
185 this->end_connection_();
186 this->download_error_callback_.call();
187 return;
188 }
189
190 if (consumed > 0) {
191 this->download_buffer_.read(consumed);
192 }
193 } else if (len < 0) {
194 ESP_LOGE(TAG, "Error downloading image: %d", len);
195 this->end_connection_();
196 this->download_error_callback_.call();
197 return;
198 }
199 } else {
200 // Buffer is full, need to decode some data first
201 auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread());
202 if (consumed > 0) {
203 this->download_buffer_.read(consumed);
204 } else if (consumed < 0) {
205 ESP_LOGE(TAG, "Decode error with full buffer: %d", consumed);
206 this->end_connection_();
207 this->download_error_callback_.call();
208 return;
209 } else {
210 // Decoder can't process more data, might need complete image
211 // This is normal for JPEG which needs complete data
212 ESP_LOGV(TAG, "Decoder waiting for more data");
213 }
214 }
215}
216
218 // Abort any in-progress decode to free decoder resources.
219 // Use RuntimeImage::release() directly to avoid recursion with OnlineImage::release().
220 if (this->is_decoding()) {
221 RuntimeImage::release();
222 }
223 if (this->downloader_) {
224 this->downloader_->end();
225 this->downloader_ = nullptr;
226 }
227 this->download_buffer_.reset();
228 this->disable_loop();
229}
230
231void OnlineImage::add_on_finished_callback(std::function<void(bool)> &&callback) {
232 this->download_finished_callback_.add(std::move(callback));
233}
234
235void OnlineImage::add_on_error_callback(std::function<void()> &&callback) {
236 this->download_error_callback_.add(std::move(callback));
237}
238
240 // Clear cache headers
241 this->etag_ = "";
242 this->last_modified_ = "";
243
244 // End any active connection
245 this->end_connection_();
246
247 // Call parent's release to free the image buffer
248 RuntimeImage::release();
249}
250
251} // namespace esphome::online_image
void enable_loop()
Enable this component's loop.
void disable_loop()
Disable this component's loop.
esphome::http_request::HttpRequestComponent * parent_
Definition helpers.h:1629
std::shared_ptr< HttpContainer > get(const std::string &url)
std::string etag_
The value of the ETag HTTP header provided in the last response.
OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format, image::ImageType type, image::Transparency transparency, image::Image *placeholder, uint32_t buffer_size, bool is_big_endian=false)
Construct a new OnlineImage object.
bool validate_url_(const std::string &url)
CallbackManager< void(bool)> download_finished_callback_
std::vector< std::pair< std::string, TemplatableValue< std::string > > > request_headers_
void add_on_error_callback(std::function< void()> &&callback)
void add_on_finished_callback(std::function< void(bool)> &&callback)
void set_url(const std::string &url)
Set the URL to download the image from.
std::shared_ptr< http_request::HttpContainer > downloader_
CallbackManager< void()> download_error_callback_
size_t download_buffer_initial_size_
This is the initial size of the download buffer, not the current size.
std::string last_modified_
The value of the Last-Modified HTTP header provided in the last response.
void release()
Release the buffer storing the image.
bool is_decoding() const
Check if decoding is currently in progress.
bool end_decode()
Complete the decoding process.
int feed_data(uint8_t *data, size_t len)
Feed data to the decoder.
bool is_decode_finished() const
Check if the decoder has finished processing all data.
ImageFormat get_format() const
Get the image format.
bool begin_decode(size_t expected_size=0)
Begin decoding an image.
uint16_t type
ImageFormat
Image format types that can be decoded dynamically.
std::string size_t len
Definition helpers.h:817