diff --git a/CODEOWNERS b/CODEOWNERS index 609f3ddd03..92e69e66cb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -302,7 +302,7 @@ esphome/components/noblex/* @AGalfra esphome/components/npi19/* @bakerkj esphome/components/number/* @esphome/core esphome/components/one_wire/* @ssieb -esphome/components/online_image/* @guillempages +esphome/components/online_image/* @clydebarrow @guillempages esphome/components/opentherm/* @olegtarasov esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core diff --git a/README.md b/README.md index da1b2b3650..8e3d8f71aa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # ESPHome [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/) -[![ESPHome Logo](https://esphome.io/_images/logo-text.png)](https://esphome.io/) + + + + ESPHome Logo + + **Documentation:** https://esphome.io/ diff --git a/docker/Dockerfile b/docker/Dockerfile index 0bb558d35e..429f5c4a1f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -29,7 +29,7 @@ RUN \ # Use pinned versions so that we get updates with build caching && apt-get install -y --no-install-recommends \ python3-pip=23.0.1+dfsg-1 \ - python3-setuptools=66.1.1-1 \ + python3-setuptools=66.1.1-1+deb12u1 \ python3-venv=3.11.2-1+b1 \ python3-wheel=0.38.4-2 \ iputils-ping=3:20221126-1+deb12u1 \ diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py index 21a82649f0..f73b8ef08f 100644 --- a/esphome/components/animation/__init__.py +++ b/esphome/components/animation/__init__.py @@ -1,28 +1,10 @@ import logging -from esphome import automation, core +from esphome import automation import esphome.codegen as cg import esphome.components.image as espImage -from esphome.components.image import ( - CONF_USE_TRANSPARENCY, - LOCAL_SCHEMA, - SOURCE_LOCAL, - SOURCE_WEB, - WEB_SCHEMA, -) import esphome.config_validation as cv -from esphome.const import ( - CONF_FILE, - CONF_ID, - CONF_PATH, - CONF_RAW_DATA_ID, - CONF_REPEAT, - CONF_RESIZE, - CONF_SOURCE, - CONF_TYPE, - CONF_URL, -) -from esphome.core import CORE, HexInt +from esphome.const import CONF_ID, CONF_REPEAT _LOGGER = logging.getLogger(__name__) @@ -30,6 +12,7 @@ AUTO_LOAD = ["image"] CODEOWNERS = ["@syndlex"] DEPENDENCIES = ["display"] MULTI_CONF = True +MULTI_CONF_NO_DEFAULT = True CONF_LOOP = "loop" CONF_START_FRAME = "start_frame" @@ -51,86 +34,19 @@ SetFrameAction = animation_ns.class_( "AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_) ) -TYPED_FILE_SCHEMA = cv.typed_schema( +CONFIG_SCHEMA = espImage.IMAGE_SCHEMA.extend( { - SOURCE_LOCAL: LOCAL_SCHEMA, - SOURCE_WEB: WEB_SCHEMA, - }, - key=CONF_SOURCE, -) - - -def _file_schema(value): - if isinstance(value, str): - return validate_file_shorthand(value) - return TYPED_FILE_SCHEMA(value) - - -FILE_SCHEMA = cv.Schema(_file_schema) - - -def validate_file_shorthand(value): - value = cv.string_strict(value) - if value.startswith("http://") or value.startswith("https://"): - return FILE_SCHEMA( + cv.Required(CONF_ID): cv.declare_id(Animation_), + cv.Optional(CONF_LOOP): cv.All( { - CONF_SOURCE: SOURCE_WEB, - CONF_URL: value, + cv.Optional(CONF_START_FRAME, default=0): cv.positive_int, + cv.Optional(CONF_END_FRAME): cv.positive_int, + cv.Optional(CONF_REPEAT): cv.positive_int, } - ) - return FILE_SCHEMA( - { - CONF_SOURCE: SOURCE_LOCAL, - CONF_PATH: value, - } - ) - - -def validate_cross_dependencies(config): - """ - Validate fields whose possible values depend on other fields. - For example, validate that explicitly transparent image types - have "use_transparency" set to True. - Also set the default value for those kind of dependent fields. - """ - image_type = config[CONF_TYPE] - is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"] - # If the use_transparency option was not specified, set the default depending on the image type - if CONF_USE_TRANSPARENCY not in config: - config[CONF_USE_TRANSPARENCY] = is_transparent_type - - if is_transparent_type and not config[CONF_USE_TRANSPARENCY]: - raise cv.Invalid(f"Image type {image_type} must always be transparent.") - - return config - - -ANIMATION_SCHEMA = cv.Schema( - cv.All( - { - cv.Required(CONF_ID): cv.declare_id(Animation_), - cv.Required(CONF_FILE): FILE_SCHEMA, - cv.Optional(CONF_RESIZE): cv.dimensions, - cv.Optional(CONF_TYPE, default="BINARY"): cv.enum( - espImage.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, - cv.Optional(CONF_LOOP): cv.All( - { - cv.Optional(CONF_START_FRAME, default=0): cv.positive_int, - cv.Optional(CONF_END_FRAME): cv.positive_int, - cv.Optional(CONF_REPEAT): cv.positive_int, - } - ), - cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), - }, - validate_cross_dependencies, - ) + ), + }, ) -CONFIG_SCHEMA = ANIMATION_SCHEMA NEXT_FRAME_SCHEMA = automation.maybe_simple_id( { @@ -164,180 +80,26 @@ async def animation_action_to_code(config, action_id, template_arg, args): async def to_code(config): - from PIL import Image + ( + prog_arr, + width, + height, + image_type, + trans_value, + frame_count, + ) = await espImage.write_image(config, all_frames=True) - conf_file = config[CONF_FILE] - if conf_file[CONF_SOURCE] == SOURCE_LOCAL: - path = CORE.relative_config_path(conf_file[CONF_PATH]) - elif conf_file[CONF_SOURCE] == SOURCE_WEB: - path = espImage.compute_local_image_path(conf_file).as_posix() - else: - raise core.EsphomeError(f"Unknown animation source: {conf_file[CONF_SOURCE]}") - - try: - image = Image.open(path) - except Exception as e: - raise core.EsphomeError(f"Could not load image file {path}: {e}") - - width, height = image.size - frames = image.n_frames - if CONF_RESIZE in config: - new_width_max, new_height_max = config[CONF_RESIZE] - ratio = min(new_width_max / width, new_height_max / height) - width, height = int(width * ratio), int(height * ratio) - elif width > 500 or height > 500: - _LOGGER.warning( - 'The image "%s" you requested is very big. Please consider' - " using the resize parameter.", - path, - ) - - transparent = config[CONF_USE_TRANSPARENCY] - - if config[CONF_TYPE] == "GRAYSCALE": - data = [0 for _ in range(height * width * frames)] - pos = 0 - for frameIndex in range(frames): - image.seek(frameIndex) - frame = image.convert("LA", dither=Image.Dither.NONE) - if CONF_RESIZE in config: - frame = frame.resize([width, height]) - pixels = list(frame.getdata()) - if len(pixels) != height * width: - raise core.EsphomeError( - f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})" - ) - for pix, a in pixels: - if transparent: - if pix == 1: - pix = 0 - if a < 0x80: - pix = 1 - - data[pos] = pix - pos += 1 - - elif config[CONF_TYPE] == "RGBA": - data = [0 for _ in range(height * width * 4 * frames)] - pos = 0 - for frameIndex in range(frames): - image.seek(frameIndex) - frame = image.convert("RGBA") - if CONF_RESIZE in config: - frame = frame.resize([width, height]) - pixels = list(frame.getdata()) - if len(pixels) != height * width: - raise core.EsphomeError( - f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})" - ) - for pix in pixels: - data[pos] = pix[0] - pos += 1 - data[pos] = pix[1] - pos += 1 - data[pos] = pix[2] - pos += 1 - data[pos] = pix[3] - pos += 1 - - elif config[CONF_TYPE] == "RGB24": - data = [0 for _ in range(height * width * 3 * frames)] - pos = 0 - for frameIndex in range(frames): - image.seek(frameIndex) - frame = image.convert("RGBA") - if CONF_RESIZE in config: - frame = frame.resize([width, height]) - pixels = list(frame.getdata()) - if len(pixels) != height * width: - raise core.EsphomeError( - f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})" - ) - for r, g, b, a in pixels: - if transparent: - if r == 0 and g == 0 and b == 1: - b = 0 - if a < 0x80: - r = 0 - g = 0 - b = 1 - - data[pos] = r - pos += 1 - data[pos] = g - pos += 1 - data[pos] = b - pos += 1 - - elif config[CONF_TYPE] in ["RGB565", "TRANSPARENT_IMAGE"]: - bytes_per_pixel = 3 if transparent else 2 - data = [0 for _ in range(height * width * bytes_per_pixel * frames)] - pos = 0 - for frameIndex in range(frames): - image.seek(frameIndex) - frame = image.convert("RGBA") - if CONF_RESIZE in config: - frame = frame.resize([width, height]) - pixels = list(frame.getdata()) - if len(pixels) != height * width: - raise core.EsphomeError( - f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})" - ) - for r, g, b, a in pixels: - R = r >> 3 - G = g >> 2 - B = b >> 3 - rgb = (R << 11) | (G << 5) | B - data[pos] = rgb >> 8 - pos += 1 - data[pos] = rgb & 0xFF - pos += 1 - if transparent: - data[pos] = a - pos += 1 - - elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: - width8 = ((width + 7) // 8) * 8 - data = [0 for _ in range((height * width8 // 8) * frames)] - for frameIndex in range(frames): - image.seek(frameIndex) - if transparent: - alpha = image.split()[-1] - has_alpha = alpha.getextrema()[0] < 0xFF - else: - has_alpha = False - frame = image.convert("1", dither=Image.Dither.NONE) - if CONF_RESIZE in config: - frame = frame.resize([width, height]) - if transparent: - alpha = alpha.resize([width, height]) - for x, y in [(i, j) for i in range(width) for j in range(height)]: - if transparent and has_alpha: - if not alpha.getpixel((x, y)): - continue - elif frame.getpixel((x, y)): - continue - - pos = x + y * width8 + (height * width8 * frameIndex) - data[pos // 8] |= 0x80 >> (pos % 8) - else: - raise core.EsphomeError( - f"Animation f{config[CONF_ID]} has not supported type {config[CONF_TYPE]}." - ) - - rhs = [HexInt(x) for x in data] - prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) var = cg.new_Pvariable( config[CONF_ID], prog_arr, width, height, - frames, - espImage.IMAGE_TYPE[config[CONF_TYPE]], + frame_count, + image_type, + trans_value, ) - cg.add(var.set_transparency(transparent)) if loop_config := config.get(CONF_LOOP): start = loop_config[CONF_START_FRAME] - end = loop_config.get(CONF_END_FRAME, frames) + end = loop_config.get(CONF_END_FRAME, frame_count) count = loop_config.get(CONF_REPEAT, -1) cg.add(var.set_loop(start, end, count)) diff --git a/esphome/components/animation/animation.cpp b/esphome/components/animation/animation.cpp index 1375dfe07e..6db6f1a7bd 100644 --- a/esphome/components/animation/animation.cpp +++ b/esphome/components/animation/animation.cpp @@ -6,8 +6,8 @@ namespace esphome { namespace animation { Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, - image::ImageType type) - : Image(data_start, width, height, type), + image::ImageType type, image::Transparency transparent) + : Image(data_start, width, height, type, transparent), animation_data_start_(data_start), current_frame_(0), animation_frame_count_(animation_frame_count), diff --git a/esphome/components/animation/animation.h b/esphome/components/animation/animation.h index 272c5153d1..c44e0060af 100644 --- a/esphome/components/animation/animation.h +++ b/esphome/components/animation/animation.h @@ -8,7 +8,8 @@ namespace animation { class Animation : public image::Image { public: - Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type); + Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type, + image::Transparency transparent); uint32_t get_animation_frame_count() const; int get_current_frame() const; diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index cbd4249d92..7d25bf5472 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -50,6 +50,10 @@ void DebugComponent::dump_config() { this->reset_reason_->publish_state(get_reset_reason_()); } #endif // USE_TEXT_SENSOR + +#ifdef USE_ESP32 + this->log_partition_info_(); // Log partition information for ESP32 +#endif // USE_ESP32 } void DebugComponent::loop() { diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h index 2b54406603..608addb4a3 100644 --- a/esphome/components/debug/debug_component.h +++ b/esphome/components/debug/debug_component.h @@ -55,6 +55,20 @@ class DebugComponent : public PollingComponent { #endif // USE_ESP32 #endif // USE_SENSOR +#ifdef USE_ESP32 + /** + * @brief Logs information about the device's partition table. + * + * This function iterates through the ESP32's partition table and logs details + * about each partition, including its name, type, subtype, starting address, + * and size. The information is useful for diagnosing issues related to flash + * memory or verifying the partition configuration dynamically at runtime. + * + * Only available when compiled for ESP32 platforms. + */ + void log_partition_info_(); +#endif // USE_ESP32 + #ifdef USE_TEXT_SENSOR text_sensor::TextSensor *device_info_{nullptr}; text_sensor::TextSensor *reset_reason_{nullptr}; diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index 5f7b9cdbb0..69ae7e3678 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #if defined(USE_ESP32_VARIANT_ESP32) #include @@ -28,6 +29,19 @@ namespace debug { static const char *const TAG = "debug"; +void DebugComponent::log_partition_info_() { + ESP_LOGCONFIG(TAG, "Partition table:"); + ESP_LOGCONFIG(TAG, " %-12s %-4s %-8s %-10s %-10s", "Name", "Type", "Subtype", "Address", "Size"); + esp_partition_iterator_t it = esp_partition_find(ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL); + while (it != NULL) { + const esp_partition_t *partition = esp_partition_get(it); + ESP_LOGCONFIG(TAG, " %-12s %-4d %-8d 0x%08X 0x%08X", partition->label, partition->type, partition->subtype, + partition->address, partition->size); + it = esp_partition_next(it); + } + esp_partition_iterator_release(it); +} + std::string DebugComponent::get_reset_reason_() { std::string reset_reason; switch (esp_reset_reason()) { @@ -276,6 +290,19 @@ void DebugComponent::get_device_info_(std::string &device_info) { device_info += " Cores:" + to_string(info.cores); device_info += " Revision:" + to_string(info.revision); + // Framework detection + device_info += "|Framework: "; +#ifdef USE_ARDUINO + ESP_LOGD(TAG, "Framework: Arduino"); + device_info += "Arduino"; +#elif defined(USE_ESP_IDF) + ESP_LOGD(TAG, "Framework: ESP-IDF"); + device_info += "ESP-IDF"; +#else + ESP_LOGW(TAG, "Framework: UNKNOWN"); + device_info += "UNKNOWN"; +#endif + ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version()); device_info += "|ESP-IDF: "; device_info += esp_get_idf_version(); diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index 98c3e91e46..70bd42e1a5 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -159,6 +159,15 @@ void DFPlayer::loop() { } break; case 9: // End byte +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + char byte_sequence[100]; + byte_sequence[0] = '\0'; + for (size_t i = 0; i < this->read_pos_ + 1; ++i) { + snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ", + this->read_buffer_[i]); + } + ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence); +#endif if (byte != 0xEF) { ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte); this->read_pos_ = 0; @@ -238,13 +247,17 @@ void DFPlayer::loop() { this->ack_set_is_playing_ = false; this->ack_reset_is_playing_ = false; break; + case 0x3C: + ESP_LOGV(TAG, "Playback finished (USB drive)"); + this->is_playing_ = false; + this->on_finished_playback_callback_.call(); case 0x3D: - ESP_LOGV(TAG, "Playback finished"); + ESP_LOGV(TAG, "Playback finished (SD card)"); this->is_playing_ = false; this->on_finished_playback_callback_.call(); break; default: - ESP_LOGV(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument); + ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument); } this->sent_cmd_ = 0; this->read_pos_ = 0; diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 48c8b2b04d..ff8e663ec1 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -58,7 +58,11 @@ uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } #else uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); } #endif -uint32_t arch_get_cpu_freq_hz() { return rtc_clk_apb_freq_get(); } +uint32_t arch_get_cpu_freq_hz() { + rtc_cpu_freq_config_t config; + rtc_clk_cpu_freq_get_config(&config); + return config.freq_mhz * 1000000U; +} #ifdef USE_ESP_IDF TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 67a0e31461..64104bb6de 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +import logging from esphome import pins import esphome.codegen as cg @@ -15,6 +16,9 @@ from esphome.const import ( CONF_RMT_CHANNEL, CONF_RMT_SYMBOLS, ) +from esphome.core import CORE + +_LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@jesserockz"] DEPENDENCIES = ["esp32"] @@ -64,13 +68,53 @@ CONF_RESET_HIGH = "reset_high" CONF_RESET_LOW = "reset_low" +class OptionalForIDF5(cv.SplitDefault): + @property + def default(self): + if not esp32_rmt.use_new_rmt_driver(): + return cv.UNDEFINED + return super().default + + @default.setter + def default(self, value): + # Ignore default set from vol.Optional + pass + + +def only_with_new_rmt_driver(obj): + if not esp32_rmt.use_new_rmt_driver(): + raise cv.Invalid( + "This feature is only available for the IDF framework version 5." + ) + return obj + + +def not_with_new_rmt_driver(obj): + if esp32_rmt.use_new_rmt_driver(): + raise cv.Invalid( + "This feature is not available for the IDF framework version 5." + ) + return obj + + def final_validation(config): - if not esp32_rmt.use_new_rmt_driver() and CONF_RMT_CHANNEL not in config: - raise cv.Invalid("rmt_channel is a required option.") + if not esp32_rmt.use_new_rmt_driver(): + if CONF_RMT_CHANNEL not in config: + if CORE.using_esp_idf: + raise cv.Invalid( + "rmt_channel is a required option for IDF version < 5." + ) + raise cv.Invalid( + "rmt_channel is a required option for the Arduino framework." + ) + _LOGGER.warning( + "RMT_LED_STRIP support for IDF version < 5 is deprecated and will be removed soon." + ) FINAL_VALIDATE_SCHEMA = final_validation + CONFIG_SCHEMA = cv.All( light.ADDRESSABLE_LIGHT_SCHEMA.extend( { @@ -79,9 +123,9 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True), cv.Optional(CONF_RMT_CHANNEL): cv.All( - cv.only_with_arduino, esp32_rmt.validate_rmt_channel(tx=True) + not_with_new_rmt_driver, esp32_rmt.validate_rmt_channel(tx=True) ), - cv.SplitDefault( + OptionalForIDF5( CONF_RMT_SYMBOLS, esp32_idf=64, esp32_s2_idf=64, @@ -89,7 +133,7 @@ CONFIG_SCHEMA = cv.All( esp32_c3_idf=48, esp32_c6_idf=48, esp32_h2_idf=48, - ): cv.All(cv.only_with_esp_idf, cv.int_range(min=2)), + ): cv.All(only_with_new_rmt_driver, cv.int_range(min=2)), cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 4669a3418a..801b05e160 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -6,7 +6,7 @@ import logging from pathlib import Path import re -import puremagic +from PIL import Image, UnidentifiedImageError from esphome import core, external_files import esphome.codegen as cg @@ -29,21 +29,236 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "image" DEPENDENCIES = ["display"] -MULTI_CONF = True -MULTI_CONF_NO_DEFAULT = True image_ns = cg.esphome_ns.namespace("image") ImageType = image_ns.enum("ImageType") + +CONF_OPAQUE = "opaque" +CONF_CHROMA_KEY = "chroma_key" +CONF_ALPHA_CHANNEL = "alpha_channel" +CONF_INVERT_ALPHA = "invert_alpha" + +TRANSPARENCY_TYPES = ( + CONF_OPAQUE, + CONF_CHROMA_KEY, + CONF_ALPHA_CHANNEL, +) + + +def get_image_type_enum(type): + return getattr(ImageType, f"IMAGE_TYPE_{type.upper()}") + + +def get_transparency_enum(transparency): + return getattr(TransparencyType, f"TRANSPARENCY_{transparency.upper()}") + + +class ImageEncoder: + """ + Superclass of image type encoders + """ + + # Control which transparency options are available for a given type + allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_OPAQUE} + + # All imageencoder types are valid + @staticmethod + def validate(value): + return value + + def __init__(self, width, height, transparency, dither, invert_alpha): + """ + :param width: The image width in pixels + :param height: The image height in pixels + :param transparency: Transparency type + :param dither: Dither method + :param invert_alpha: True if the alpha channel should be inverted; for monochrome formats inverts the colours. + """ + self.transparency = transparency + self.width = width + self.height = height + self.data = [0 for _ in range(width * height)] + self.dither = dither + self.index = 0 + self.invert_alpha = invert_alpha + + def convert(self, image): + """ + Convert the image format + :param image: Input image + :return: converted image + """ + return image + + def encode(self, pixel): + """ + Encode a single pixel + """ + + def end_row(self): + """ + Marks the end of a pixel row + :return: + """ + + +class ImageBinary(ImageEncoder): + allow_config = {CONF_OPAQUE, CONF_INVERT_ALPHA, CONF_CHROMA_KEY} + + def __init__(self, width, height, transparency, dither, invert_alpha): + self.width8 = (width + 7) // 8 + super().__init__(self.width8, height, transparency, dither, invert_alpha) + self.bitno = 0 + + def convert(self, image): + return image.convert("1", dither=self.dither) + + def encode(self, pixel): + if self.invert_alpha: + pixel = not pixel + if pixel: + self.data[self.index] |= 0x80 >> (self.bitno % 8) + self.bitno += 1 + if self.bitno == 8: + self.bitno = 0 + self.index += 1 + + def end_row(self): + """ + Pad rows to a byte boundary + """ + if self.bitno != 0: + self.bitno = 0 + self.index += 1 + + +class ImageGrayscale(ImageEncoder): + allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_INVERT_ALPHA, CONF_OPAQUE} + + def convert(self, image): + return image.convert("LA") + + def encode(self, pixel): + b, a = pixel + if self.transparency == CONF_CHROMA_KEY: + if b == 1: + b = 0 + if a != 0xFF: + b = 1 + if self.invert_alpha: + b ^= 0xFF + if self.transparency == CONF_ALPHA_CHANNEL: + if a != 0xFF: + b = a + self.data[self.index] = b + self.index += 1 + + +class ImageRGB565(ImageEncoder): + def __init__(self, width, height, transparency, dither, invert_alpha): + stride = 3 if transparency == CONF_ALPHA_CHANNEL else 2 + super().__init__( + width * stride, + height, + transparency, + dither, + invert_alpha, + ) + + def convert(self, image): + return image.convert("RGBA") + + def encode(self, pixel): + r, g, b, a = pixel + r = r >> 3 + g = g >> 2 + b = b >> 3 + if self.transparency == CONF_CHROMA_KEY: + if r == 0 and g == 1 and b == 0: + g = 0 + elif a < 128: + r = 0 + g = 1 + b = 0 + rgb = (r << 11) | (g << 5) | b + self.data[self.index] = rgb >> 8 + self.index += 1 + self.data[self.index] = rgb & 0xFF + self.index += 1 + if self.transparency == CONF_ALPHA_CHANNEL: + if self.invert_alpha: + a ^= 0xFF + self.data[self.index] = a + self.index += 1 + + +class ImageRGB(ImageEncoder): + def __init__(self, width, height, transparency, dither, invert_alpha): + stride = 4 if transparency == CONF_ALPHA_CHANNEL else 3 + super().__init__( + width * stride, + height, + transparency, + dither, + invert_alpha, + ) + + def convert(self, image): + return image.convert("RGBA") + + def encode(self, pixel): + r, g, b, a = pixel + if self.transparency == CONF_CHROMA_KEY: + if r == 0 and g == 1 and b == 0: + g = 0 + elif a < 128: + r = 0 + g = 1 + b = 0 + self.data[self.index] = r + self.index += 1 + self.data[self.index] = g + self.index += 1 + self.data[self.index] = b + self.index += 1 + if self.transparency == CONF_ALPHA_CHANNEL: + if self.invert_alpha: + a ^= 0xFF + self.data[self.index] = a + self.index += 1 + + +class ReplaceWith: + """ + Placeholder class to provide feedback on deprecated features + """ + + allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_OPAQUE} + + def __init__(self, replace_with): + self.replace_with = replace_with + + def validate(self, value): + raise cv.Invalid( + f"Image type {value} is removed; replace with {self.replace_with}" + ) + + IMAGE_TYPE = { - "BINARY": ImageType.IMAGE_TYPE_BINARY, - "TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_BINARY, - "GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE, - "RGB565": ImageType.IMAGE_TYPE_RGB565, - "RGB24": ImageType.IMAGE_TYPE_RGB24, - "RGBA": ImageType.IMAGE_TYPE_RGBA, + "BINARY": ImageBinary, + "GRAYSCALE": ImageGrayscale, + "RGB565": ImageRGB565, + "RGB": ImageRGB, + "TRANSPARENT_BINARY": ReplaceWith( + "'type: BINARY' and 'use_transparency: chroma_key'" + ), + "RGB24": ReplaceWith("'type: RGB'"), + "RGBA": ReplaceWith("'type: RGB' and 'use_transparency: alpha_channel'"), } +TransparencyType = image_ns.enum("TransparencyType") + CONF_USE_TRANSPARENCY = "use_transparency" # If the MDI file cannot be downloaded within this time, abort. @@ -53,17 +268,11 @@ SOURCE_LOCAL = "local" SOURCE_MDI = "mdi" SOURCE_WEB = "web" - Image_ = image_ns.class_("Image") -def _compute_local_icon_path(value: dict) -> Path: - base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi" - return base_dir / f"{value[CONF_ICON]}.svg" - - -def compute_local_image_path(value: dict) -> Path: - url = value[CONF_URL] +def compute_local_image_path(value) -> Path: + url = value[CONF_URL] if isinstance(value, dict) else value h = hashlib.new("sha256") h.update(url.encode()) key = h.hexdigest()[:8] @@ -71,30 +280,38 @@ def compute_local_image_path(value: dict) -> Path: return base_dir / key -def download_mdi(value): - validate_cairosvg_installed(value) +def local_path(value): + value = value[CONF_PATH] if isinstance(value, dict) else value + return str(CORE.relative_config_path(value)) - mdi_id = value[CONF_ICON] - path = _compute_local_icon_path(value) + +def download_file(url, path): + external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT) + return str(path) + + +def download_mdi(value): + mdi_id = value[CONF_ICON] if isinstance(value, dict) else value + base_dir = external_files.compute_local_file_dir(DOMAIN) / "mdi" + path = base_dir / f"{mdi_id}.svg" url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg" - - external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT) - - return value + return download_file(url, path) def download_image(value): - url = value[CONF_URL] - path = compute_local_image_path(value) - - external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT) - - return value + value = value[CONF_URL] if isinstance(value, dict) else value + return download_file(value, compute_local_image_path(value)) -def validate_cairosvg_installed(value): - """Validate that cairosvg is installed""" +def is_svg_file(file): + if not file: + return False + with open(file, "rb") as f: + return " 500 or height > 500): + if not resize and (width > 500 or height > 500): _LOGGER.warning( 'The image "%s" you requested is very big. Please consider' " using the resize parameter.", path, ) - transparent = config[CONF_USE_TRANSPARENCY] - dither = ( Image.Dither.NONE if config[CONF_DITHER] == "NONE" else Image.Dither.FLOYDSTEINBERG ) - if config[CONF_TYPE] == "GRAYSCALE": - image = image.convert("LA", dither=dither) - pixels = list(image.getdata()) - data = [0 for _ in range(height * width)] - pos = 0 - for g, a in pixels: - if transparent: - if g == 1: - g = 0 - if a < 0x80: - g = 1 + type = config[CONF_TYPE] + transparency = config[CONF_USE_TRANSPARENCY] + invert_alpha = config[CONF_INVERT_ALPHA] + frame_count = 1 + if all_frames: + try: + frame_count = image.n_frames + except AttributeError: + pass + if frame_count <= 1: + _LOGGER.warning("Image file %s has no animation frames", path) - data[pos] = g - pos += 1 + total_rows = height * frame_count + encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha) + for frame_index in range(frame_count): + image.seek(frame_index) + pixels = encoder.convert(image.resize((width, height))).getdata() + for row in range(height): + for col in range(width): + encoder.encode(pixels[row * width + col]) + encoder.end_row() - elif config[CONF_TYPE] == "RGBA": - image = image.convert("RGBA") - pixels = list(image.getdata()) - data = [0 for _ in range(height * width * 4)] - pos = 0 - for r, g, b, a in pixels: - data[pos] = r - pos += 1 - data[pos] = g - pos += 1 - data[pos] = b - pos += 1 - data[pos] = a - pos += 1 - - elif config[CONF_TYPE] == "RGB24": - image = image.convert("RGBA") - pixels = list(image.getdata()) - data = [0 for _ in range(height * width * 3)] - pos = 0 - for r, g, b, a in pixels: - if transparent: - if r == 0 and g == 0 and b == 1: - b = 0 - if a < 0x80: - r = 0 - g = 0 - b = 1 - - data[pos] = r - pos += 1 - data[pos] = g - pos += 1 - data[pos] = b - pos += 1 - - elif config[CONF_TYPE] in ["RGB565"]: - image = image.convert("RGBA") - pixels = list(image.getdata()) - bytes_per_pixel = 3 if transparent else 2 - data = [0 for _ in range(height * width * bytes_per_pixel)] - pos = 0 - for r, g, b, a in pixels: - R = r >> 3 - G = g >> 2 - B = b >> 3 - rgb = (R << 11) | (G << 5) | B - data[pos] = rgb >> 8 - pos += 1 - data[pos] = rgb & 0xFF - pos += 1 - if transparent: - data[pos] = a - pos += 1 - - elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: - if transparent: - alpha = image.split()[-1] - has_alpha = alpha.getextrema()[0] < 0xFF - _LOGGER.debug("%s Has alpha: %s", config[CONF_ID], has_alpha) - image = image.convert("1", dither=dither) - width8 = ((width + 7) // 8) * 8 - data = [0 for _ in range(height * width8 // 8)] - for y in range(height): - for x in range(width): - if transparent and has_alpha: - a = alpha.getpixel((x, y)) - if not a: - continue - elif image.getpixel((x, y)): - continue - pos = x + y * width8 - data[pos // 8] |= 0x80 >> (pos % 8) - else: - raise core.EsphomeError( - f"Image f{config[CONF_ID]} has an unsupported type: {config[CONF_TYPE]}." - ) - - rhs = [HexInt(x) for x in data] + rhs = [HexInt(x) for x in encoder.data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) - var = cg.new_Pvariable( - config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]] - ) - cg.add(var.set_transparency(transparent)) + image_type = get_image_type_enum(type) + trans_value = get_transparency_enum(transparency) + + return prog_arr, width, height, image_type, trans_value, frame_count + + +async def to_code(config): + if isinstance(config, list): + for entry in config: + await to_code(entry) + elif CONF_ID not in config: + for entry in config.values(): + await to_code(entry) + else: + prog_arr, width, height, image_type, trans_value, _ = await write_image(config) + cg.new_Pvariable( + config[CONF_ID], prog_arr, width, height, image_type, trans_value + ) diff --git a/esphome/components/image/image.cpp b/esphome/components/image/image.cpp index ca2f659fb0..e380112050 100644 --- a/esphome/components/image/image.cpp +++ b/esphome/components/image/image.cpp @@ -12,7 +12,7 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color 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_) { + } else if (!this->transparency_) { display->draw_pixel_at(x + img_x, y + img_y, color_off); } } @@ -39,20 +39,10 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color } } break; - case IMAGE_TYPE_RGB24: + case IMAGE_TYPE_RGB: 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 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); + auto color = this->get_rgb_pixel_(img_x, img_y); if (color.w >= 0x80) { display->draw_pixel_at(x + img_x, y + img_y, color); } @@ -61,20 +51,20 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color break; } } -Color Image::get_pixel(int x, int y, Color color_on, Color color_off) const { +Color Image::get_pixel(int x, int y, const Color color_on, const Color color_off) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) return color_off; switch (this->type_) { case IMAGE_TYPE_BINARY: - return this->get_binary_pixel_(x, y) ? color_on : color_off; + if (this->get_binary_pixel_(x, y)) + return color_on; + return color_off; case IMAGE_TYPE_GRAYSCALE: return this->get_grayscale_pixel_(x, y); case IMAGE_TYPE_RGB565: return this->get_rgb565_pixel_(x, y); - case IMAGE_TYPE_RGB24: - return this->get_rgb24_pixel_(x, y); - case IMAGE_TYPE_RGBA: - return this->get_rgba_pixel_(x, y); + case IMAGE_TYPE_RGB: + return this->get_rgb_pixel_(x, y); default: return color_off; } @@ -98,23 +88,40 @@ lv_img_dsc_t *Image::get_lv_img_dsc() { this->dsc_.header.cf = LV_IMG_CF_ALPHA_8BIT; break; - case IMAGE_TYPE_RGB24: - this->dsc_.header.cf = LV_IMG_CF_RGB888; + case IMAGE_TYPE_RGB: +#if LV_COLOR_DEPTH == 32 + switch (this->transparent_) { + case TRANSPARENCY_ALPHA_CHANNEL: + this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA; + break; + case TRANSPARENCY_CHROMA_KEY: + this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED; + break; + default: + this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR; + break; + } +#else + this->dsc_.header.cf = + this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGBA8888 : LV_IMG_CF_RGB888; +#endif break; case IMAGE_TYPE_RGB565: #if LV_COLOR_DEPTH == 16 - this->dsc_.header.cf = this->has_transparency() ? LV_IMG_CF_TRUE_COLOR_ALPHA : LV_IMG_CF_TRUE_COLOR; + switch (this->transparency_) { + case TRANSPARENCY_ALPHA_CHANNEL: + this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA; + break; + case TRANSPARENCY_CHROMA_KEY: + this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_CHROMA_KEYED; + break; + default: + this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR; + break; + } #else - this->dsc_.header.cf = LV_IMG_CF_RGB565; -#endif - break; - - case IMAGE_TYPE_RGBA: -#if LV_COLOR_DEPTH == 32 - this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR; -#else - this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA; + this->dsc_.header.cf = this->transparent_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565; #endif break; } @@ -128,51 +135,73 @@ bool Image::get_binary_pixel_(int x, int y) const { const uint32_t pos = x + y * width_8; return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); } -Color Image::get_rgba_pixel_(int x, int y) const { - const uint32_t pos = (x + y * this->width_) * 4; - return Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), - progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 3)); -} -Color Image::get_rgb24_pixel_(int x, int y) const { - const uint32_t pos = (x + y * this->width_) * 3; +Color Image::get_rgb_pixel_(int x, int y) const { + const uint32_t pos = (x + y * this->width_) * this->bpp_ / 8; Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), - progmem_read_byte(this->data_start_ + pos + 2)); - if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) { - // (0, 0, 1) has been defined as transparent color for non-alpha images. - // putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if) - color.w = 0; - } else { - color.w = 0xFF; + progmem_read_byte(this->data_start_ + pos + 2), 0xFF); + + switch (this->transparency_) { + case TRANSPARENCY_CHROMA_KEY: + if (color.g == 1 && color.r == 0 && color.b == 0) { + // (0, 1, 0) has been defined as transparent color for non-alpha images. + color.w = 0; + } + break; + case TRANSPARENCY_ALPHA_CHANNEL: + color.w = progmem_read_byte(this->data_start_ + (pos + 3)); + break; + default: + break; } return color; } Color Image::get_rgb565_pixel_(int x, int y) const { - const uint8_t *pos = this->data_start_; - if (this->transparent_) { - pos += (x + y * this->width_) * 3; - } else { - pos += (x + y * this->width_) * 2; - } + const uint8_t *pos = this->data_start_ + (x + y * this->width_) * this->bpp_ / 8; uint16_t rgb565 = encode_uint16(progmem_read_byte(pos), progmem_read_byte(pos + 1)); auto r = (rgb565 & 0xF800) >> 11; auto g = (rgb565 & 0x07E0) >> 5; auto b = rgb565 & 0x001F; - auto a = this->transparent_ ? progmem_read_byte(pos + 2) : 0xFF; - Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2), a); - return color; + auto a = 0xFF; + switch (this->transparency_) { + case TRANSPARENCY_ALPHA_CHANNEL: + a = progmem_read_byte(pos + 2); + break; + case TRANSPARENCY_CHROMA_KEY: + if (rgb565 == 0x0020) + a = 0; + break; + default: + break; + } + return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2), a); } Color Image::get_grayscale_pixel_(int x, int y) const { const uint32_t pos = (x + y * this->width_); const uint8_t gray = progmem_read_byte(this->data_start_ + pos); - uint8_t alpha = (gray == 1 && transparent_) ? 0 : 0xFF; + uint8_t alpha = (gray == 1 && this->transparency_ == TRANSPARENCY_CHROMA_KEY) ? 0 : 0xFF; return Color(gray, gray, gray, alpha); } int Image::get_width() const { return this->width_; } int Image::get_height() const { return this->height_; } ImageType Image::get_type() const { return this->type_; } -Image::Image(const uint8_t *data_start, int width, int height, ImageType type) - : width_(width), height_(height), type_(type), data_start_(data_start) {} +Image::Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency) + : width_(width), height_(height), type_(type), data_start_(data_start), transparency_(transparency) { + switch (this->type_) { + case IMAGE_TYPE_BINARY: + this->bpp_ = 1; + break; + case IMAGE_TYPE_GRAYSCALE: + this->bpp_ = 8; + break; + case IMAGE_TYPE_RGB565: + this->bpp_ = transparency == TRANSPARENCY_ALPHA_CHANNEL ? 24 : 16; + break; + case IMAGE_TYPE_RGB: + this->bpp_ = this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? 32 : 24; + break; + } +} } // namespace image } // namespace esphome diff --git a/esphome/components/image/image.h b/esphome/components/image/image.h index 40370d18da..4024ab1357 100644 --- a/esphome/components/image/image.h +++ b/esphome/components/image/image.h @@ -12,51 +12,40 @@ namespace image { enum ImageType { IMAGE_TYPE_BINARY = 0, IMAGE_TYPE_GRAYSCALE = 1, - IMAGE_TYPE_RGB24 = 2, + IMAGE_TYPE_RGB = 2, IMAGE_TYPE_RGB565 = 3, - IMAGE_TYPE_RGBA = 4, +}; + +enum Transparency { + TRANSPARENCY_OPAQUE = 0, + TRANSPARENCY_CHROMA_KEY = 1, + TRANSPARENCY_ALPHA_CHANNEL = 2, }; class Image : public display::BaseImage { public: - Image(const uint8_t *data_start, int width, int height, ImageType type); + Image(const uint8_t *data_start, int width, int height, ImageType type, Transparency transparency); Color get_pixel(int x, int y, Color color_on = display::COLOR_ON, Color color_off = display::COLOR_OFF) const; int get_width() const override; int get_height() const override; const uint8_t *get_data_start() const { return this->data_start_; } ImageType get_type() const; - int get_bpp() const { - switch (this->type_) { - case IMAGE_TYPE_BINARY: - return 1; - case IMAGE_TYPE_GRAYSCALE: - return 8; - case IMAGE_TYPE_RGB565: - return this->transparent_ ? 24 : 16; - case IMAGE_TYPE_RGB24: - return 24; - case IMAGE_TYPE_RGBA: - return 32; - } - return 0; - } + int get_bpp() const { return this->bpp_; } /// Return the stride of the image in bytes, that is, the distance in bytes /// between two consecutive rows of pixels. - uint32_t get_width_stride() const { return (this->width_ * this->get_bpp() + 7u) / 8u; } + size_t get_width_stride() const { return (this->width_ * this->get_bpp() + 7u) / 8u; } void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; - void set_transparency(bool transparent) { transparent_ = transparent; } - bool has_transparency() const { return transparent_; } + bool has_transparency() const { return this->transparency_ != TRANSPARENCY_OPAQUE; } #ifdef USE_LVGL lv_img_dsc_t *get_lv_img_dsc(); #endif protected: bool get_binary_pixel_(int x, int y) const; - Color get_rgb24_pixel_(int x, int y) const; - Color get_rgba_pixel_(int x, int y) const; + Color get_rgb_pixel_(int x, int y) const; Color get_rgb565_pixel_(int x, int y) const; Color get_grayscale_pixel_(int x, int y) const; @@ -64,7 +53,9 @@ class Image : public display::BaseImage { int height_; ImageType type_; const uint8_t *data_start_; - bool transparent_; + Transparency transparency_; + size_t bpp_{}; + size_t stride_{}; #ifdef USE_LVGL lv_img_dsc_t dsc_{}; #endif diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 87b1cc6d2d..d50b2b483c 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -17,11 +17,11 @@ std::string build_json(const json_build_t &f) { auto free_heap = ALLOCATOR.get_max_free_block_size(); size_t request_size = std::min(free_heap, (size_t) 512); while (true) { - ESP_LOGV(TAG, "Attempting to allocate %u bytes for JSON serialization", request_size); + ESP_LOGV(TAG, "Attempting to allocate %zu bytes for JSON serialization", request_size); DynamicJsonDocument json_document(request_size); if (json_document.capacity() == 0) { ESP_LOGE(TAG, - "Could not allocate memory for JSON document! Requested %u bytes, largest free heap block: %u bytes", + "Could not allocate memory for JSON document! Requested %zu bytes, largest free heap block: %zu bytes", request_size, free_heap); return "{}"; } @@ -29,7 +29,7 @@ std::string build_json(const json_build_t &f) { f(root); if (json_document.overflowed()) { if (request_size == free_heap) { - ESP_LOGE(TAG, "Could not allocate memory for JSON document! Overflowed largest free heap block: %u bytes", + ESP_LOGE(TAG, "Could not allocate memory for JSON document! Overflowed largest free heap block: %zu bytes", free_heap); return "{}"; } @@ -37,7 +37,7 @@ std::string build_json(const json_build_t &f) { continue; } json_document.shrinkToFit(); - ESP_LOGV(TAG, "Size after shrink %u bytes", json_document.capacity()); + ESP_LOGV(TAG, "Size after shrink %zu bytes", json_document.capacity()); std::string output; serializeJson(json_document, output); return output; diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index ceeb30baf5..a090f42aa7 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -147,7 +147,7 @@ class LibreTinyPreferences : public ESPPreferences { ESP_LOGV(TAG, "fdb_kv_get_obj('%s'): nullptr - the key might not be set yet", to_save.key.c_str()); return true; } - stored_data.data.reserve(kv.value_len); + stored_data.data.resize(kv.value_len); fdb_blob_make(&blob, stored_data.data.data(), kv.value_len); size_t actual_len = fdb_kv_get_blob(db, to_save.key.c_str(), &blob); if (actual_len != kv.value_len) { diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 5abeead9d8..a9fe56fb32 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -501,9 +501,7 @@ size_t lv_millis(void) { return esphome::millis(); } void *lv_custom_mem_alloc(size_t size) { auto *ptr = malloc(size); // NOLINT if (ptr == nullptr) { -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR - esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); -#endif + ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); } return ptr; } @@ -520,30 +518,22 @@ void *lv_custom_mem_alloc(size_t size) { ptr = heap_caps_malloc(size, cap_bits); } if (ptr == nullptr) { -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR - esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); -#endif + ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); return nullptr; } -#ifdef ESPHOME_LOG_HAS_VERBOSE - esphome::ESP_LOGV(esphome::lvgl::TAG, "allocate %zu - > %p", size, ptr); -#endif + ESP_LOGV(esphome::lvgl::TAG, "allocate %zu - > %p", size, ptr); return ptr; } void lv_custom_mem_free(void *ptr) { -#ifdef ESPHOME_LOG_HAS_VERBOSE - esphome::ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr); -#endif + ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr); if (ptr == nullptr) return; heap_caps_free(ptr); } void *lv_custom_mem_realloc(void *ptr, size_t size) { -#ifdef ESPHOME_LOG_HAS_VERBOSE - esphome::ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size); -#endif + ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size); return heap_caps_realloc(ptr, size, cap_bits); } #endif diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 2b0d941220..e1002478a1 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -373,7 +373,7 @@ async def to_code(config): ) ) - cg.add(var.set_topic_prefix(config[CONF_TOPIC_PREFIX])) + cg.add(var.set_topic_prefix(config[CONF_TOPIC_PREFIX], CORE.name)) if config[CONF_USE_ABBREVIATIONS]: cg.add_define("USE_MQTT_ABBREVIATIONS") diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index c7ace505a8..9afa3a588d 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -606,7 +606,13 @@ void MQTTClientComponent::set_log_level(int level) { this->log_level_ = level; } void MQTTClientComponent::set_keep_alive(uint16_t keep_alive_s) { this->mqtt_backend_.set_keep_alive(keep_alive_s); } void MQTTClientComponent::set_log_message_template(MQTTMessage &&message) { this->log_message_ = std::move(message); } const MQTTDiscoveryInfo &MQTTClientComponent::get_discovery_info() const { return this->discovery_info_; } -void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix) { this->topic_prefix_ = topic_prefix; } +void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix, const std::string &check_topic_prefix) { + if (App.is_name_add_mac_suffix_enabled() && (topic_prefix == check_topic_prefix)) { + this->topic_prefix_ = str_sanitize(App.get_name()); + } else { + this->topic_prefix_ = topic_prefix; + } +} const std::string &MQTTClientComponent::get_topic_prefix() const { return this->topic_prefix_; } void MQTTClientComponent::set_publish_nan_as_none(bool publish_nan_as_none) { this->publish_nan_as_none_ = publish_nan_as_none; diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 34eac29464..c68b3c62eb 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -165,7 +165,7 @@ class MQTTClientComponent : public Component { * * @param topic_prefix The topic prefix. The last "/" is appended automatically. */ - void set_topic_prefix(const std::string &topic_prefix); + void set_topic_prefix(const std::string &topic_prefix, const std::string &check_topic_prefix); /// Get the topic prefix of this device, using default if necessary const std::string &get_topic_prefix() const; diff --git a/esphome/components/nextion/automation.h b/esphome/components/nextion/automation.h index 65f1fd0058..c718355af8 100644 --- a/esphome/components/nextion/automation.h +++ b/esphome/components/nextion/automation.h @@ -49,6 +49,23 @@ class TouchTrigger : public Trigger { } }; +template class NextionSetBrightnessAction : public Action { + public: + explicit NextionSetBrightnessAction(Nextion *component) : component_(component) {} + + TEMPLATABLE_VALUE(float, brightness) + + void play(Ts... x) override { + this->component_->set_brightness(this->brightness_.value(x...)); + this->component_->set_backlight_brightness(this->brightness_.value(x...)); + } + + void set_brightness(std::function brightness) { this->brightness_ = brightness; } + + protected: + Nextion *component_; +}; + template class NextionPublishFloatAction : public Action { public: explicit NextionPublishFloatAction(NextionComponent *component) : component_(component) {} diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index f6bd863d42..60f26e5234 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -1,30 +1,30 @@ -import esphome.codegen as cg -import esphome.config_validation as cv from esphome import automation -from esphome.components import display, uart -from esphome.components import esp32 +import esphome.codegen as cg +from esphome.components import display, esp32, uart +import esphome.config_validation as cv from esphome.const import ( + CONF_BRIGHTNESS, CONF_ID, CONF_LAMBDA, - CONF_BRIGHTNESS, - CONF_TRIGGER_ID, CONF_ON_TOUCH, + CONF_TRIGGER_ID, ) from esphome.core import CORE + from . import Nextion, nextion_ns, nextion_ref from .base_component import ( + CONF_AUTO_WAKE_ON_TOUCH, + CONF_EXIT_REPARSE_ON_START, CONF_ON_BUFFER_OVERFLOW, + CONF_ON_PAGE, + CONF_ON_SETUP, CONF_ON_SLEEP, CONF_ON_WAKE, - CONF_ON_SETUP, - CONF_ON_PAGE, + CONF_SKIP_CONNECTION_HANDSHAKE, + CONF_START_UP_PAGE, CONF_TFT_URL, CONF_TOUCH_SLEEP_TIMEOUT, CONF_WAKE_UP_PAGE, - CONF_START_UP_PAGE, - CONF_AUTO_WAKE_ON_TOUCH, - CONF_EXIT_REPARSE_ON_START, - CONF_SKIP_CONNECTION_HANDSHAKE, ) CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"] @@ -32,6 +32,9 @@ CODEOWNERS = ["@senexcrenshaw", "@edwardtfn"] DEPENDENCIES = ["uart"] AUTO_LOAD = ["binary_sensor", "switch", "sensor", "text_sensor"] +NextionSetBrightnessAction = nextion_ns.class_( + "NextionSetBrightnessAction", automation.Action +) SetupTrigger = nextion_ns.class_("SetupTrigger", automation.Trigger.template()) SleepTrigger = nextion_ns.class_("SleepTrigger", automation.Trigger.template()) WakeTrigger = nextion_ns.class_("WakeTrigger", automation.Trigger.template()) @@ -46,7 +49,7 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(Nextion), cv.Optional(CONF_TFT_URL): cv.url, - cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + cv.Optional(CONF_BRIGHTNESS): cv.percentage, cv.Optional(CONF_ON_SETUP): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SetupTrigger), @@ -92,12 +95,34 @@ CONFIG_SCHEMA = ( ) +@automation.register_action( + "display.nextion.set_brightness", + NextionSetBrightnessAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(Nextion), + cv.Required(CONF_BRIGHTNESS): cv.templatable(cv.percentage), + }, + key=CONF_BRIGHTNESS, + ), +) +async def nextion_set_brightness_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) + + template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, float) + cg.add(var.set_brightness(template_)) + + return var + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await uart.register_uart_device(var, config) if CONF_BRIGHTNESS in config: cg.add(var.set_brightness(config[CONF_BRIGHTNESS])) + if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( config[CONF_LAMBDA], [(nextion_ref, "it")], return_type=cg.void diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index e5df13c64e..67f08f68f8 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -273,7 +273,9 @@ void Nextion::loop() { this->sent_setup_commands_ = true; this->send_command_("bkcmd=3"); // Always, returns 0x00 to 0x23 result of serial command. - this->set_backlight_brightness(this->brightness_); + if (this->brightness_.has_value()) { + this->set_backlight_brightness(this->brightness_.value()); + } // Check if a startup page has been set and send the command if (this->start_up_page_ != -1) { diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index c293f80aee..b2404e1f0d 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1339,7 +1339,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe CallbackManager buffer_overflow_callback_{}; optional writer_; - float brightness_{1.0}; + optional brightness_; std::string device_model_; std::string firmware_version_; diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index be1bfb4a00..d1915c7364 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -4,14 +4,18 @@ from esphome import automation import esphome.codegen as cg from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent from esphome.components.image import ( + CONF_INVERT_ALPHA, CONF_USE_TRANSPARENCY, - IMAGE_TYPE, + IMAGE_SCHEMA, Image_, - validate_cross_dependencies, + get_image_type_enum, + get_transparency_enum, ) import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, + CONF_DITHER, + CONF_FILE, CONF_FORMAT, CONF_ID, CONF_ON_ERROR, @@ -23,7 +27,7 @@ from esphome.const import ( AUTO_LOAD = ["image"] DEPENDENCIES = ["display", "http_request"] -CODEOWNERS = ["@guillempages"] +CODEOWNERS = ["@guillempages", "@clydebarrow"] MULTI_CONF = True CONF_ON_DOWNLOAD_FINISHED = "on_download_finished" @@ -35,9 +39,30 @@ online_image_ns = cg.esphome_ns.namespace("online_image") ImageFormat = online_image_ns.enum("ImageFormat") -FORMAT_PNG = "PNG" -IMAGE_FORMAT = {FORMAT_PNG: ImageFormat.PNG} # Add new supported formats here +class Format: + def __init__(self, image_type): + self.image_type = image_type + + @property + def enum(self): + return getattr(ImageFormat, self.image_type) + + def actions(self): + pass + + +class PNGFormat(Format): + def __init__(self): + super().__init__("PNG") + + def actions(self): + cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT") + cg.add_library("pngle", "1.0.2") + + +# New formats can be added here. +IMAGE_FORMATS = {x.image_type: x for x in (PNGFormat(),)} OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_) @@ -57,48 +82,54 @@ DownloadErrorTrigger = online_image_ns.class_( "DownloadErrorTrigger", automation.Trigger.template() ) -ONLINE_IMAGE_SCHEMA = cv.Schema( - { - cv.Required(CONF_ID): cv.declare_id(OnlineImage), - cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), - # - # Common image options - # - 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.Required(CONF_URL): cv.url, - cv.Required(CONF_FORMAT): cv.enum(IMAGE_FORMAT, upper=True), - cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_), - 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), - } - ), + +def remove_options(*options): + return { + cv.Optional(option): cv.invalid( + f"{option} is an invalid option for online_image" + ) + for option in options } -).extend(cv.polling_component_schema("never")) + + +ONLINE_IMAGE_SCHEMA = ( + IMAGE_SCHEMA.extend(remove_options(CONF_FILE, CONF_INVERT_ALPHA, CONF_DITHER)) + .extend( + { + cv.Required(CONF_ID): cv.declare_id(OnlineImage), + cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent), + # Online Image specific options + cv.Required(CONF_URL): cv.url, + cv.Required(CONF_FORMAT): cv.one_of(*IMAGE_FORMATS, upper=True), + cv.Optional(CONF_PLACEHOLDER): cv.use_id(Image_), + 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), + } + ), + } + ) + .extend(cv.polling_component_schema("never")) +) CONFIG_SCHEMA = cv.Schema( cv.All( ONLINE_IMAGE_SCHEMA, - validate_cross_dependencies, cv.require_framework_version( # esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed # esp8266_arduino=cv.Version(2, 7, 0), esp32_arduino=cv.Version(0, 0, 0), esp_idf=cv.Version(4, 0, 0), rp2040_arduino=cv.Version(0, 0, 0), + host=cv.Version(0, 0, 0), ), ) ) @@ -132,29 +163,26 @@ async def online_image_action_to_code(config, action_id, template_arg, args): async def to_code(config): - format = config[CONF_FORMAT] - if format in [FORMAT_PNG]: - cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT") - cg.add_library("pngle", "1.0.2") + image_format = IMAGE_FORMATS[config[CONF_FORMAT]] + image_format.actions() url = config[CONF_URL] width, height = config.get(CONF_RESIZE, (0, 0)) - transparent = config[CONF_USE_TRANSPARENCY] + transparent = get_transparency_enum(config[CONF_USE_TRANSPARENCY]) var = cg.new_Pvariable( config[CONF_ID], url, width, height, - format, - config[CONF_TYPE], + image_format.enum, + get_image_type_enum(config[CONF_TYPE]), + transparent, config[CONF_BUFFER_SIZE], ) await cg.register_component(var, config) await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) - cg.add(var.set_transparency(transparent)) - if placeholder_id := config.get(CONF_PLACEHOLDER): placeholder = await cg.get_variable(placeholder_id) cg.add(var.set_placeholder(placeholder)) diff --git a/esphome/components/online_image/image_decoder.h b/esphome/components/online_image/image_decoder.h index 908efab987..cde7f572e3 100644 --- a/esphome/components/online_image/image_decoder.h +++ b/esphome/components/online_image/image_decoder.h @@ -1,5 +1,4 @@ #pragma once -#include "esphome/core/defines.h" #include "esphome/core/color.h" namespace esphome { @@ -23,7 +22,7 @@ class ImageDecoder { /** * @brief Initialize the decoder. * - * @param download_size The total number of bytes that need to be download for the image. + * @param download_size The total number of bytes that need to be downloaded for the image. */ virtual void prepare(uint32_t download_size) { this->download_size_ = download_size; } @@ -38,7 +37,7 @@ class ImageDecoder { * @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); + virtual int decode(uint8_t *buffer, size_t size) = 0; /** * @brief Request the image to be resized once the actual dimensions are known. @@ -50,7 +49,7 @@ class ImageDecoder { void set_size(int width, int height); /** - * @brief Draw a rectangle on the display_buffer using the defined color. + * @brief Fill 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. @@ -59,7 +58,7 @@ class ImageDecoder { * @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. + * @param color The fill color */ void draw(int x, int y, int w, int h, const Color &color); @@ -67,7 +66,7 @@ class ImageDecoder { protected: OnlineImage *image_; - // Initializing to 1, to ensure it is different than initial "decoded_bytes_". + // Initializing to 1, to ensure it is distinguishable from initial "decoded_bytes_". // Will be overwritten anyway once the download size is known. uint32_t download_size_ = 1; uint32_t decoded_bytes_ = 0; diff --git a/esphome/components/online_image/online_image.cpp b/esphome/components/online_image/online_image.cpp index 8c4669cba5..93d070c6a9 100644 --- a/esphome/components/online_image/online_image.cpp +++ b/esphome/components/online_image/online_image.cpp @@ -25,8 +25,8 @@ inline bool is_color_on(const Color &color) { } OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type, - uint32_t download_buffer_size) - : Image(nullptr, 0, 0, type), + image::Transparency transparency, uint32_t download_buffer_size) + : Image(nullptr, 0, 0, type, transparency), buffer_(nullptr), download_buffer_(download_buffer_size), format_(format), @@ -45,7 +45,7 @@ void OnlineImage::draw(int x, int y, display::Display *display, Color color_on, void OnlineImage::release() { if (this->buffer_) { - ESP_LOGD(TAG, "Deallocating old buffer..."); + ESP_LOGV(TAG, "Deallocating old buffer..."); this->allocator_.deallocate(this->buffer_, this->get_buffer_size_()); this->data_start_ = nullptr; this->buffer_ = nullptr; @@ -70,20 +70,19 @@ bool OnlineImage::resize_(int width_in, int height_in) { if (this->buffer_) { return false; } - auto new_size = this->get_buffer_size_(width, height); - ESP_LOGD(TAG, "Allocating new buffer of %d Bytes...", new_size); - delay_microseconds_safe(2000); + size_t new_size = this->get_buffer_size_(width, height); + ESP_LOGD(TAG, "Allocating new buffer of %zu bytes", new_size); this->buffer_ = this->allocator_.allocate(new_size); - if (this->buffer_) { - this->buffer_width_ = width; - this->buffer_height_ = height; - this->width_ = width; - ESP_LOGD(TAG, "New size: (%d, %d)", width, height); - } else { - ESP_LOGE(TAG, "allocation failed. Biggest block in heap: %zu Bytes", this->allocator_.get_max_free_block_size()); + if (this->buffer_ == nullptr) { + ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", new_size, + this->allocator_.get_max_free_block_size()); this->end_connection_(); return false; } + this->buffer_width_ = width; + this->buffer_height_ = height; + this->width_ = width; + ESP_LOGV(TAG, "New size: (%d, %d)", width, height); return true; } @@ -91,9 +90,8 @@ void OnlineImage::update() { if (this->decoder_) { ESP_LOGW(TAG, "Image already being updated."); return; - } else { - ESP_LOGI(TAG, "Updating image"); } + ESP_LOGI(TAG, "Updating image %s", this->url_.c_str()); this->downloader_ = this->parent_->get(this->url_); @@ -142,10 +140,11 @@ void OnlineImage::loop() { return; } if (!this->downloader_ || this->decoder_->is_finished()) { - ESP_LOGD(TAG, "Image fully downloaded"); this->data_start_ = buffer_; this->width_ = buffer_width_; this->height_ = buffer_height_; + ESP_LOGD(TAG, "Image fully downloaded, read %zu bytes, width/height = %d/%d", this->downloader_->get_bytes_read(), + this->width_, this->height_); this->end_connection_(); this->download_finished_callback_.call(); return; @@ -171,6 +170,19 @@ void OnlineImage::loop() { } } +void OnlineImage::map_chroma_key(Color &color) { + if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { + if (color.g == 1 && color.r == 0 && color.b == 0) { + color.g = 0; + } + if (color.w < 0x80) { + color.r = 0; + color.g = this->type_ == ImageType::IMAGE_TYPE_RGB565 ? 4 : 1; + color.b = 0; + } + } +} + void OnlineImage::draw_pixel_(int x, int y, Color color) { if (!this->buffer_) { ESP_LOGE(TAG, "Buffer not allocated!"); @@ -184,57 +196,53 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) { switch (this->type_) { case ImageType::IMAGE_TYPE_BINARY: { const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; - const uint32_t pos = x + y * width_8; - if ((this->has_transparency() && color.w > 127) || is_color_on(color)) { - this->buffer_[pos / 8u] |= (0x80 >> (pos % 8u)); + pos = x + y * width_8; + auto bitno = 0x80 >> (pos % 8u); + pos /= 8u; + auto on = is_color_on(color); + if (this->has_transparency() && color.w < 0x80) + on = false; + if (on) { + this->buffer_[pos] |= bitno; } else { - this->buffer_[pos / 8u] &= ~(0x80 >> (pos % 8u)); + this->buffer_[pos] &= ~bitno; } break; } case ImageType::IMAGE_TYPE_GRAYSCALE: { uint8_t gray = static_cast(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); - if (this->has_transparency()) { + if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { if (gray == 1) { gray = 0; } if (color.w < 0x80) { gray = 1; } + } else if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + if (color.w != 0xFF) + gray = color.w; } this->buffer_[pos] = gray; break; } case ImageType::IMAGE_TYPE_RGB565: { + this->map_chroma_key(color); uint16_t col565 = display::ColorUtil::color_to_565(color); this->buffer_[pos + 0] = static_cast((col565 >> 8) & 0xFF); this->buffer_[pos + 1] = static_cast(col565 & 0xFF); - if (this->has_transparency()) + if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { this->buffer_[pos + 2] = color.w; - break; - } - case ImageType::IMAGE_TYPE_RGBA: { - this->buffer_[pos + 0] = color.r; - this->buffer_[pos + 1] = color.g; - this->buffer_[pos + 2] = color.b; - this->buffer_[pos + 3] = color.w; - break; - } - case ImageType::IMAGE_TYPE_RGB24: - default: { - if (this->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; - } } + break; + } + case ImageType::IMAGE_TYPE_RGB: { + this->map_chroma_key(color); this->buffer_[pos + 0] = color.r; this->buffer_[pos + 1] = color.g; this->buffer_[pos + 2] = color.b; + if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { + this->buffer_[pos + 3] = color.w; + } break; } } diff --git a/esphome/components/online_image/online_image.h b/esphome/components/online_image/online_image.h index 017402a088..e044b4f390 100644 --- a/esphome/components/online_image/online_image.h +++ b/esphome/components/online_image/online_image.h @@ -48,12 +48,13 @@ class OnlineImage : public PollingComponent, * @param buffer_size Size of the buffer used to download the image. */ OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, - uint32_t buffer_size); + image::Transparency transparency, 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; + void map_chroma_key(Color &color); /** Set the URL to download the image from. */ void set_url(const std::string &url) { diff --git a/esphome/components/online_image/png_image.h b/esphome/components/online_image/png_image.h index a928276dcc..d82ff93149 100644 --- a/esphome/components/online_image/png_image.h +++ b/esphome/components/online_image/png_image.h @@ -1,6 +1,7 @@ #pragma once #include "image_decoder.h" +#include "esphome/core/defines.h" #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #include diff --git a/esphome/components/psram/psram.cpp b/esphome/components/psram/psram.cpp index 68d8dfd697..d9a5bd101f 100644 --- a/esphome/components/psram/psram.cpp +++ b/esphome/components/psram/psram.cpp @@ -21,7 +21,14 @@ void PsramComponent::dump_config() { ESP_LOGCONFIG(TAG, " Available: %s", YESNO(available)); #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 1, 0) if (available) { - ESP_LOGCONFIG(TAG, " Size: %d KB", heap_caps_get_total_size(MALLOC_CAP_SPIRAM) / 1024); + const size_t psram_total_size_bytes = heap_caps_get_total_size(MALLOC_CAP_SPIRAM); + const float psram_total_size_kb = psram_total_size_bytes / 1024.0f; + + if (abs(std::round(psram_total_size_kb) - psram_total_size_kb) < 0.05f) { + ESP_LOGCONFIG(TAG, " Size: %.0f KB", psram_total_size_kb); + } else { + ESP_LOGCONFIG(TAG, " Size: %zu bytes", psram_total_size_bytes); + } } #endif } diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index cb3b19aa1a..fb9e8ff6e5 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -2425,28 +2425,21 @@ void WaveshareEPaper7P5InBV3BWR::init_display_() { this->command(0x01); // 1-0=11: internal power - this->data(0x07); - this->data(0x17); // VGH&VGL - this->data(0x3F); // VSH - this->data(0x26); // VSL - this->data(0x11); // VSHR + this->data(0x07); // VRS_EN=1, VS_EN=1, VG_EN=1 + this->data(0x17); // VGH&VGL ??? VCOM_SLEW=1 but this is fixed, VG_LVL[2:0]=111 => VGH=20V VGL=-20V, it could be 0x07 + this->data(0x3F); // VSH=15V? + this->data(0x26); // VSL=-9.4V? + this->data(0x11); // VSHR=5.8V? // VCOM DC Setting this->command(0x82); - this->data(0x24); // VCOM - - // Booster Setting - this->command(0x06); - this->data(0x27); - this->data(0x27); - this->data(0x2F); - this->data(0x17); + this->data(0x24); // VCOM=-1.9V // POWER ON this->command(0x04); - delay(100); // NOLINT this->wait_until_idle_(); + // COMMAND PANEL SETTING this->command(0x00); this->data(0x0F); // KW-3f KWR-2F BWROTP 0f BWOTP 1f @@ -2457,16 +2450,16 @@ void WaveshareEPaper7P5InBV3BWR::init_display_() { this->data(0x20); this->data(0x01); // gate 480 this->data(0xE0); - // COMMAND ...? - this->command(0x15); - this->data(0x00); + // COMMAND VCOM AND DATA INTERVAL SETTING this->command(0x50); this->data(0x20); this->data(0x00); + // COMMAND TCON SETTING this->command(0x60); this->data(0x22); + // Resolution setting this->command(0x65); this->data(0x00); diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 0467023039..ed0cb3db2c 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1415,6 +1415,30 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques request->send(200, "application/json", data.c_str()); return; } + + auto call = obj->make_call(); + if (request->hasParam("code")) { + call.set_code(request->getParam("code")->value().c_str()); + } + + if (match.method == "disarm") { + call.disarm(); + } else if (match.method == "arm_away") { + call.arm_away(); + } else if (match.method == "arm_home") { + call.arm_home(); + } else if (match.method == "arm_night") { + call.arm_night(); + } else if (match.method == "arm_vacation") { + call.arm_vacation(); + } else { + request->send(404); + return; + } + + this->schedule_([call]() mutable { call.perform(); }); + request->send(200); + return; } request->send(404); } @@ -1664,7 +1688,7 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { #endif #ifdef USE_ALARM_CONTROL_PANEL - if (request->method() == HTTP_GET && match.domain == "alarm_control_panel") + if ((request->method() == HTTP_GET || request->method() == HTTP_POST) && match.domain == "alarm_control_panel") return true; #endif diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index bc10bbd1e5..b7a77fcdc9 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -11,10 +11,19 @@ #ifdef USE_WIFI_WPA2_EAP #include #endif + +#ifdef USE_WIFI_AP +#include "dhcpserver/dhcpserver.h" +#endif // USE_WIFI_AP + #include "lwip/apps/sntp.h" #include "lwip/dns.h" #include "lwip/err.h" +#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING +#include "lwip/priv/tcpip_priv.h" +#endif + #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" @@ -286,11 +295,26 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { } if (!manual_ip.has_value()) { +// sntp_servermode_dhcp lwip/sntp.c (Required to lock TCPIP core functionality!) +// https://github.com/esphome/issues/issues/6591 +// https://github.com/espressif/arduino-esp32/issues/10526 +#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING + if (!sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) { + LOCK_TCPIP_CORE(); + } +#endif + // lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly, // the built-in SNTP client has a memory leak in certain situations. Disable this feature. // https://github.com/esphome/issues/issues/2299 sntp_servermode_dhcp(false); +#ifdef CONFIG_LWIP_TCPIP_CORE_LOCKING + if (sys_thread_tcpip(LWIP_CORE_LOCK_QUERY_HOLDER)) { + UNLOCK_TCPIP_CORE(); + } +#endif + // No manual IP is set; use DHCP client if (dhcp_status != ESP_NETIF_DHCP_STARTED) { err = esp_netif_dhcpc_start(s_sta_netif); @@ -638,7 +662,12 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { - auto status = WiFiClass::status(); +#if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0) + const auto status = WiFiClass::status(); +#else + const auto status = WiFi.status(); +#endif + if (status == WL_CONNECT_FAILED || status == WL_CONNECTION_LOST) { return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; } diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index dcf7da2f21..13179b90bb 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -2,6 +2,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/hal.h" #include "esphome/core/defines.h" #include "esphome/core/preferences.h" diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 2d2c88b844..439bb2ccb0 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -45,7 +45,9 @@ #endif #ifdef USE_ESP32 #include "esp32/rom/crc.h" - +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 2) +#include "esp_mac.h" +#endif #include "esp_efuse.h" #include "esp_efuse_table.h" #endif @@ -261,7 +263,7 @@ bool random_bytes(uint8_t *data, size_t len) { bool str_equals_case_insensitive(const std::string &a, const std::string &b) { return strcasecmp(a.c_str(), b.c_str()) == 0; } -#if ESP_IDF_VERSION_MAJOR >= 5 +#if __cplusplus >= 202002L bool str_startswith(const std::string &str, const std::string &start) { return str.starts_with(start); } bool str_endswith(const std::string &str, const std::string &end) { return str.ends_with(end); } #else diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index c823439fb3..82b0fe07f8 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -163,7 +163,7 @@ template T remap(U value, U min, U max, T min_out, T max return (value - min) * (max_out - min_out) / (max - min) + min_out; } -/// Calculate a CRC-8 checksum of \p data with size \p len. +/// Calculate a CRC-8 checksum of \p data with size \p len using the CRC-8-Dallas/Maxim polynomial. uint8_t crc8(const uint8_t *data, uint8_t len); /// Calculate a CRC-16 checksum of \p data with size \p len. diff --git a/esphome/core/log.h b/esphome/core/log.h index 86af534f98..99a68024c5 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -74,7 +74,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE #define esph_log_vv(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_VERY_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) + ::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_VERY_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_VERY_VERBOSE #else @@ -83,7 +83,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE #define esph_log_v(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) + ::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_VERBOSE #else @@ -92,9 +92,9 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG #define esph_log_d(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) + ::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define esph_log_config(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_CONFIG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) + ::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_CONFIG, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_DEBUG #define ESPHOME_LOG_HAS_CONFIG @@ -105,7 +105,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO #define esph_log_i(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) + ::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_INFO #else @@ -114,7 +114,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN #define esph_log_w(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) + ::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_WARN #else @@ -123,7 +123,7 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR #define esph_log_e(tag, format, ...) \ - esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) + ::esphome::esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, __LINE__, ESPHOME_LOG_FORMAT(format), ##__VA_ARGS__) #define ESPHOME_LOG_HAS_ERROR #else diff --git a/script/ci-custom.py b/script/ci-custom.py index 81e3da311a..d5d3ab88c8 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -58,7 +58,19 @@ file_types = ( ) cpp_include = ("*.h", "*.c", "*.cpp", "*.tcc") py_include = ("*.py",) -ignore_types = (".ico", ".png", ".woff", ".woff2", "", ".ttf", ".otf", ".pcf") +ignore_types = ( + ".ico", + ".png", + ".woff", + ".woff2", + "", + ".ttf", + ".otf", + ".pcf", + ".apng", + ".gif", + ".webp", +) LINT_FILE_CHECKS = [] LINT_CONTENT_CHECKS = [] @@ -669,8 +681,7 @@ def main(): ) args = parser.parse_args() - global EXECUTABLE_BIT - EXECUTABLE_BIT = git_ls_files() + EXECUTABLE_BIT.update(git_ls_files()) files = list(EXECUTABLE_BIT.keys()) # Match against re file_name_re = re.compile("|".join(args.files)) diff --git a/tests/components/animation/.gitattributes b/tests/components/animation/.gitattributes new file mode 100644 index 0000000000..ff9fc6f1f1 --- /dev/null +++ b/tests/components/animation/.gitattributes @@ -0,0 +1,4 @@ +*.apng -text +*.webp -text +*.gif -text + diff --git a/tests/components/animation/anim.apng b/tests/components/animation/anim.apng new file mode 100644 index 0000000000..927af5eb05 Binary files /dev/null and b/tests/components/animation/anim.apng differ diff --git a/tests/components/animation/anim.gif b/tests/components/animation/anim.gif new file mode 100644 index 0000000000..9932e77448 Binary files /dev/null and b/tests/components/animation/anim.gif differ diff --git a/tests/components/animation/anim.webp b/tests/components/animation/anim.webp new file mode 100644 index 0000000000..8958377afa Binary files /dev/null and b/tests/components/animation/anim.webp differ diff --git a/tests/components/animation/common.yaml b/tests/components/animation/common.yaml new file mode 100644 index 0000000000..c0f04fe768 --- /dev/null +++ b/tests/components/animation/common.yaml @@ -0,0 +1,23 @@ +animation: + - id: rgb565_animation + file: $component_dir/anim.gif + type: RGB565 + use_transparency: opaque + resize: 50x50 + - id: rgb_animation + file: $component_dir/anim.apng + type: RGB + use_transparency: chroma_key + resize: 50x50 + - id: grayscale_animation + file: $component_dir/anim.apng + type: grayscale + +display: + lambda: |- + id(rgb565_animation).next_frame(); + id(rgb_animation1).next_frame(); + id(grayscale_animation2).next_frame(); + it.image(0, 0, rgb565_animation); + it.image(120, 0, rgb_animation1); + it.image(240, 0, grayscale_animation2); diff --git a/tests/components/animation/test.esp32-ard.yaml b/tests/components/animation/test.esp32-ard.yaml index af6cd202dd..5d330900df 100644 --- a/tests/components/animation/test.esp32-ard.yaml +++ b/tests/components/animation/test.esp32-ard.yaml @@ -13,12 +13,6 @@ display: reset_pin: 21 invert_colors: false -# Purposely test that `animation:` does auto-load `image:` -# Keep the `image:` undefined. -# image: +packages: + animation: !include common.yaml -animation: - - id: rgb565_animation - file: ../../pnglogo.png - type: RGB565 - use_transparency: false diff --git a/tests/components/animation/test.esp32-c3-ard.yaml b/tests/components/animation/test.esp32-c3-ard.yaml index 10e8ccb47e..18aa2a5b06 100644 --- a/tests/components/animation/test.esp32-c3-ard.yaml +++ b/tests/components/animation/test.esp32-c3-ard.yaml @@ -13,12 +13,5 @@ display: reset_pin: 10 invert_colors: false -# Purposely test that `animation:` does auto-load `image:` -# Keep the `image:` undefined. -# image: - -animation: - - id: rgb565_animation - file: ../../pnglogo.png - type: RGB565 - use_transparency: false +packages: + animation: !include common.yaml diff --git a/tests/components/animation/test.esp32-c3-idf.yaml b/tests/components/animation/test.esp32-c3-idf.yaml index 10e8ccb47e..18aa2a5b06 100644 --- a/tests/components/animation/test.esp32-c3-idf.yaml +++ b/tests/components/animation/test.esp32-c3-idf.yaml @@ -13,12 +13,5 @@ display: reset_pin: 10 invert_colors: false -# Purposely test that `animation:` does auto-load `image:` -# Keep the `image:` undefined. -# image: - -animation: - - id: rgb565_animation - file: ../../pnglogo.png - type: RGB565 - use_transparency: false +packages: + animation: !include common.yaml diff --git a/tests/components/animation/test.esp32-idf.yaml b/tests/components/animation/test.esp32-idf.yaml index af6cd202dd..7d9fe45bff 100644 --- a/tests/components/animation/test.esp32-idf.yaml +++ b/tests/components/animation/test.esp32-idf.yaml @@ -13,12 +13,5 @@ display: reset_pin: 21 invert_colors: false -# Purposely test that `animation:` does auto-load `image:` -# Keep the `image:` undefined. -# image: - -animation: - - id: rgb565_animation - file: ../../pnglogo.png - type: RGB565 - use_transparency: false +packages: + animation: !include common.yaml diff --git a/tests/components/animation/test.esp8266-ard.yaml b/tests/components/animation/test.esp8266-ard.yaml index ced4996f25..9548c7fbeb 100644 --- a/tests/components/animation/test.esp8266-ard.yaml +++ b/tests/components/animation/test.esp8266-ard.yaml @@ -13,12 +13,5 @@ display: reset_pin: 16 invert_colors: false -# Purposely test that `animation:` does auto-load `image:` -# Keep the `image:` undefined. -# image: - -animation: - - id: rgb565_animation - file: ../../pnglogo.png - type: RGB565 - use_transparency: false +packages: + animation: !include common.yaml diff --git a/tests/components/animation/test.rp2040-ard.yaml b/tests/components/animation/test.rp2040-ard.yaml index 0e33959cc6..efb3f2907c 100644 --- a/tests/components/animation/test.rp2040-ard.yaml +++ b/tests/components/animation/test.rp2040-ard.yaml @@ -13,12 +13,5 @@ display: reset_pin: 22 invert_colors: false -# Purposely test that `animation:` does auto-load `image:` -# Keep the `image:` undefined. -# image: - -animation: - - id: rgb565_animation - file: ../../pnglogo.png - type: RGB565 - use_transparency: false +packages: + animation: !include common.yaml diff --git a/tests/components/debug/test.esp32-s2-ard.yaml b/tests/components/debug/test.esp32-s2-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.esp32-s2-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/debug/test.esp32-s2-idf.yaml b/tests/components/debug/test.esp32-s2-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.esp32-s2-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/debug/test.esp32-s3-ard.yaml b/tests/components/debug/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.esp32-s3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/debug/test.esp32-s3-idf.yaml b/tests/components/debug/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/debug/test.esp32-s3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/image/common.yaml b/tests/components/image/common.yaml index 313da6bc0b..fdb0493d2a 100644 --- a/tests/components/image/common.yaml +++ b/tests/components/image/common.yaml @@ -5,32 +5,65 @@ image: dither: FloydSteinberg - id: transparent_transparent_image file: ../../pnglogo.png - type: TRANSPARENT_BINARY + type: BINARY + use_transparency: chroma_key + - id: rgba_image file: ../../pnglogo.png - type: RGBA + type: RGB + use_transparency: alpha_channel resize: 50x50 - id: rgb24_image file: ../../pnglogo.png - type: RGB24 - use_transparency: yes + type: RGB + use_transparency: chroma_key + - id: rgb_image + file: ../../pnglogo.png + type: RGB + use_transparency: opaque + - id: rgb565_image file: ../../pnglogo.png type: RGB565 - use_transparency: no + use_transparency: opaque + - id: rgb565_ck_image + file: ../../pnglogo.png + type: RGB565 + use_transparency: chroma_key + - id: rgb565_alpha_image + file: ../../pnglogo.png + type: RGB565 + use_transparency: alpha_channel + + - id: grayscale_alpha_image + file: ../../pnglogo.png + type: grayscale + use_transparency: alpha_channel + resize: 50x50 + - id: grayscale_ck_image + file: ../../pnglogo.png + type: grayscale + use_transparency: chroma_key + - id: grayscale_image + file: ../../pnglogo.png + type: grayscale + use_transparency: opaque + - id: web_svg_image file: https://raw.githubusercontent.com/esphome/esphome-docs/a62d7ab193c1a464ed791670170c7d518189109b/images/logo.svg resize: 256x48 - type: TRANSPARENT_BINARY + type: BINARY + use_transparency: chroma_key - id: web_tiff_image file: https://upload.wikimedia.org/wikipedia/commons/b/b6/SIPI_Jelly_Beans_4.1.07.tiff - type: RGB24 + type: RGB resize: 48x48 - id: web_redirect_image file: https://avatars.githubusercontent.com/u/3060199?s=48&v=4 - type: RGB24 + type: RGB resize: 48x48 - id: mdi_alert + type: BINARY file: mdi:alert-circle-outline resize: 50x50 - id: another_alert_icon diff --git a/tests/components/image/test.host.yaml b/tests/components/image/test.host.yaml index 29509db66c..61ecd5e374 100644 --- a/tests/components/image/test.host.yaml +++ b/tests/components/image/test.host.yaml @@ -5,4 +5,44 @@ display: width: 480 height: 480 -<<: !include common.yaml +image: + binary: + - id: binary_image + file: ../../pnglogo.png + dither: FloydSteinberg + - id: transparent_transparent_image + file: ../../pnglogo.png + use_transparency: chroma_key + rgb: + alpha_channel: + - id: rgba_image + file: ../../pnglogo.png + resize: 50x50 + chroma_key: + - id: rgb24_image + file: ../../pnglogo.png + type: RGB + opaque: + - id: rgb_image + file: ../../pnglogo.png + rgb565: + - id: rgb565_image + file: ../../pnglogo.png + use_transparency: opaque + - id: rgb565_ck_image + file: ../../pnglogo.png + use_transparency: chroma_key + - id: rgb565_alpha_image + file: ../../pnglogo.png + use_transparency: alpha_channel + grayscale: + - id: grayscale_alpha_image + file: ../../pnglogo.png + use_transparency: alpha_channel + resize: 50x50 + - id: grayscale_ck_image + file: ../../pnglogo.png + use_transparency: chroma_key + - id: grayscale_image + file: ../../pnglogo.png + use_transparency: opaque diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 234fd78678..7c59cfa171 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -795,6 +795,19 @@ lvgl: color: 0xA0A0A0 r_mod: -20 opa: 0% + - id: page3 + widgets: + - keyboard: + id: lv_keyboard + align: bottom_mid + on_value: + then: + - logger.log: + format: "keyboard value %s" + args: [text.c_str()] + - keyboard: + id: lv_keyboard1 + mode: special font: - file: "gfonts://Roboto" @@ -805,10 +818,13 @@ image: - id: cat_image resize: 256x48 file: $component_dir/logo-text.svg + type: RGB565 + use_transparency: alpha_channel - id: dog_image file: $component_dir/logo-text.svg resize: 256x48 - type: TRANSPARENT_BINARY + type: BINARY + use_transparency: chroma_key color: - id: light_blue diff --git a/tests/components/nextion/common.yaml b/tests/components/nextion/common.yaml index 73fc8484c0..589afcfefb 100644 --- a/tests/components/nextion/common.yaml +++ b/tests/components/nextion/common.yaml @@ -2,6 +2,8 @@ esphome: on_boot: - lambda: 'ESP_LOGD("display","is_connected(): %s", YESNO(id(main_lcd).is_connected()));' + - display.nextion.set_brightness: 80% + # Binary sensor publish action tests - binary_sensor.nextion.publish: id: r0_sensor diff --git a/tests/components/online_image/common.yaml b/tests/components/online_image/common.yaml index 5c6feb4c81..81f43e9fdc 100644 --- a/tests/components/online_image/common.yaml +++ b/tests/components/online_image/common.yaml @@ -13,33 +13,32 @@ online_image: resize: 50x50 - id: online_binary_transparent_image url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png - type: TRANSPARENT_BINARY + type: BINARY + use_transparency: chroma_key format: png - id: online_rgba_image url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png format: PNG - type: RGBA + type: RGB + use_transparency: alpha_channel - id: online_rgb24_image url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png format: PNG - type: RGB24 - use_transparency: true + type: RGB + use_transparency: chroma_key # 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 - - online_image.set_url: - id: online_rgba_image - url: !lambda |- - return "http://www.example.org/example.png"; - - online_image.set_url: - id: online_rgba_image - url: !lambda |- - return str_sprintf("http://homeassistant.local:8123"); - +esphome: + on_boot: + then: + - online_image.set_url: + id: online_rgba_image + url: http://www.example.org/example.png + - online_image.set_url: + id: online_rgba_image + url: !lambda |- + return "http://www.example.org/example.png"; + - online_image.set_url: + id: online_rgba_image + url: !lambda |- + return str_sprintf("http://homeassistant.local:8123"); diff --git a/tests/components/psram/test.esp32-s2-ard.yaml b/tests/components/psram/test.esp32-s2-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/psram/test.esp32-s2-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/psram/test.esp32-s2-idf.yaml b/tests/components/psram/test.esp32-s2-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/psram/test.esp32-s2-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/psram/test.esp32-s3-ard.yaml b/tests/components/psram/test.esp32-s3-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/psram/test.esp32-s3-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/psram/test.esp32-s3-idf.yaml b/tests/components/psram/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/psram/test.esp32-s3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml