mirror of https://github.com/esphome/esphome.git
Merge a48ddcb1cf
into c7c0d97a5e
This commit is contained in:
commit
fb290e172a
|
@ -253,6 +253,7 @@ esphome/components/nextion/text_sensor/* @senexcrenshaw
|
|||
esphome/components/nfc/* @jesserockz @kbx81
|
||||
esphome/components/noblex/* @AGalfra
|
||||
esphome/components/number/* @esphome/core
|
||||
esphome/components/online_image/* @guillempages
|
||||
esphome/components/ota/* @esphome/core
|
||||
esphome/components/output/* @esphome/core
|
||||
esphome/components/pca6416a/* @Mat931
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
import logging
|
||||
from esphome import automation
|
||||
import esphome.config_validation as cv
|
||||
import esphome.codegen as cg
|
||||
from esphome.const import (
|
||||
CONF_BUFFER_SIZE,
|
||||
CONF_ESP8266_DISABLE_SSL_SUPPORT,
|
||||
CONF_FORMAT,
|
||||
CONF_ID,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_TYPE,
|
||||
CONF_RESIZE,
|
||||
CONF_URL,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.components.image import (
|
||||
Image_,
|
||||
IMAGE_TYPE,
|
||||
CONF_USE_TRANSPARENCY,
|
||||
validate_cross_dependencies,
|
||||
)
|
||||
from esphome.components.http_request import (
|
||||
CONF_FOLLOW_REDIRECTS,
|
||||
CONF_REDIRECT_LIMIT,
|
||||
CONF_TIMEOUT,
|
||||
CONF_USERAGENT,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["network", "display"]
|
||||
AUTO_LOAD = ["image"]
|
||||
CODEOWNERS = ["@guillempages"]
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
|
||||
CONF_ON_ERROR = "on_error"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
online_image_ns = cg.esphome_ns.namespace("online_image")
|
||||
|
||||
ImageFormat = online_image_ns.enum("ImageFormat")
|
||||
IMAGE_FORMAT = {"PNG": ImageFormat.PNG} # Add new supported formats here
|
||||
|
||||
OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_)
|
||||
|
||||
# Actions
|
||||
SetUrlAction = online_image_ns.class_(
|
||||
"OnlineImageSetUrlAction", automation.Action, cg.Parented.template(OnlineImage)
|
||||
)
|
||||
ReleaseImageAction = online_image_ns.class_(
|
||||
"OnlineImageReleaseAction", automation.Action, cg.Parented.template(OnlineImage)
|
||||
)
|
||||
|
||||
# Triggers
|
||||
DownloadFinishedTrigger = online_image_ns.class_(
|
||||
"DownloadFinishedTrigger", automation.Trigger.template()
|
||||
)
|
||||
DownloadErrorTrigger = online_image_ns.class_(
|
||||
"DownloadErrorTrigger", automation.Trigger.template()
|
||||
)
|
||||
|
||||
ONLINE_IMAGE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(OnlineImage),
|
||||
#
|
||||
# Image options
|
||||
#
|
||||
cv.Required(CONF_URL): cv.url,
|
||||
cv.Optional(CONF_RESIZE): cv.dimensions,
|
||||
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True),
|
||||
# Not setting default here on purpose; the default depends on the image type,
|
||||
# and thus will be set in the "validate_cross_dependencies" validator.
|
||||
cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean,
|
||||
#
|
||||
# Online Image specific options
|
||||
#
|
||||
cv.Optional(CONF_FORMAT, default="PNG"): cv.enum(IMAGE_FORMAT, upper=True),
|
||||
cv.Optional(CONF_BUFFER_SIZE, default=2048): cv.int_range(256, 65536),
|
||||
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadFinishedTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ERROR): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadErrorTrigger),
|
||||
}
|
||||
),
|
||||
#
|
||||
# HTTP Request options
|
||||
#
|
||||
cv.Optional(CONF_FOLLOW_REDIRECTS, True): cv.boolean,
|
||||
cv.Optional(CONF_REDIRECT_LIMIT, 3): cv.int_,
|
||||
cv.Optional(CONF_TIMEOUT, "5s"): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_USERAGENT, "ESPHome"): cv.string,
|
||||
cv.Optional(CONF_ESP8266_DISABLE_SSL_SUPPORT, False): cv.boolean,
|
||||
}
|
||||
).extend(cv.polling_component_schema("never"))
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
cv.All(
|
||||
ONLINE_IMAGE_SCHEMA,
|
||||
validate_cross_dependencies,
|
||||
cv.require_framework_version(
|
||||
esp8266_arduino=cv.Version(2, 7, 0),
|
||||
esp32_arduino=cv.Version(0, 0, 0),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
SET_URL_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(OnlineImage),
|
||||
cv.Required(CONF_URL): cv.templatable(cv.url),
|
||||
}
|
||||
)
|
||||
|
||||
RELEASE_IMAGE_SCHEMA = automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(OnlineImage),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action("online_image.set_url", SetUrlAction, SET_URL_SCHEMA)
|
||||
@automation.register_action(
|
||||
"online_image.release", ReleaseImageAction, RELEASE_IMAGE_SCHEMA
|
||||
)
|
||||
async def online_image_action_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
if CONF_URL in config:
|
||||
template_ = await cg.templatable(config[CONF_URL], args, cg.const_char_ptr)
|
||||
cg.add(var.set_url(template_))
|
||||
return var
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_ONLINE_IMAGE")
|
||||
|
||||
if CORE.is_esp8266 and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]:
|
||||
cg.add_define("USE_HTTP_REQUEST_ESP8266_HTTPS")
|
||||
|
||||
if CORE.is_esp32:
|
||||
cg.add_library("WiFiClientSecure", None)
|
||||
cg.add_library("HTTPClient", None)
|
||||
if CORE.is_esp8266:
|
||||
cg.add_library("ESP8266HTTPClient", None)
|
||||
|
||||
if config[CONF_FORMAT] in ["PNG"]:
|
||||
cg.add_define("ONLINE_IMAGE_PNG_SUPPORT")
|
||||
cg.add_library("pngle", None)
|
||||
|
||||
url = config[CONF_URL]
|
||||
width, height = config.get(CONF_RESIZE, (0, 0))
|
||||
transparent = config[CONF_USE_TRANSPARENCY]
|
||||
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
url,
|
||||
width,
|
||||
height,
|
||||
config[CONF_FORMAT],
|
||||
config[CONF_TYPE],
|
||||
config[CONF_BUFFER_SIZE],
|
||||
)
|
||||
await cg.register_component(var, config)
|
||||
cg.add(var.set_transparency(transparent))
|
||||
cg.add(
|
||||
var.set_follow_redirects(
|
||||
config[CONF_FOLLOW_REDIRECTS], config[CONF_REDIRECT_LIMIT]
|
||||
)
|
||||
)
|
||||
cg.add(var.set_timeout(config[CONF_TIMEOUT]))
|
||||
cg.add(var.set_useragent(config[CONF_USERAGENT]))
|
||||
|
||||
for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_ERROR, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
|
@ -0,0 +1,46 @@
|
|||
#ifdef USE_ARDUINO
|
||||
|
||||
#include "image_decoder.h"
|
||||
#include "online_image.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace online_image {
|
||||
|
||||
static const char *const TAG = "online_image.decoder";
|
||||
|
||||
void ImageDecoder::set_size(int width, int height) {
|
||||
image_->resize_(width, height);
|
||||
x_scale_ = static_cast<double>(image_->buffer_width_) / width;
|
||||
y_scale_ = static_cast<double>(image_->buffer_height_) / height;
|
||||
}
|
||||
|
||||
void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
|
||||
auto width = std::min(image_->buffer_width_, static_cast<int>(std::ceil((x + w) * x_scale_)));
|
||||
auto height = std::min(image_->buffer_height_, static_cast<int>(std::ceil((y + h) * y_scale_)));
|
||||
for (int i = x * x_scale_; i < width; i++) {
|
||||
for (int j = y * y_scale_; j < height; j++) {
|
||||
image_->draw_pixel_(i, j, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t *DownloadBuffer::data(size_t offset) {
|
||||
if (offset > buffer_.size()) {
|
||||
ESP_LOGE(TAG, "Tried to access beyond download buffer bounds!!!");
|
||||
return buffer_.data();
|
||||
}
|
||||
return buffer_.data() + offset;
|
||||
}
|
||||
|
||||
size_t DownloadBuffer::read(size_t len) {
|
||||
unread_ -= len;
|
||||
if (unread_ > 0) {
|
||||
memmove(data(), data(len), unread_);
|
||||
}
|
||||
return unread_;
|
||||
}
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ARDUINO
|
|
@ -0,0 +1,121 @@
|
|||
#pragma once
|
||||
#ifdef USE_ARDUINO
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include <HTTPClient.h>
|
||||
#endif
|
||||
#ifdef USE_ESP8266
|
||||
#include <ESP8266HTTPClient.h>
|
||||
#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
|
||||
#include <WiFiClientSecure.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include "esphome/core/color.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace online_image {
|
||||
|
||||
class OnlineImage;
|
||||
|
||||
/**
|
||||
* @brief Class to abstract decoding different image formats.
|
||||
*/
|
||||
class ImageDecoder {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Image Decoder object
|
||||
*
|
||||
* @param image The image to decode the stream into.
|
||||
*/
|
||||
ImageDecoder(OnlineImage *image) : image_(image) {}
|
||||
virtual ~ImageDecoder() = default;
|
||||
|
||||
/**
|
||||
* @brief Initialize the decoder.
|
||||
*
|
||||
* @param stream WiFiClient to read the data from, in case the decoder needs initial data to auto-configure itself.
|
||||
* @param download_size The total number of bytes that need to be download for the image.
|
||||
*/
|
||||
virtual void prepare(uint32_t download_size) { download_size_ = download_size; }
|
||||
|
||||
/**
|
||||
* @brief Decode a part of the image. It will try reading from the buffer.
|
||||
* There is no guarantee that the whole available buffer will be read/decoded;
|
||||
* the method will return the amount of bytes actually decoded, so that the
|
||||
* unread content can be moved to the beginning.
|
||||
*
|
||||
* @param buffer The buffer to read from.
|
||||
* @param size The maximum amount of bytes that can be read from the buffer.
|
||||
* @return int The amount of bytes read. It can be 0 if the buffer does not have enough content to meaningfully
|
||||
* decode anything, or negative in case of a decoding error.
|
||||
*/
|
||||
virtual int decode(uint8_t *buffer, size_t size);
|
||||
|
||||
/**
|
||||
* @brief Request the image to be resized once the actual dimensions are known.
|
||||
* Called by the callback functions, to be able to access the parent Image class.
|
||||
*
|
||||
* @param width The image's width.
|
||||
* @param height The image's height.
|
||||
*/
|
||||
void set_size(int width, int height);
|
||||
|
||||
/**
|
||||
* @brief Draw a rectangle on the display_buffer using the defined color.
|
||||
* Will check the given coordinates for out-of-bounds, and clip the rectangle accordingly.
|
||||
* In case of binary displays, the color will be converted to binary as well.
|
||||
* Called by the callback functions, to be able to access the parent Image class.
|
||||
*
|
||||
* @param x The left-most coordinate of the rectangle.
|
||||
* @param y The top-most coordinate of the rectangle.
|
||||
* @param w The width of the rectangle.
|
||||
* @param h The height of the rectangle.
|
||||
* @param color The color to draw the rectangle with.
|
||||
*/
|
||||
void draw(int x, int y, int w, int h, const Color &color);
|
||||
|
||||
bool is_finished() const { return decoded_bytes_ == download_size_; }
|
||||
|
||||
protected:
|
||||
OnlineImage *image_;
|
||||
// Initializing to 1, to ensure it is different than initial "decoded_bytes_".
|
||||
// Will be overwritten anyway once the download size is known.
|
||||
uint32_t download_size_ = 1;
|
||||
uint32_t decoded_bytes_ = 0;
|
||||
double x_scale_ = 1.0;
|
||||
double y_scale_ = 1.0;
|
||||
};
|
||||
|
||||
class DownloadBuffer {
|
||||
public:
|
||||
DownloadBuffer(size_t size) : buffer_(size) { reset(); }
|
||||
|
||||
uint8_t *data(size_t offset = 0);
|
||||
|
||||
uint8_t *append() { return data(unread_); }
|
||||
|
||||
size_t unread() const { return unread_; }
|
||||
size_t size() const { return buffer_.size(); }
|
||||
size_t free_capacity() const { return buffer_.size() - unread_; }
|
||||
|
||||
size_t read(size_t len);
|
||||
size_t write(size_t len) {
|
||||
unread_ += len;
|
||||
return unread_;
|
||||
}
|
||||
|
||||
void reset() { unread_ = 0; }
|
||||
|
||||
private:
|
||||
std::vector<uint8_t> buffer_;
|
||||
/** Total number of downloaded bytes not yet read. */
|
||||
size_t unread_;
|
||||
};
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ARDUINO
|
|
@ -0,0 +1,428 @@
|
|||
#ifdef USE_ARDUINO
|
||||
|
||||
#include "online_image.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include <HTTPClient.h>
|
||||
#endif
|
||||
#ifdef USE_ESP8266
|
||||
#include <ESP8266HTTPClient.h>
|
||||
#ifdef USE_HTTP_REQUEST_ESP8266_HTTPS
|
||||
#include <WiFiClientSecure.h>
|
||||
|
||||
#include <utility>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
static const char *const TAG = "online_image";
|
||||
|
||||
#include "image_decoder.h"
|
||||
|
||||
#ifdef ONLINE_IMAGE_PNG_SUPPORT
|
||||
#include "png_image.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace online_image {
|
||||
|
||||
using display::Display;
|
||||
using image::ImageType;
|
||||
|
||||
inline bool is_color_on(const Color &color) {
|
||||
// This produces the most accurate monochrome conversion, but is slightly slower.
|
||||
// return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127;
|
||||
|
||||
// Approximation using fast integer computations; produces acceptable results
|
||||
// Equivalent to 0.25 * R + 0.5 * G + 0.25 * B
|
||||
return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80;
|
||||
}
|
||||
|
||||
OnlineImage::OnlineImage(const char *url, int width, int height, ImageFormat format, ImageType type,
|
||||
uint32_t download_buffer_size)
|
||||
: Image(nullptr, 0, 0, type),
|
||||
buffer_(nullptr),
|
||||
url_(url),
|
||||
download_buffer_(download_buffer_size),
|
||||
format_(format),
|
||||
fixed_width_(width),
|
||||
fixed_height_(height) {}
|
||||
|
||||
void OnlineImage::draw(int x, int y, Display *display, Color color_on, Color color_off) {
|
||||
switch (type_) {
|
||||
case ImageType::IMAGE_TYPE_BINARY: {
|
||||
for (int img_x = 0; img_x < width_; img_x++) {
|
||||
for (int img_y = 0; img_y < height_; img_y++) {
|
||||
if (this->get_binary_pixel_(img_x, img_y)) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color_on);
|
||||
} else if (!this->transparent_) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color_off);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ImageType::IMAGE_TYPE_GRAYSCALE:
|
||||
for (int img_x = 0; img_x < width_; img_x++) {
|
||||
for (int img_y = 0; img_y < height_; img_y++) {
|
||||
auto color = this->get_grayscale_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ImageType::IMAGE_TYPE_RGB565:
|
||||
for (int img_x = 0; img_x < width_; img_x++) {
|
||||
for (int img_y = 0; img_y < height_; img_y++) {
|
||||
auto color = this->get_rgb565_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ImageType::IMAGE_TYPE_RGB24:
|
||||
for (int img_x = 0; img_x < width_; img_x++) {
|
||||
for (int img_y = 0; img_y < height_; img_y++) {
|
||||
auto color = this->get_rgb24_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ImageType::IMAGE_TYPE_RGBA:
|
||||
for (int img_x = 0; img_x < width_; img_x++) {
|
||||
for (int img_y = 0; img_y < height_; img_y++) {
|
||||
auto color = this->get_rgba_pixel_(img_x, img_y);
|
||||
if (color.w >= 0x80) {
|
||||
display->draw_pixel_at(x + img_x, y + img_y, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void OnlineImage::set_follow_redirects(bool follow, int limit) {
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266)
|
||||
if (follow) {
|
||||
http_.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS);
|
||||
} else {
|
||||
http_.setFollowRedirects(HTTPC_DISABLE_FOLLOW_REDIRECTS);
|
||||
}
|
||||
http_.setRedirectLimit(limit);
|
||||
#endif
|
||||
}
|
||||
|
||||
void OnlineImage::set_useragent(const char *useragent) {
|
||||
if (useragent != nullptr) {
|
||||
http_.setUserAgent(useragent);
|
||||
}
|
||||
}
|
||||
|
||||
void OnlineImage::release() {
|
||||
if (buffer_) {
|
||||
ESP_LOGD(TAG, "Deallocating old buffer...");
|
||||
allocator_.deallocate(buffer_, get_buffer_size_());
|
||||
buffer_ = nullptr;
|
||||
width_ = 0;
|
||||
height_ = 0;
|
||||
buffer_width_ = 0;
|
||||
buffer_height_ = 0;
|
||||
etag_ = "";
|
||||
end_connection_();
|
||||
}
|
||||
}
|
||||
|
||||
bool OnlineImage::resize_(int width_in, int height_in) {
|
||||
int width = fixed_width_;
|
||||
int height = fixed_height_;
|
||||
if (auto_resize_()) {
|
||||
width = width_in;
|
||||
height = height_in;
|
||||
if (width_ != width && height_ != height) {
|
||||
release();
|
||||
}
|
||||
}
|
||||
if (buffer_) {
|
||||
return false;
|
||||
}
|
||||
auto new_size = get_buffer_size_(width, height);
|
||||
ESP_LOGD(TAG, "Allocating new buffer of %d Bytes...", new_size);
|
||||
delay_microseconds_safe(2000);
|
||||
buffer_ = allocator_.allocate(new_size);
|
||||
if (buffer_) {
|
||||
buffer_width_ = width;
|
||||
buffer_height_ = height;
|
||||
width_ = width;
|
||||
ESP_LOGD(TAG, "New size: (%d, %d)", width, height);
|
||||
} else {
|
||||
#if defined(USE_ESP8266)
|
||||
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
|
||||
int max_block = ESP.getMaxFreeBlockSize();
|
||||
#elif defined(USE_ESP32)
|
||||
int max_block = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL);
|
||||
#endif
|
||||
ESP_LOGE(TAG, "allocation failed. Biggest block in heap: %d Bytes", max_block);
|
||||
end_connection_();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void OnlineImage::loop() {
|
||||
PollingComponent::loop();
|
||||
|
||||
if (!decoder_) {
|
||||
// Not decoding at the moment => nothing to do.
|
||||
return;
|
||||
}
|
||||
if (!http_.connected() || decoder_->is_finished()) {
|
||||
ESP_LOGD(TAG, "Image fully downloaded");
|
||||
data_start_ = buffer_;
|
||||
width_ = buffer_width_;
|
||||
height_ = buffer_height_;
|
||||
end_connection_();
|
||||
download_finished_callback_.call();
|
||||
return;
|
||||
}
|
||||
WiFiClient *stream = http_.getStreamPtr();
|
||||
size_t available = stream->available();
|
||||
if (available) {
|
||||
if (available > download_buffer_.free_capacity()) {
|
||||
available = download_buffer_.free_capacity();
|
||||
}
|
||||
|
||||
auto len = stream->readBytes(download_buffer_.append(), available);
|
||||
if (len > 0) {
|
||||
download_buffer_.write(len);
|
||||
auto fed = decoder_->decode(download_buffer_.data(), download_buffer_.unread());
|
||||
if (fed < 0) {
|
||||
ESP_LOGE(TAG, "Error when decoding image.");
|
||||
etag_ = ""; // Need to re-download
|
||||
end_connection_();
|
||||
download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
download_buffer_.read(fed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnlineImage::draw_pixel_(int x, int y, Color color) {
|
||||
if (!buffer_) {
|
||||
ESP_LOGE(TAG, "Buffer not allocated!");
|
||||
return;
|
||||
}
|
||||
if (x < 0 || y < 0 || x >= buffer_width_ || y >= buffer_height_) {
|
||||
ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y);
|
||||
return;
|
||||
}
|
||||
uint32_t pos = get_position_(x, y);
|
||||
switch (type_) {
|
||||
case ImageType::IMAGE_TYPE_BINARY: {
|
||||
uint32_t byte_num = pos;
|
||||
uint8_t bit_num = (x + y * buffer_width_) % 8;
|
||||
if ((has_transparency() && color.w > 127) || is_color_on(color)) {
|
||||
buffer_[byte_num] |= 1 << bit_num;
|
||||
} else {
|
||||
buffer_[byte_num] &= ~(1 << bit_num);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ImageType::IMAGE_TYPE_GRAYSCALE: {
|
||||
uint8_t gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
|
||||
if (has_transparency()) {
|
||||
if (gray == 1) {
|
||||
gray = 0;
|
||||
}
|
||||
if (color.w < 0x80) {
|
||||
gray = 1;
|
||||
}
|
||||
}
|
||||
buffer_[pos] = gray;
|
||||
break;
|
||||
}
|
||||
case ImageType::IMAGE_TYPE_RGB565: {
|
||||
uint16_t col565 = display::ColorUtil::color_to_565(color);
|
||||
if (has_transparency()) {
|
||||
if (col565 == 0x0020) {
|
||||
col565 = 0;
|
||||
}
|
||||
if (color.w < 0x80) {
|
||||
col565 = 0x0020;
|
||||
}
|
||||
}
|
||||
buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
|
||||
buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
|
||||
break;
|
||||
}
|
||||
case ImageType::IMAGE_TYPE_RGBA: {
|
||||
buffer_[pos + 0] = color.r;
|
||||
buffer_[pos + 1] = color.g;
|
||||
buffer_[pos + 2] = color.b;
|
||||
buffer_[pos + 3] = color.w;
|
||||
break;
|
||||
}
|
||||
case ImageType::IMAGE_TYPE_RGB24:
|
||||
default: {
|
||||
if (has_transparency()) {
|
||||
if (color.b == 1 && color.r == 0 && color.g == 0) {
|
||||
color.b = 0;
|
||||
}
|
||||
if (color.w < 0x80) {
|
||||
color.r = 0;
|
||||
color.g = 0;
|
||||
color.b = 1;
|
||||
}
|
||||
}
|
||||
buffer_[pos + 0] = color.r;
|
||||
buffer_[pos + 1] = color.g;
|
||||
buffer_[pos + 2] = color.b;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool OnlineImage::get_binary_pixel_(int x, int y) const {
|
||||
uint32_t pos = x + y * width_;
|
||||
uint8_t position_offset = pos % 8;
|
||||
uint8_t mask = 1 << position_offset;
|
||||
return buffer_[pos / 8] & mask;
|
||||
}
|
||||
|
||||
Color OnlineImage::get_grayscale_pixel_(int x, int y) const {
|
||||
auto pos = get_position_(x, y);
|
||||
uint8_t grey = buffer_[pos];
|
||||
uint8_t alpha;
|
||||
if (grey == 1 && has_transparency()) {
|
||||
alpha = 0;
|
||||
} else {
|
||||
alpha = 0xFF;
|
||||
}
|
||||
return Color(grey, grey, grey, alpha);
|
||||
}
|
||||
|
||||
Color OnlineImage::get_rgb565_pixel_(int x, int y) const {
|
||||
auto pos = get_position_(x, y);
|
||||
uint16_t col565 = encode_uint16(buffer_[pos], buffer_[pos + 1]);
|
||||
uint8_t alpha;
|
||||
if (col565 == 0x0020 && has_transparency()) {
|
||||
alpha = 0;
|
||||
} else {
|
||||
alpha = 0xFF;
|
||||
}
|
||||
return Color(static_cast<uint8_t>((col565 >> 8) & 0xF8), static_cast<uint8_t>((col565 & 0x7E0) >> 3),
|
||||
static_cast<uint8_t>((col565 & 0x1F) << 3), alpha);
|
||||
}
|
||||
|
||||
Color OnlineImage::get_color_pixel_(int x, int y) const {
|
||||
auto pos = get_position_(x, y);
|
||||
auto r = buffer_[pos + 0];
|
||||
auto g = buffer_[pos + 1];
|
||||
auto b = buffer_[pos + 2];
|
||||
auto a = (b == 1 && r == 0 && g == 0 && has_transparency()) ? 0 : 0xFF;
|
||||
return Color(r, g, b, a);
|
||||
}
|
||||
|
||||
Color OnlineImage::get_rgba_pixel_(int x, int y) const {
|
||||
if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) {
|
||||
return Color(0);
|
||||
}
|
||||
auto pos = get_position_(x, y);
|
||||
auto r = buffer_[pos + 0];
|
||||
auto g = buffer_[pos + 1];
|
||||
auto b = buffer_[pos + 2];
|
||||
auto a = buffer_[pos + 3];
|
||||
return Color(r, g, b, a);
|
||||
}
|
||||
|
||||
void OnlineImage::end_connection_() {
|
||||
http_.end();
|
||||
decoder_.reset();
|
||||
download_buffer_.reset();
|
||||
}
|
||||
|
||||
void OnlineImage::update() {
|
||||
if (decoder_) {
|
||||
ESP_LOGW(TAG, "Image already being updated.");
|
||||
return;
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Updating image");
|
||||
}
|
||||
|
||||
int begin_status = http_.begin(url_);
|
||||
if (!begin_status) {
|
||||
ESP_LOGE(TAG, "Could not download image from %s. Connection failed: %i", url_, begin_status);
|
||||
return;
|
||||
}
|
||||
|
||||
http_.setTimeout(timeout_);
|
||||
#if defined(USE_ESP32)
|
||||
http_.setConnectTimeout(timeout_);
|
||||
#endif
|
||||
if (etag_ != "") {
|
||||
http_.addHeader("If-None-Match", etag_, false, true);
|
||||
}
|
||||
|
||||
const char *header_keys[] = {"Content-Type", "Content-Length", "ETag"};
|
||||
http_.collectHeaders(header_keys, 3);
|
||||
|
||||
int http_code = http_.GET();
|
||||
if (http_code == HTTP_CODE_NOT_MODIFIED) {
|
||||
ESP_LOGI(TAG, "Image hasn't changed on server. Skipping download.");
|
||||
end_connection_();
|
||||
return;
|
||||
}
|
||||
if (http_code != HTTP_CODE_OK) {
|
||||
ESP_LOGE(TAG, "Could not download image from %s. Error code: %i", url_, http_code);
|
||||
end_connection_();
|
||||
download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
|
||||
String content_type = http_.header("Content-Type");
|
||||
String etag = http_.header("ETag");
|
||||
uint32_t total_size = http_.header("Content-Length").toInt();
|
||||
ESP_LOGD(TAG, "Content Type: %s", content_type.c_str());
|
||||
ESP_LOGD(TAG, "Content Length: %d", total_size);
|
||||
ESP_LOGD(TAG, "ETag: %s", etag.c_str());
|
||||
|
||||
if (etag != "" && etag == etag_) {
|
||||
ESP_LOGI(TAG, "Image hasn't changed on server. Skipping download.");
|
||||
end_connection_();
|
||||
return;
|
||||
}
|
||||
etag_ = etag;
|
||||
|
||||
#ifdef ONLINE_IMAGE_PNG_SUPPORT
|
||||
if (format_ == ImageFormat::PNG) {
|
||||
decoder_ = esphome::make_unique<PngDecoder>(this);
|
||||
}
|
||||
#endif // ONLINE_IMAGE_PNG_SUPPORT
|
||||
|
||||
if (!decoder_) {
|
||||
ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported.");
|
||||
end_connection_();
|
||||
download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
decoder_->prepare(total_size);
|
||||
ESP_LOGI(TAG, "Downloading image from %s", url_);
|
||||
}
|
||||
|
||||
void OnlineImage::add_on_finished_callback(std::function<void()> &&callback) {
|
||||
download_finished_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void OnlineImage::add_on_error_callback(std::function<void()> &&callback) {
|
||||
download_error_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ARDUINO
|
|
@ -0,0 +1,168 @@
|
|||
#pragma once
|
||||
#ifdef USE_ARDUINO
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/image/image.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include "image_decoder.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace online_image {
|
||||
/**
|
||||
* @brief Format that the image is encoded with.
|
||||
*/
|
||||
enum ImageFormat {
|
||||
/** Automatically detect from MIME type. */
|
||||
AUTO,
|
||||
/** JPEG format. Not supported yet. */
|
||||
JPEG,
|
||||
/** PNG format. */
|
||||
PNG,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Download an image from a given URL, and decode it using the specified decoder.
|
||||
* The image will then be stored in a buffer, so that it can be displayed.
|
||||
*/
|
||||
class OnlineImage : public PollingComponent, public image::Image {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Online Image object.
|
||||
*
|
||||
* @param url URL to download the image from.
|
||||
* @param width Desired width of the target image area.
|
||||
* @param height Desired height of the target image area.
|
||||
* @param format Format that the image is encoded in (@see ImageFormat).
|
||||
* @param buffer_size Size of the buffer used to download the image.
|
||||
*/
|
||||
OnlineImage(const char *url, int width, int height, ImageFormat format, image::ImageType type, uint32_t buffer_size);
|
||||
|
||||
void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override;
|
||||
|
||||
void update() override;
|
||||
void loop() override;
|
||||
|
||||
/** Set the URL to download the image from. */
|
||||
void set_url(const char *url) { url_ = url; }
|
||||
/**
|
||||
* Release the buffer storing the image. The image will need to be downloaded again
|
||||
* to be able to be displayed.
|
||||
*/
|
||||
void release();
|
||||
|
||||
void set_follow_redirects(bool follow, int limit);
|
||||
void set_useragent(const char *useragent);
|
||||
|
||||
void set_timeout(uint16_t timeout) { this->timeout_ = timeout; }
|
||||
|
||||
void add_on_finished_callback(std::function<void()> &&callback);
|
||||
void add_on_error_callback(std::function<void()> &&callback);
|
||||
|
||||
protected:
|
||||
bool get_binary_pixel_(int x, int y) const;
|
||||
Color get_rgba_pixel_(int x, int y) const;
|
||||
Color get_color_pixel_(int x, int y) const;
|
||||
Color get_rgb565_pixel_(int x, int y) const;
|
||||
Color get_grayscale_pixel_(int x, int y) const;
|
||||
|
||||
using Allocator = ExternalRAMAllocator<uint8_t>;
|
||||
Allocator allocator_{Allocator::Flags::ALLOW_FAILURE};
|
||||
|
||||
uint32_t get_buffer_size_() const { return get_buffer_size_(buffer_width_, buffer_height_); }
|
||||
int get_buffer_size_(int width, int height) const {
|
||||
return std::ceil(image::image_type_to_bpp(type_) * width * height / 8.0);
|
||||
}
|
||||
|
||||
int get_position_(int x, int y) const { return ((x + y * buffer_width_) * image::image_type_to_bpp(type_)) / 8; }
|
||||
|
||||
ALWAYS_INLINE bool auto_resize_() const { return fixed_width_ == 0 || fixed_height_ == 0; }
|
||||
|
||||
bool resize_(int width, int height);
|
||||
void draw_pixel_(int x, int y, Color color);
|
||||
|
||||
void end_connection_();
|
||||
|
||||
CallbackManager<void()> download_finished_callback_{};
|
||||
CallbackManager<void()> download_error_callback_{};
|
||||
|
||||
HTTPClient http_;
|
||||
std::unique_ptr<ImageDecoder> decoder_;
|
||||
|
||||
uint8_t *buffer_;
|
||||
const char *url_;
|
||||
String etag_ = "";
|
||||
DownloadBuffer download_buffer_;
|
||||
|
||||
const ImageFormat format_;
|
||||
|
||||
/** width requested on configuration, or 0 if non specified. */
|
||||
const int fixed_width_;
|
||||
/** height requested on configuration, or 0 if non specified. */
|
||||
const int fixed_height_;
|
||||
/**
|
||||
* Actual width of the current image. If fixed_width_ is specified,
|
||||
* this will be equal to it; otherwise it will be set once the decoding
|
||||
* starts and the original size is known.
|
||||
* This needs to be separate from "BaseImage::get_width()" because the latter
|
||||
* must return 0 until the image has been decoded (to avoid showing partially
|
||||
* decoded images).
|
||||
*/
|
||||
int buffer_width_;
|
||||
/**
|
||||
* Actual height of the current image. If fixed_height_ is specified,
|
||||
* this will be equal to it; otherwise it will be set once the decoding
|
||||
* starts and the original size is known.
|
||||
* This needs to be separate from "BaseImage::get_height()" because the latter
|
||||
* must return 0 until the image has been decoded (to avoid showing partially
|
||||
* decoded images).
|
||||
*/
|
||||
int buffer_height_;
|
||||
|
||||
uint16_t timeout_;
|
||||
|
||||
friend void ImageDecoder::set_size(int width, int height);
|
||||
friend void ImageDecoder::draw(int x, int y, int w, int h, const Color &color);
|
||||
};
|
||||
|
||||
template<typename... Ts> class OnlineImageSetUrlAction : public Action<Ts...> {
|
||||
public:
|
||||
OnlineImageSetUrlAction(OnlineImage *parent) : parent_(parent) {}
|
||||
TEMPLATABLE_VALUE(const char *, url)
|
||||
void play(Ts... x) override {
|
||||
this->parent_->set_url(this->url_.value(x...));
|
||||
this->parent_->update();
|
||||
}
|
||||
|
||||
protected:
|
||||
OnlineImage *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class OnlineImageReleaseAction : public Action<Ts...> {
|
||||
public:
|
||||
OnlineImageReleaseAction(OnlineImage *parent) : parent_(parent) {}
|
||||
TEMPLATABLE_VALUE(const char *, url)
|
||||
void play(Ts... x) override { this->parent_->release(); }
|
||||
|
||||
protected:
|
||||
OnlineImage *parent_;
|
||||
};
|
||||
|
||||
class DownloadFinishedTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit DownloadFinishedTrigger(OnlineImage *parent) {
|
||||
parent->add_on_finished_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
class DownloadErrorTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit DownloadErrorTrigger(OnlineImage *parent) {
|
||||
parent->add_on_error_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ARDUINO
|
|
@ -0,0 +1,71 @@
|
|||
#ifdef USE_ARDUINO
|
||||
|
||||
#include "png_image.h"
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef ONLINE_IMAGE_PNG_SUPPORT
|
||||
|
||||
static const char *const TAG = "online_image.png";
|
||||
|
||||
namespace esphome {
|
||||
namespace online_image {
|
||||
|
||||
/**
|
||||
* @brief Callback method that will be called by the PNGLE engine when the basic
|
||||
* data of the image is received (i.e. width and height);
|
||||
*
|
||||
* @param pngle The PNGLE object, including the context data.
|
||||
* @param w The width of the image.
|
||||
* @param h The height of the image.
|
||||
*/
|
||||
static void initCallback(pngle_t *pngle, uint32_t w, uint32_t h) {
|
||||
PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle);
|
||||
decoder->set_size(w, h);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Callback method that will be called by the PNGLE engine when a chunk
|
||||
* of the image is decoded.
|
||||
*
|
||||
* @param pngle The PNGLE object, including the context data.
|
||||
* @param x The X coordinate to draw the rectangle on.
|
||||
* @param y The Y coordinate to draw the rectangle on.
|
||||
* @param w The width of the rectangle to draw.
|
||||
* @param h The height of the rectangle to draw.
|
||||
* @param rgba The color to paint the rectangle in.
|
||||
*/
|
||||
static void drawCallback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint8_t rgba[4]) {
|
||||
PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle);
|
||||
Color color(rgba[0], rgba[1], rgba[2], rgba[3]);
|
||||
decoder->draw(x, y, w, h, color);
|
||||
}
|
||||
|
||||
void PngDecoder::prepare(uint32_t download_size) {
|
||||
ImageDecoder::prepare(download_size);
|
||||
pngle_set_user_data(pngle, this);
|
||||
pngle_set_init_callback(pngle, initCallback);
|
||||
pngle_set_draw_callback(pngle, drawCallback);
|
||||
}
|
||||
|
||||
int HOT PngDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
if (size < 256 && size < download_size_ - decoded_bytes_) {
|
||||
ESP_LOGD(TAG, "Waiting for data");
|
||||
return 0;
|
||||
}
|
||||
auto fed = pngle_feed(pngle, buffer, size);
|
||||
if (fed < 0) {
|
||||
ESP_LOGE(TAG, "Error decoding image: %s", pngle_error(pngle));
|
||||
} else {
|
||||
decoded_bytes_ += fed;
|
||||
}
|
||||
return fed;
|
||||
}
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
||||
|
||||
#endif // ONLINE_IMAGE_PNG_SUPPORT
|
||||
|
||||
#endif // USE_ARDUINO
|
|
@ -0,0 +1,37 @@
|
|||
#pragma once
|
||||
#ifdef USE_ARDUINO
|
||||
|
||||
#include "image_decoder.h"
|
||||
#ifdef ONLINE_IMAGE_PNG_SUPPORT
|
||||
|
||||
#include <pngle.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace online_image {
|
||||
|
||||
/**
|
||||
* @brief Image decoder specialization for PNG images.
|
||||
*/
|
||||
class PngDecoder : public ImageDecoder {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new PNG Decoder object.
|
||||
*
|
||||
* @param display The image to decode the stream into.
|
||||
*/
|
||||
PngDecoder(OnlineImage *image) : ImageDecoder(image), pngle(pngle_new()) {}
|
||||
virtual ~PngDecoder() { pngle_destroy(pngle); }
|
||||
|
||||
void prepare(uint32_t download_size) override;
|
||||
int HOT decode(uint8_t *buffer, size_t size) override;
|
||||
|
||||
private:
|
||||
pngle_t *pngle;
|
||||
};
|
||||
|
||||
} // namespace online_image
|
||||
} // namespace esphome
|
||||
|
||||
#endif // ONLINE_IMAGE_PNG_SUPPORT
|
||||
|
||||
#endif // USE_ARDUINO
|
|
@ -0,0 +1,51 @@
|
|||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
spi:
|
||||
- id: spi_main_lcd
|
||||
clk_pin: 16
|
||||
mosi_pin: 17
|
||||
miso_pin: 15
|
||||
|
||||
display:
|
||||
- platform: ili9xxx
|
||||
id: main_lcd
|
||||
model: ili9342
|
||||
cs_pin: 12
|
||||
dc_pin: 13
|
||||
reset_pin: 21
|
||||
lambda: |-
|
||||
it.fill(Color(0, 0, 0));
|
||||
it.image(0, 0, id(online_rgba_image));
|
||||
|
||||
# Purposely test that `online_image:` does auto-load `image:`
|
||||
# Keep the `image:` undefined.
|
||||
# image:
|
||||
online_image:
|
||||
- id: online_binary_image
|
||||
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
||||
type: BINARY
|
||||
resize: 50x50
|
||||
- id: online_binary_transparent_image
|
||||
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
||||
type: TRANSPARENT_BINARY
|
||||
format: png
|
||||
- id: online_rgba_image
|
||||
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
||||
type: RGBA
|
||||
- id: online_rgb24_image
|
||||
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
||||
type: RGB24
|
||||
use_transparency: true
|
||||
|
||||
# Check the set_url action
|
||||
time:
|
||||
- platform: sntp
|
||||
on_time:
|
||||
- at: "13:37:42"
|
||||
then:
|
||||
- online_image.set_url:
|
||||
id: online_rgba_image
|
||||
url: http://www.example.org/example.png
|
||||
|
Loading…
Reference in New Issue