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