From 731fb1d172f83d15a3f03152477dbd5f482cd19f Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Sun, 12 Jan 2025 23:15:39 +0100 Subject: [PATCH 01/30] [spi] relay on KEY_TARGET_PLATFORM as the other platforms does (#8066) --- esphome/components/spi/__init__.py | 6 +----- script/build_codeowners.py | 4 ++-- script/build_language_schema.py | 4 ++-- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 52afbf365e..3e6d680b89 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -97,11 +97,7 @@ RP_SPI_PINSETS = [ def get_target_platform(): - return ( - CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] - if KEY_TARGET_PLATFORM in CORE.data[KEY_CORE] - else "" - ) + return CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] def get_target_variant(): diff --git a/script/build_codeowners.py b/script/build_codeowners.py index db34ad7702..523fe8ac7f 100755 --- a/script/build_codeowners.py +++ b/script/build_codeowners.py @@ -5,7 +5,7 @@ from pathlib import Path import sys from esphome.config import get_component, get_platform -from esphome.const import KEY_CORE, KEY_TARGET_FRAMEWORK +from esphome.const import KEY_CORE, KEY_TARGET_FRAMEWORK, KEY_TARGET_PLATFORM from esphome.core import CORE from esphome.helpers import write_file_if_changed @@ -39,7 +39,7 @@ parts = [BASE] # Fake some directory so that get_component works CORE.config_path = str(root) -CORE.data[KEY_CORE] = {KEY_TARGET_FRAMEWORK: None} +CORE.data[KEY_CORE] = {KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: None} codeowners = defaultdict(list) diff --git a/script/build_language_schema.py b/script/build_language_schema.py index 2023dc0402..07093e179a 100644 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -85,12 +85,12 @@ def load_components(): # pylint: disable=wrong-import-position -from esphome.const import CONF_TYPE, KEY_CORE +from esphome.const import CONF_TYPE, KEY_CORE, KEY_TARGET_PLATFORM from esphome.core import CORE # pylint: enable=wrong-import-position -CORE.data[KEY_CORE] = {} +CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: None} load_components() # Import esphome after loading components (so schema is tracked) From 7c3942269200e43f81391cc0e63d636ab63b6761 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:54:44 +1300 Subject: [PATCH 02/30] Bump actions/upload-artifact from 4.5.0 to 4.6.0 (#8058) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0304cd2304..e791f666d4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -141,7 +141,7 @@ jobs: echo name=$(cat /tmp/platform) >> $GITHUB_OUTPUT - name: Upload digests - uses: actions/upload-artifact@v4.5.0 + uses: actions/upload-artifact@v4.6.0 with: name: digests-${{ steps.sanitize.outputs.name }} path: /tmp/digests From 571935fb3b97cd031390bb5b7ac4b4da5f62575b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:55:00 +1300 Subject: [PATCH 03/30] Bump peter-evans/create-pull-request from 7.0.5 to 7.0.6 (#8024) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/sync-device-classes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 7a46d596a1..9160ab4a1b 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -36,7 +36,7 @@ jobs: python ./script/sync-device_class.py - name: Commit changes - uses: peter-evans/create-pull-request@v7.0.5 + uses: peter-evans/create-pull-request@v7.0.6 with: commit-message: "Synchronise Device Classes from Home Assistant" committer: esphomebot From f25f3334d11d5a508aad6cdf5056b4cc33213fe7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:55:37 +1300 Subject: [PATCH 04/30] Bump docker/setup-qemu-action from 3.2.0 to 3.3.0 in the docker-actions group (#8052) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-docker.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index cf7b08f3d4..b994cfaf17 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -48,7 +48,7 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.8.0 - name: Set up QEMU - uses: docker/setup-qemu-action@v3.2.0 + uses: docker/setup-qemu-action@v3.3.0 - name: Set TAG run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e791f666d4..962bc66e94 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,7 +93,7 @@ jobs: uses: docker/setup-buildx-action@v3.8.0 - name: Set up QEMU if: matrix.platform != 'linux/amd64' - uses: docker/setup-qemu-action@v3.2.0 + uses: docker/setup-qemu-action@v3.3.0 - name: Log in to docker hub uses: docker/login-action@v3.3.0 From 739edce268f83657ce5ef7d823008260a7c424db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 12:55:53 +1300 Subject: [PATCH 05/30] Bump docker/build-push-action from 6.10.0 to 6.11.0 in /.github/actions/build-image (#8053) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-image/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index cc9894a657..e6b6da4177 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -46,7 +46,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@v6.10.0 + uses: docker/build-push-action@v6.11.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false @@ -72,7 +72,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@v6.10.0 + uses: docker/build-push-action@v6.11.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false From 4409471cd1c20a43a45bcfddead959c9d58c3419 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 13 Jan 2025 14:32:10 +1300 Subject: [PATCH 06/30] Bump python3-setuptools to 66.1.1-1+deb12u1 (#8074) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 \ From fb87a1c0bc565447785f90367f28bb0e74584f49 Mon Sep 17 00:00:00 2001 From: Mischa Siekmann <45062894+gnumpi@users.noreply.github.com> Date: Mon, 13 Jan 2025 02:42:03 +0100 Subject: [PATCH 07/30] Allow CONF_RMT_CHANNEL parameter for IDF 4.X (#8035) --- .../components/esp32_rmt_led_strip/light.py | 54 +++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) 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, From aac38419915486fff121651bf6a4d063f6f4b312 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 12 Jan 2025 20:45:35 -0500 Subject: [PATCH 08/30] [esp32] Fix arch_get_cpu_freq_hz (#8047) Co-authored-by: Jonathan Swoboda --- esphome/components/esp32/core.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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) From dd3ffc7f29d8436171af8ce070ab68f7e226d570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=BBbik?= Date: Mon, 13 Jan 2025 03:55:30 +0100 Subject: [PATCH 09/30] Fix Waveshare 7in5bv3bwr image quality in BWR mode (#8043) Co-authored-by: zbikmarc --- .../waveshare_epaper/waveshare_epaper.cpp | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) 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); From 92a8ebe1f8c15db118dac1269eb309b83f0ed9ee Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 13 Jan 2025 13:56:42 +1100 Subject: [PATCH 10/30] [json] use correct formatting (#8039) --- esphome/components/json/json_util.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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; From aa87c607173d9ef65e047a5e6b73d7ab4fac649d Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 12 Jan 2025 21:12:54 -0600 Subject: [PATCH 11/30] [nextion] Brightness control tweaks (#8027) --- esphome/components/nextion/automation.h | 17 +++++++++ esphome/components/nextion/display.py | 51 ++++++++++++++++++------- esphome/components/nextion/nextion.cpp | 4 +- esphome/components/nextion/nextion.h | 2 +- tests/components/nextion/common.yaml | 2 + 5 files changed, 61 insertions(+), 15 deletions(-) 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/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 From f1c0570e3b51062f12c8ebf0fcefeb6fc81e22b7 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 13 Jan 2025 14:21:42 +1100 Subject: [PATCH 12/30] [image] Transparency changes; code refactor (#7908) --- CODEOWNERS | 2 +- esphome/components/animation/__init__.py | 284 +------ esphome/components/animation/animation.cpp | 4 +- esphome/components/animation/animation.h | 3 +- esphome/components/image/__init__.py | 691 +++++++++++------- esphome/components/image/image.cpp | 143 ++-- esphome/components/image/image.h | 39 +- esphome/components/online_image/__init__.py | 118 +-- .../components/online_image/image_decoder.h | 11 +- .../components/online_image/online_image.cpp | 92 +-- .../components/online_image/online_image.h | 3 +- esphome/components/online_image/png_image.h | 1 + script/ci-custom.py | 17 +- tests/components/animation/.gitattributes | 4 + tests/components/animation/anim.apng | Bin 0 -> 12626 bytes tests/components/animation/anim.gif | Bin 0 -> 9735 bytes tests/components/animation/anim.webp | Bin 0 -> 8244 bytes tests/components/animation/common.yaml | 23 + .../components/animation/test.esp32-ard.yaml | 10 +- .../animation/test.esp32-c3-ard.yaml | 11 +- .../animation/test.esp32-c3-idf.yaml | 11 +- .../components/animation/test.esp32-idf.yaml | 11 +- .../animation/test.esp8266-ard.yaml | 11 +- .../components/animation/test.rp2040-ard.yaml | 11 +- tests/components/image/common.yaml | 49 +- tests/components/image/test.host.yaml | 42 +- tests/components/online_image/common.yaml | 41 +- 27 files changed, 845 insertions(+), 787 deletions(-) create mode 100644 tests/components/animation/.gitattributes create mode 100644 tests/components/animation/anim.apng create mode 100644 tests/components/animation/anim.gif create mode 100644 tests/components/animation/anim.webp create mode 100644 tests/components/animation/common.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 404ad35efc..088e350f5d 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/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/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/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/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 0000000000000000000000000000000000000000..927af5eb05a94ea8b1cdab493d2bfd8feffb7eac GIT binary patch literal 12626 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

m9s*zMUB&9i)Vj-aXg1Rr|O2;S{r^I>B3s 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 Date: Sun, 12 Jan 2025 19:23:35 -0800 Subject: [PATCH 13/30] Added VERY_VERBOSE dfplayer printing (#8026) --- esphome/components/dfplayer/dfplayer.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) 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; From d8c943972b2a3e68ec126922cd8601fba290b6c0 Mon Sep 17 00:00:00 2001 From: Djordje Mandic <6750655+DjordjeMandic@users.noreply.github.com> Date: Mon, 13 Jan 2025 05:05:53 +0100 Subject: [PATCH 14/30] [core] fix comment for crc8 function in helpers.h (#8016) --- esphome/core/helpers.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From aa1879082c67dd893591d854ea8e057e2ad99fa6 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Mon, 13 Jan 2025 05:06:44 +0100 Subject: [PATCH 15/30] [debug] Add framework type to debug info (#8013) --- esphome/components/debug/debug_esp32.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index 5f7b9cdbb0..b0631f2b61 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -276,6 +276,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(); From fef50afef8bbf333de3d8f0fc3754d231f7bf165 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Mon, 13 Jan 2025 05:08:20 +0100 Subject: [PATCH 16/30] [debug] Add ESP32 partition table logging to `dump_config` (#8012) --- esphome/components/debug/debug_component.cpp | 4 ++++ esphome/components/debug/debug_component.h | 14 ++++++++++++++ esphome/components/debug/debug_esp32.cpp | 14 ++++++++++++++ tests/components/debug/test.esp32-s2-ard.yaml | 1 + tests/components/debug/test.esp32-s2-idf.yaml | 1 + tests/components/debug/test.esp32-s3-ard.yaml | 1 + tests/components/debug/test.esp32-s3-idf.yaml | 1 + 7 files changed, 36 insertions(+) create mode 100644 tests/components/debug/test.esp32-s2-ard.yaml create mode 100644 tests/components/debug/test.esp32-s2-idf.yaml create mode 100644 tests/components/debug/test.esp32-s3-ard.yaml create mode 100644 tests/components/debug/test.esp32-s3-idf.yaml 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 b0631f2b61..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()) { 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 From 528d3672b499650185427297cfb7399fedd8fcf4 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Mon, 13 Jan 2025 05:11:48 +0100 Subject: [PATCH 17/30] [psram] Improve total PSRAM display in logs by using rounded KB values (#8008) Co-authored-by: Djordje Mandic <6750655+DjordjeMandic@users.noreply.github.com> --- esphome/components/psram/psram.cpp | 9 ++++++++- tests/components/psram/test.esp32-s2-ard.yaml | 1 + tests/components/psram/test.esp32-s2-idf.yaml | 1 + tests/components/psram/test.esp32-s3-ard.yaml | 1 + tests/components/psram/test.esp32-s3-idf.yaml | 1 + 5 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 tests/components/psram/test.esp32-s2-ard.yaml create mode 100644 tests/components/psram/test.esp32-s2-idf.yaml create mode 100644 tests/components/psram/test.esp32-s3-ard.yaml create mode 100644 tests/components/psram/test.esp32-s3-idf.yaml 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/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 From 8fbd512952e516b5b4a64d9592d81cbeb9349c38 Mon Sep 17 00:00:00 2001 From: Douglas <31328123+dougiteixeira@users.noreply.github.com> Date: Mon, 13 Jan 2025 01:16:43 -0300 Subject: [PATCH 18/30] Use ESPHome logo on readme page according to theme (light/dark) (#7992) --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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/ From 3fa67fad32c9515b1bf876bb7aef02b6d311c1b3 Mon Sep 17 00:00:00 2001 From: Ryan Henderson Date: Sun, 12 Jan 2025 20:17:28 -0800 Subject: [PATCH 19/30] Fix compile errors with pioarduino/platform-espressif32: wifi_component_esp32_arduino.cpp (#7998) --- .../components/wifi/wifi_component_esp32_arduino.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index bc10bbd1e5..76d0b7d96c 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -11,6 +11,11 @@ #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" @@ -638,7 +643,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; } From df50e57409d80e8e43c9d0366ad4bbbdd42c07e1 Mon Sep 17 00:00:00 2001 From: Ryan Henderson Date: Sun, 12 Jan 2025 20:18:20 -0800 Subject: [PATCH 20/30] Include esp_mac.h and C++20 str_startswith/str_ends (#7999) --- esphome/core/helpers.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 From 13909b7994de2c25a62714bfc44c20b4fb845958 Mon Sep 17 00:00:00 2001 From: Ryan Henderson Date: Sun, 12 Jan 2025 20:26:23 -0800 Subject: [PATCH 21/30] [esp32_wifi] Enhance WiFi component with TCPIP core locking. (#7997) --- .../wifi/wifi_component_esp32_arduino.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index 76d0b7d96c..b7a77fcdc9 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -20,6 +20,10 @@ #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" @@ -291,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); From 9874d17613147fd617841d7eae66505a5c024b56 Mon Sep 17 00:00:00 2001 From: NP v/d Spek Date: Mon, 13 Jan 2025 05:29:38 +0100 Subject: [PATCH 22/30] add missing include in base_automation.h (#8001) --- esphome/core/base_automation.h | 1 + 1 file changed, 1 insertion(+) 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" From 30bb806f26853e00c053f1fc69751a61f105b8ee Mon Sep 17 00:00:00 2001 From: Piotr Szulc Date: Mon, 13 Jan 2025 05:31:01 +0100 Subject: [PATCH 23/30] Fixed libretiny preference wrongly detecting change in the data to store (#7990) --- esphome/components/libretiny/preferences.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) { From b4a2b50ee092abd0eabbbce6db0c8da1d825464e Mon Sep 17 00:00:00 2001 From: Dusan Cervenka Date: Mon, 13 Jan 2025 05:34:07 +0100 Subject: [PATCH 24/30] Fixed topic when mac is used (#7988) --- esphome/components/mqtt/__init__.py | 2 +- esphome/components/mqtt/mqtt_client.cpp | 8 +++++++- esphome/components/mqtt/mqtt_client.h | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) 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; From f319472066a45c11f4d71e96b39a201f07e7f0dd Mon Sep 17 00:00:00 2001 From: Nate Clark Date: Sun, 12 Jan 2025 23:35:29 -0500 Subject: [PATCH 25/30] web_server: Adds REST API POST endpoints to arm and disarm (#7985) --- esphome/components/web_server/web_server.cpp | 26 +++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) 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 From 6262fb8fcf9538d161d94512a857755c839eb48d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 14 Jan 2025 08:32:54 +1100 Subject: [PATCH 26/30] [lvgl] fix tests (#8075) --- tests/components/lvgl/lvgl-package.yaml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) 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 From bdb1094b477baad40cfcf9d9bc86665c14069a55 Mon Sep 17 00:00:00 2001 From: Stefan Rado <628587+kroimon@users.noreply.github.com> Date: Tue, 14 Jan 2025 04:20:52 +0100 Subject: [PATCH 27/30] Allow external libraries to use ESP_LOGx macros (#8078) --- esphome/components/lvgl/lvgl_esphome.cpp | 20 +++++--------------- esphome/core/log.h | 14 +++++++------- 2 files changed, 12 insertions(+), 22 deletions(-) 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/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 From fc2b15e307833359f079aec46ceffd30b9398abe Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 15 Jan 2025 09:27:47 +1100 Subject: [PATCH 28/30] [uptime] Add text_sensor (#8028) --- .../uptime/{sensor.py => sensor/__init__.py} | 6 +-- .../{ => sensor}/uptime_seconds_sensor.cpp | 0 .../{ => sensor}/uptime_seconds_sensor.h | 0 .../{ => sensor}/uptime_timestamp_sensor.cpp | 0 .../{ => sensor}/uptime_timestamp_sensor.h | 0 .../components/uptime/text_sensor/__init__.py | 19 ++++++++ .../uptime/text_sensor/uptime_text_sensor.cpp | 46 +++++++++++++++++++ .../uptime/text_sensor/uptime_text_sensor.h | 25 ++++++++++ tests/components/uptime/common.yaml | 4 ++ 9 files changed, 97 insertions(+), 3 deletions(-) rename esphome/components/uptime/{sensor.py => sensor/__init__.py} (100%) rename esphome/components/uptime/{ => sensor}/uptime_seconds_sensor.cpp (100%) rename esphome/components/uptime/{ => sensor}/uptime_seconds_sensor.h (100%) rename esphome/components/uptime/{ => sensor}/uptime_timestamp_sensor.cpp (100%) rename esphome/components/uptime/{ => sensor}/uptime_timestamp_sensor.h (100%) create mode 100644 esphome/components/uptime/text_sensor/__init__.py create mode 100644 esphome/components/uptime/text_sensor/uptime_text_sensor.cpp create mode 100644 esphome/components/uptime/text_sensor/uptime_text_sensor.h diff --git a/esphome/components/uptime/sensor.py b/esphome/components/uptime/sensor/__init__.py similarity index 100% rename from esphome/components/uptime/sensor.py rename to esphome/components/uptime/sensor/__init__.py index 30220751b6..e2a7aee1a2 100644 --- a/esphome/components/uptime/sensor.py +++ b/esphome/components/uptime/sensor/__init__.py @@ -1,14 +1,14 @@ import esphome.codegen as cg -import esphome.config_validation as cv from esphome.components import sensor, time +import esphome.config_validation as cv from esphome.const import ( CONF_TIME_ID, + DEVICE_CLASS_DURATION, DEVICE_CLASS_TIMESTAMP, ENTITY_CATEGORY_DIAGNOSTIC, + ICON_TIMER, STATE_CLASS_TOTAL_INCREASING, UNIT_SECOND, - ICON_TIMER, - DEVICE_CLASS_DURATION, ) uptime_ns = cg.esphome_ns.namespace("uptime") diff --git a/esphome/components/uptime/uptime_seconds_sensor.cpp b/esphome/components/uptime/sensor/uptime_seconds_sensor.cpp similarity index 100% rename from esphome/components/uptime/uptime_seconds_sensor.cpp rename to esphome/components/uptime/sensor/uptime_seconds_sensor.cpp diff --git a/esphome/components/uptime/uptime_seconds_sensor.h b/esphome/components/uptime/sensor/uptime_seconds_sensor.h similarity index 100% rename from esphome/components/uptime/uptime_seconds_sensor.h rename to esphome/components/uptime/sensor/uptime_seconds_sensor.h diff --git a/esphome/components/uptime/uptime_timestamp_sensor.cpp b/esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp similarity index 100% rename from esphome/components/uptime/uptime_timestamp_sensor.cpp rename to esphome/components/uptime/sensor/uptime_timestamp_sensor.cpp diff --git a/esphome/components/uptime/uptime_timestamp_sensor.h b/esphome/components/uptime/sensor/uptime_timestamp_sensor.h similarity index 100% rename from esphome/components/uptime/uptime_timestamp_sensor.h rename to esphome/components/uptime/sensor/uptime_timestamp_sensor.h diff --git a/esphome/components/uptime/text_sensor/__init__.py b/esphome/components/uptime/text_sensor/__init__.py new file mode 100644 index 0000000000..996d983e71 --- /dev/null +++ b/esphome/components/uptime/text_sensor/__init__.py @@ -0,0 +1,19 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import ENTITY_CATEGORY_DIAGNOSTIC, ICON_TIMER + +uptime_ns = cg.esphome_ns.namespace("uptime") +UptimeTextSensor = uptime_ns.class_( + "UptimeTextSensor", text_sensor.TextSensor, cg.PollingComponent +) +CONFIG_SCHEMA = text_sensor.text_sensor_schema( + UptimeTextSensor, + icon=ICON_TIMER, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, +).extend(cv.polling_component_schema("60s")) + + +async def to_code(config): + var = await text_sensor.new_text_sensor(config) + await cg.register_component(var, config) diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp new file mode 100644 index 0000000000..0fa5e199f3 --- /dev/null +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp @@ -0,0 +1,46 @@ +#include "uptime_text_sensor.h" + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace uptime { + +static const char *const TAG = "uptime.sensor"; + +void UptimeTextSensor::setup() { this->last_ms_ = millis(); } + +void UptimeTextSensor::update() { + const auto now = millis(); + // get whole seconds since last update. Note that even if the millis count has overflowed between updates, + // the difference will still be correct due to the way twos-complement arithmetic works. + const uint32_t delta = (now - this->last_ms_) / 1000; + if (delta == 0) + return; + // set last_ms_ to the last second boundary + this->last_ms_ = now - (now % 1000); + this->uptime_ += delta; + auto uptime = this->uptime_; + unsigned days = uptime / (24 * 3600); + unsigned seconds = uptime % (24 * 3600); + unsigned hours = seconds / 3600; + seconds %= 3600; + unsigned minutes = seconds / 60; + seconds %= 60; + if (days != 0) { + this->publish_state(str_sprintf("%dd%dh%dm%ds", days, hours, minutes, seconds)); + } else if (hours != 0) { + this->publish_state(str_sprintf("%dh%dm%ds", hours, minutes, seconds)); + } else if (minutes != 0) { + this->publish_state(str_sprintf("%dm%ds", minutes, seconds)); + } else { + this->publish_state(str_sprintf("%ds", seconds)); + } +} + +float UptimeTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } +void UptimeTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Uptime Text Sensor", this); } + +} // namespace uptime +} // namespace esphome diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.h b/esphome/components/uptime/text_sensor/uptime_text_sensor.h new file mode 100644 index 0000000000..4baf1039b6 --- /dev/null +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/defines.h" + +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace uptime { + +class UptimeTextSensor : public text_sensor::TextSensor, public PollingComponent { + public: + void update() override; + void dump_config() override; + void setup() override; + + float get_setup_priority() const override; + + protected: + uint64_t uptime_{0}; + uint64_t last_ms_{0}; +}; + +} // namespace uptime +} // namespace esphome diff --git a/tests/components/uptime/common.yaml b/tests/components/uptime/common.yaml index f63f80b050..d78ef8eca9 100644 --- a/tests/components/uptime/common.yaml +++ b/tests/components/uptime/common.yaml @@ -13,3 +13,7 @@ sensor: - platform: uptime name: Uptime Sensor Timestamp type: timestamp + +text_sensor: + - platform: uptime + name: Uptime Text From c3412df169a0fc742b5bf854245d6a07adef836d Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 15 Jan 2025 09:29:27 +1100 Subject: [PATCH 29/30] [image] Fix mdi images (#8082) --- esphome/components/image/__init__.py | 39 ++++++++++++++++++++++------ esphome/components/image/image.cpp | 35 +++++++++++++++++++++---- 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 801b05e160..a503e8f471 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -82,11 +82,13 @@ class ImageEncoder: self.dither = dither self.index = 0 self.invert_alpha = invert_alpha + self.path = "" - def convert(self, image): + def convert(self, image, path): """ Convert the image format :param image: Input image + :param path: Path to the image file :return: converted image """ return image @@ -103,6 +105,16 @@ class ImageEncoder: """ +def is_alpha_only(image: Image): + """ + Check if an image (assumed to be RGBA) is only alpha + """ + # Any alpha data? + if image.split()[-1].getextrema()[0] == 0xFF: + return False + return all(b.getextrema()[1] == 0 for b in image.split()[:-1]) + + class ImageBinary(ImageEncoder): allow_config = {CONF_OPAQUE, CONF_INVERT_ALPHA, CONF_CHROMA_KEY} @@ -111,7 +123,9 @@ class ImageBinary(ImageEncoder): super().__init__(self.width8, height, transparency, dither, invert_alpha) self.bitno = 0 - def convert(self, image): + def convert(self, image, path): + if is_alpha_only(image): + image = image.split()[-1] return image.convert("1", dither=self.dither) def encode(self, pixel): @@ -136,7 +150,16 @@ class ImageBinary(ImageEncoder): class ImageGrayscale(ImageEncoder): allow_config = {CONF_ALPHA_CHANNEL, CONF_CHROMA_KEY, CONF_INVERT_ALPHA, CONF_OPAQUE} - def convert(self, image): + def convert(self, image, path): + if is_alpha_only(image): + if self.transparency != CONF_ALPHA_CHANNEL: + _LOGGER.warning( + "Grayscale image %s is alpha only, but transparency is set to %s", + path, + self.transparency, + ) + self.transparency = CONF_ALPHA_CHANNEL + image = image.split()[-1] return image.convert("LA") def encode(self, pixel): @@ -166,7 +189,7 @@ class ImageRGB565(ImageEncoder): invert_alpha, ) - def convert(self, image): + def convert(self, image, path): return image.convert("RGBA") def encode(self, pixel): @@ -204,7 +227,7 @@ class ImageRGB(ImageEncoder): invert_alpha, ) - def convert(self, image): + def convert(self, image, path): return image.convert("RGBA") def encode(self, pixel): @@ -308,7 +331,7 @@ def is_svg_file(file): if not file: return False with open(file, "rb") as f: - return "get_grayscale_pixel_(img_x, img_y); - if (color.w >= 0x80) { - display->draw_pixel_at(x + img_x, y + img_y, color); + const uint32_t pos = (img_x + img_y * this->width_); + const uint8_t gray = progmem_read_byte(this->data_start_ + pos); + Color color = Color(gray, gray, gray, 0xFF); + switch (this->transparency_) { + case TRANSPARENCY_CHROMA_KEY: + if (gray == 1) { + continue; // skip drawing + } + break; + case TRANSPARENCY_ALPHA_CHANNEL: { + auto on = (float) gray / 255.0f; + auto off = 1.0f - on; + // blend color_on and color_off + color = Color(color_on.r * on + color_off.r * off, color_on.g * on + color_off.g * off, + color_on.b * on + color_off.b * off, 0xFF); + break; + } + default: + break; } + display->draw_pixel_at(x + img_x, y + img_y, color); } } break; @@ -179,8 +196,16 @@ Color Image::get_rgb565_pixel_(int x, int y) const { 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 && this->transparency_ == TRANSPARENCY_CHROMA_KEY) ? 0 : 0xFF; - return Color(gray, gray, gray, alpha); + switch (this->transparency_) { + case TRANSPARENCY_CHROMA_KEY: + if (gray == 1) + return Color(0, 0, 0, 0); + return Color(gray, gray, gray, 0xFF); + case TRANSPARENCY_ALPHA_CHANNEL: + return Color(0, 0, 0, gray); + default: + return Color(gray, gray, gray, 0xFF); + } } int Image::get_width() const { return this->width_; } int Image::get_height() const { return this->height_; } From e8d2ad4ce856ab2e2ae9b156c6f0cbe10f069ddf Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 15 Jan 2025 09:53:44 +1100 Subject: [PATCH 30/30] [ili9xxx] psram and 8 bit changes (#8084) --- esphome/components/ili9xxx/display.py | 40 ++++++++++++++----- .../components/ili9xxx/ili9xxx_display.cpp | 7 +--- esphome/components/ili9xxx/ili9xxx_display.h | 3 +- tests/components/ili9xxx/test.esp32-ard.yaml | 1 + 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/esphome/components/ili9xxx/display.py b/esphome/components/ili9xxx/display.py index 3c9dd2dab9..e3abb7e98c 100644 --- a/esphome/components/ili9xxx/display.py +++ b/esphome/components/ili9xxx/display.py @@ -1,9 +1,12 @@ +import logging + from esphome import core, pins import esphome.codegen as cg from esphome.components import display, spi from esphome.components.display import validate_rotation import esphome.config_validation as cv from esphome.const import ( + CONF_AUTO_CLEAR_ENABLED, CONF_COLOR_ORDER, CONF_COLOR_PALETTE, CONF_DC_PIN, @@ -27,17 +30,12 @@ from esphome.const import ( CONF_WIDTH, ) from esphome.core import CORE, HexInt +from esphome.final_validate import full_config DEPENDENCIES = ["spi"] - -def AUTO_LOAD(): - if CORE.is_esp32: - return ["psram"] - return [] - - CODEOWNERS = ["@nielsnl68", "@clydebarrow"] +LOGGER = logging.getLogger(__name__) ili9xxx_ns = cg.esphome_ns.namespace("ili9xxx") ILI9XXXDisplay = ili9xxx_ns.class_( @@ -84,7 +82,7 @@ COLOR_ORDERS = { "BGR": ColorOrder.COLOR_ORDER_BGR, } -COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE") +COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE", "8BIT", upper=True) CONF_LED_PIN = "led_pin" CONF_COLOR_PALETTE_IMAGES = "color_palette_images" @@ -195,9 +193,27 @@ CONFIG_SCHEMA = cv.All( _validate, ) -FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( - "ili9xxx", require_miso=False, require_mosi=True -) + +def final_validate(config): + global_config = full_config.get() + # Ideally would calculate buffer size here, but that info is not available on the Python side + needs_buffer = ( + CONF_LAMBDA in config or CONF_PAGES in config or config[CONF_AUTO_CLEAR_ENABLED] + ) + if ( + CORE.is_esp32 + and config[CONF_COLOR_PALETTE] == "NONE" + and "psram" not in global_config + and needs_buffer + ): + LOGGER.info("Consider enabling PSRAM if available for the display buffer") + + return spi.final_validate_device_schema( + "ili9xxx", require_miso=False, require_mosi=True + ) + + +FINAL_VALIDATE_SCHEMA = final_validate async def to_code(config): @@ -283,6 +299,8 @@ async def to_code(config): palette = converted.getpalette() assert len(palette) == 256 * 3 rhs = palette + elif config[CONF_COLOR_PALETTE] == "8BIT": + cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_8)) else: cg.add(var.set_buffer_color_mode(ILI9XXXColorMode.BITS_16)) diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index b9664067a9..f056f0a128 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -66,12 +66,9 @@ void ILI9XXXDisplay::setup() { void ILI9XXXDisplay::alloc_buffer_() { if (this->buffer_color_mode_ == BITS_16) { this->init_internal_(this->get_buffer_length_() * 2); - if (this->buffer_ != nullptr) { - return; - } - this->buffer_color_mode_ = BITS_8; + } else { + this->init_internal_(this->get_buffer_length_()); } - this->init_internal_(this->get_buffer_length_()); if (this->buffer_ == nullptr) { this->mark_failed(); } diff --git a/esphome/components/ili9xxx/ili9xxx_display.h b/esphome/components/ili9xxx/ili9xxx_display.h index c141739d2a..87d7c86e5c 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.h +++ b/esphome/components/ili9xxx/ili9xxx_display.h @@ -98,7 +98,8 @@ class ILI9XXXDisplay : public display::DisplayBuffer, protected: inline bool check_buffer_() { if (this->buffer_ == nullptr) { - this->alloc_buffer_(); + if (!this->is_failed()) + this->alloc_buffer_(); return !this->is_failed(); } return true; diff --git a/tests/components/ili9xxx/test.esp32-ard.yaml b/tests/components/ili9xxx/test.esp32-ard.yaml index 850273230a..c00c38ce3e 100644 --- a/tests/components/ili9xxx/test.esp32-ard.yaml +++ b/tests/components/ili9xxx/test.esp32-ard.yaml @@ -20,6 +20,7 @@ display: it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: ili9xxx invert_colors: false + color_palette: 8bit dimensions: width: 320 height: 240