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