From 287f65cbaf4ab31fcab7a0502ff5ddbeee51d0e4 Mon Sep 17 00:00:00 2001 From: Stuart Parmenter Date: Wed, 29 Oct 2025 13:27:31 -0700 Subject: [PATCH 1/7] [lvgl] fix typo from previous refactor (#11596) --- esphome/components/image/image.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/image/image.cpp b/esphome/components/image/image.cpp index 7b65c4d0cb..90e021467f 100644 --- a/esphome/components/image/image.cpp +++ b/esphome/components/image/image.cpp @@ -125,7 +125,7 @@ lv_img_dsc_t *Image::get_lv_img_dsc() { case IMAGE_TYPE_RGB: #if LV_COLOR_DEPTH == 32 - switch (this->transparent_) { + switch (this->transparency_) { case TRANSPARENCY_ALPHA_CHANNEL: this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA; break; @@ -156,7 +156,8 @@ lv_img_dsc_t *Image::get_lv_img_dsc() { break; } #else - this->dsc_.header.cf = this->transparent_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565; + this->dsc_.header.cf = + this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565; #endif break; } From 918650f15adb10d87ae6d0a4f04accdb27b97b73 Mon Sep 17 00:00:00 2001 From: Stuart Parmenter Date: Wed, 29 Oct 2025 14:06:45 -0700 Subject: [PATCH 2/7] [lvgl] memset canvas buffer to prevent display of random garbage (#11582) Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/lvgl/widgets/canvas.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/esphome/components/lvgl/widgets/canvas.py b/esphome/components/lvgl/widgets/canvas.py index 217e8935f1..f0a9cd35ba 100644 --- a/esphome/components/lvgl/widgets/canvas.py +++ b/esphome/components/lvgl/widgets/canvas.py @@ -33,7 +33,7 @@ from ..lv_validation import ( pixels, size, ) -from ..lvcode import LocalVariable, lv, lv_assign +from ..lvcode import LocalVariable, lv, lv_assign, lv_expr from ..schemas import STYLE_PROPS, STYLE_REMAP, TEXT_SCHEMA, point_schema from ..types import LvType, ObjUpdateAction, WidgetType from . import Widget, get_widgets @@ -70,15 +70,18 @@ class CanvasType(WidgetType): width = config[CONF_WIDTH] height = config[CONF_HEIGHT] use_alpha = "_ALPHA" if config[CONF_TRANSPARENT] else "" - lv.canvas_set_buffer( - w.obj, - lv.custom_mem_alloc( - literal(f"LV_CANVAS_BUF_SIZE_TRUE_COLOR{use_alpha}({width}, {height})") - ), - width, - height, - literal(f"LV_IMG_CF_TRUE_COLOR{use_alpha}"), + buf_size = literal( + f"LV_CANVAS_BUF_SIZE_TRUE_COLOR{use_alpha}({width}, {height})" ) + with LocalVariable("buf", cg.void, lv_expr.custom_mem_alloc(buf_size)) as buf: + cg.add(cg.RawExpression(f"memset({buf}, 0, {buf_size});")) + lv.canvas_set_buffer( + w.obj, + buf, + width, + height, + literal(f"LV_IMG_CF_TRUE_COLOR{use_alpha}"), + ) canvas_spec = CanvasType() From 03fd1143710d58243a93f5901dc49e677413b981 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 20:26:37 -0500 Subject: [PATCH 3/7] [ci] Restore parallel execution for clang-tidy split mode (#11613) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd45adb78b..a2b1f84ca8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -458,7 +458,7 @@ jobs: GH_TOKEN: ${{ github.token }} strategy: fail-fast: false - max-parallel: 1 + max-parallel: 2 matrix: include: - id: clang-tidy From 08aae39ea47d65dd4ff4af655fd967f2a64167f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 20:27:28 -0500 Subject: [PATCH 4/7] [ci] Consolidate component splitting into determine-jobs (#11614) --- .github/workflows/ci.yml | 46 ++--------------------------- script/determine-jobs.py | 24 +++++++++++++++ script/split_components_for_ci.py | 4 +++ tests/script/test_determine_jobs.py | 11 +++++++ 4 files changed, 42 insertions(+), 43 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a2b1f84ca8..1756d5b765 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -180,6 +180,7 @@ jobs: memory_impact: ${{ steps.determine.outputs.memory-impact }} cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }} cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }} + component-test-batches: ${{ steps.determine.outputs.component-test-batches }} steps: - name: Check out code from GitHub uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -214,6 +215,7 @@ jobs: echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT + echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT integration-tests: name: Run integration tests @@ -536,59 +538,18 @@ jobs: run: script/ci-suggest-changes if: always() - test-build-components-splitter: - name: Split components for intelligent grouping (40 weighted per batch) - runs-on: ubuntu-24.04 - needs: - - common - - determine-jobs - if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 - outputs: - matrix: ${{ steps.split.outputs.components }} - steps: - - name: Check out code from GitHub - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - - name: Restore Python - uses: ./.github/actions/restore-python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - cache-key: ${{ needs.common.outputs.cache-key }} - - name: Split components intelligently based on bus configurations - id: split - run: | - . venv/bin/activate - - # Use intelligent splitter that groups components with same bus configs - components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}' - - # Only isolate directly changed components when targeting dev branch - # For beta/release branches, group everything for faster CI - if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then - directly_changed='[]' - echo "Target branch: ${{ github.base_ref }} - grouping all components" - else - directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}' - echo "Target branch: ${{ github.base_ref }} - isolating directly changed components" - fi - - echo "Splitting components intelligently..." - output=$(python3 script/split_components_for_ci.py --components "$components" --directly-changed "$directly_changed" --batch-size 40 --output github) - - echo "$output" >> $GITHUB_OUTPUT - test-build-components-split: name: Test components batch (${{ matrix.components }}) runs-on: ubuntu-24.04 needs: - common - determine-jobs - - test-build-components-splitter if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 strategy: fail-fast: false max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }} matrix: - components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }} + components: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }} steps: - name: Show disk space run: | @@ -980,7 +941,6 @@ jobs: - clang-tidy-nosplit - clang-tidy-split - determine-jobs - - test-build-components-splitter - test-build-components-split - pre-commit-ci-lite - memory-impact-target-branch diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 21eb529f33..4a0edebb0d 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -43,12 +43,14 @@ from enum import StrEnum from functools import cache import json import os +from pathlib import Path import subprocess import sys from typing import Any from helpers import ( CPP_FILE_EXTENSIONS, + ESPHOME_TESTS_COMPONENTS_PATH, PYTHON_FILE_EXTENSIONS, changed_files, core_changed, @@ -65,12 +67,17 @@ from helpers import ( parse_test_filename, root_path, ) +from split_components_for_ci import create_intelligent_batches # Threshold for splitting clang-tidy jobs # For small PRs (< 65 files), use nosplit for faster CI # For large PRs (>= 65 files), use split for better parallelization CLANG_TIDY_SPLIT_THRESHOLD = 65 +# Component test batch size (weighted) +# Isolated components count as 10x, groupable components count as 1x +COMPONENT_TEST_BATCH_SIZE = 40 + class Platform(StrEnum): """Platform identifiers for memory impact analysis.""" @@ -686,6 +693,22 @@ def main() -> None: # Determine which C++ unit tests to run cpp_run_all, cpp_components = determine_cpp_unit_tests(args.branch) + # Split components into batches for CI testing + # This intelligently groups components with similar bus configurations + component_test_batches: list[str] + if changed_components_with_tests: + tests_dir = Path(root_path) / ESPHOME_TESTS_COMPONENTS_PATH + batches, _ = create_intelligent_batches( + components=changed_components_with_tests, + tests_dir=tests_dir, + batch_size=COMPONENT_TEST_BATCH_SIZE, + directly_changed=directly_changed_with_tests, + ) + # Convert batches to space-separated strings for CI matrix + component_test_batches = [" ".join(batch) for batch in batches] + else: + component_test_batches = [] + output: dict[str, Any] = { "integration_tests": run_integration, "clang_tidy": run_clang_tidy, @@ -703,6 +726,7 @@ def main() -> None: "memory_impact": memory_impact, "cpp_unit_tests_run_all": cpp_run_all, "cpp_unit_tests_components": cpp_components, + "component_test_batches": component_test_batches, } # Output as JSON diff --git a/script/split_components_for_ci.py b/script/split_components_for_ci.py index 87da540d43..65d09efb9b 100755 --- a/script/split_components_for_ci.py +++ b/script/split_components_for_ci.py @@ -62,6 +62,10 @@ def create_intelligent_batches( ) -> tuple[list[list[str]], dict[tuple[str, str], list[str]]]: """Create batches optimized for component grouping. + IMPORTANT: This function is called from both split_components_for_ci.py (standalone script) + and determine-jobs.py (integrated into job determination). Be careful when refactoring + to ensure changes work in both contexts. + Args: components: List of component names to batch tests_dir: Path to tests/components directory diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index c8ef76184f..e73c134151 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -152,6 +152,14 @@ def test_main_all_tests_should_run( assert output["memory_impact"]["should_run"] == "false" assert output["cpp_unit_tests_run_all"] is False assert output["cpp_unit_tests_components"] == ["wifi", "api", "sensor"] + # component_test_batches should be present and be a list of space-separated strings + assert "component_test_batches" in output + assert isinstance(output["component_test_batches"], list) + # Each batch should be a space-separated string of component names + for batch in output["component_test_batches"]: + assert isinstance(batch, str) + # Should contain at least one component (no empty batches) + assert len(batch) > 0 def test_main_no_tests_should_run( @@ -209,6 +217,9 @@ def test_main_no_tests_should_run( assert output["memory_impact"]["should_run"] == "false" assert output["cpp_unit_tests_run_all"] is False assert output["cpp_unit_tests_components"] == [] + # component_test_batches should be empty list + assert "component_test_batches" in output + assert output["component_test_batches"] == [] def test_main_with_branch_argument( From 29ed3c20af179fcdb2fd641566f7167fe79c863a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 20:28:38 -0500 Subject: [PATCH 5/7] [gpio] Skip set_use_interrupt call when using default value (#11612) --- esphome/components/gpio/binary_sensor/__init__.py | 4 +++- tests/component_tests/gpio/test_gpio_binary_sensor.py | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index 8372bc7e08..ca4dc43e9c 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -94,6 +94,8 @@ async def to_code(config): ) use_interrupt = False - cg.add(var.set_use_interrupt(use_interrupt)) if use_interrupt: cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) + else: + # Only generate call when disabling interrupts (default is true) + cg.add(var.set_use_interrupt(use_interrupt)) diff --git a/tests/component_tests/gpio/test_gpio_binary_sensor.py b/tests/component_tests/gpio/test_gpio_binary_sensor.py index 74fa2ab1c1..73665dc45d 100644 --- a/tests/component_tests/gpio/test_gpio_binary_sensor.py +++ b/tests/component_tests/gpio/test_gpio_binary_sensor.py @@ -18,7 +18,8 @@ def test_gpio_binary_sensor_basic_setup( assert "new gpio::GPIOBinarySensor();" in main_cpp assert "App.register_binary_sensor" in main_cpp - assert "bs_gpio->set_use_interrupt(true);" in main_cpp + # set_use_interrupt(true) should NOT be generated (uses C++ default) + assert "bs_gpio->set_use_interrupt(true);" not in main_cpp assert "bs_gpio->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp @@ -51,8 +52,8 @@ def test_gpio_binary_sensor_esp8266_other_pins_use_interrupt( "tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml" ) - # GPIO5 should still use interrupts - assert "bs_gpio5->set_use_interrupt(true);" in main_cpp + # GPIO5 should still use interrupts (default, so no setter call) + assert "bs_gpio5->set_use_interrupt(true);" not in main_cpp assert "bs_gpio5->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp From 58235049e3c7a023a37441aed6b8440abaec0626 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 21:06:21 -0500 Subject: [PATCH 6/7] [template] Eliminate optional wrapper to save 4 bytes RAM per instance (#11610) --- .../binary_sensor/template_binary_sensor.cpp | 14 +++-- .../binary_sensor/template_binary_sensor.h | 5 +- .../template/cover/template_cover.cpp | 33 ++++++------ .../template/cover/template_cover.h | 9 ++-- .../template/datetime/template_date.cpp | 15 +++--- .../template/datetime/template_date.h | 5 +- .../template/datetime/template_datetime.cpp | 21 ++++---- .../template/datetime/template_datetime.h | 5 +- .../template/datetime/template_time.cpp | 15 +++--- .../template/datetime/template_time.h | 5 +- .../template/lock/template_lock.cpp | 15 +++--- .../components/template/lock/template_lock.h | 6 ++- .../template/number/template_number.cpp | 9 ++-- .../template/number/template_number.h | 5 +- .../template/select/template_select.cpp | 16 +++--- .../template/select/template_select.h | 5 +- .../template/sensor/template_sensor.cpp | 5 +- .../template/sensor/template_sensor.h | 5 +- .../template/switch/template_switch.cpp | 15 +++--- .../template/switch/template_switch.h | 5 +- .../template/text/template_text.cpp | 18 +++---- .../components/template/text/template_text.h | 5 +- .../text_sensor/template_text_sensor.cpp | 5 +- .../text_sensor/template_text_sensor.h | 5 +- .../template/valve/template_valve.cpp | 17 +++---- .../template/valve/template_valve.h | 5 +- esphome/core/template_lambda.h | 51 +++++++++++++++++++ tests/components/template/common-base.yaml | 8 +++ tests/integration/fixtures/runtime_stats.yaml | 1 + 29 files changed, 196 insertions(+), 132 deletions(-) create mode 100644 esphome/core/template_lambda.h diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.cpp b/esphome/components/template/binary_sensor/template_binary_sensor.cpp index 8543dff4dc..806aed49b1 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.cpp +++ b/esphome/components/template/binary_sensor/template_binary_sensor.cpp @@ -6,17 +6,21 @@ namespace template_ { static const char *const TAG = "template.binary_sensor"; -void TemplateBinarySensor::setup() { this->loop(); } +void TemplateBinarySensor::setup() { + if (!this->f_.has_value()) { + this->disable_loop(); + } else { + this->loop(); + } +} void TemplateBinarySensor::loop() { - if (!this->f_.has_value()) - return; - - auto s = (*this->f_)(); + auto s = this->f_(); if (s.has_value()) { this->publish_state(*s); } } + void TemplateBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Template Binary Sensor", this); } } // namespace template_ diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index 2e0b216eb4..bc591391b9 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/binary_sensor/binary_sensor.h" namespace esphome { @@ -8,7 +9,7 @@ namespace template_ { class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor { public: - void set_template(optional (*f)()) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void loop() override; @@ -17,7 +18,7 @@ class TemplateBinarySensor : public Component, public binary_sensor::BinarySenso float get_setup_priority() const override { return setup_priority::HARDWARE; } protected: - optional (*)()> f_; + TemplateLambda f_; }; } // namespace template_ diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index bed3931e78..a87f28ccec 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -33,28 +33,27 @@ void TemplateCover::setup() { break; } } + if (!this->state_f_.has_value() && !this->tilt_f_.has_value()) + this->disable_loop(); } void TemplateCover::loop() { bool changed = false; - if (this->state_f_.has_value()) { - auto s = (*this->state_f_)(); - if (s.has_value()) { - auto pos = clamp(*s, 0.0f, 1.0f); - if (pos != this->position) { - this->position = pos; - changed = true; - } + auto s = this->state_f_(); + if (s.has_value()) { + auto pos = clamp(*s, 0.0f, 1.0f); + if (pos != this->position) { + this->position = pos; + changed = true; } } - if (this->tilt_f_.has_value()) { - auto s = (*this->tilt_f_)(); - if (s.has_value()) { - auto tilt = clamp(*s, 0.0f, 1.0f); - if (tilt != this->tilt) { - this->tilt = tilt; - changed = true; - } + + auto tilt = this->tilt_f_(); + if (tilt.has_value()) { + auto tilt_val = clamp(*tilt, 0.0f, 1.0f); + if (tilt_val != this->tilt) { + this->tilt = tilt_val; + changed = true; } } @@ -63,7 +62,6 @@ void TemplateCover::loop() { } void TemplateCover::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateCover::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } -void TemplateCover::set_state_lambda(optional (*f)()) { this->state_f_ = f; } float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; } @@ -124,7 +122,6 @@ CoverTraits TemplateCover::get_traits() { } Trigger *TemplateCover::get_position_trigger() const { return this->position_trigger_; } Trigger *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } -void TemplateCover::set_tilt_lambda(optional (*tilt_f)()) { this->tilt_f_ = tilt_f; } void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index ed1ebf4e43..faff69f867 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/cover/cover.h" namespace esphome { @@ -17,7 +18,8 @@ class TemplateCover : public cover::Cover, public Component { public: TemplateCover(); - void set_state_lambda(optional (*f)()); + template void set_state_lambda(F &&f) { this->state_f_.set(std::forward(f)); } + template void set_tilt_lambda(F &&f) { this->tilt_f_.set(std::forward(f)); } Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; @@ -26,7 +28,6 @@ class TemplateCover : public cover::Cover, public Component { Trigger *get_tilt_trigger() const; void set_optimistic(bool optimistic); void set_assumed_state(bool assumed_state); - void set_tilt_lambda(optional (*tilt_f)()); void set_has_stop(bool has_stop); void set_has_position(bool has_position); void set_has_tilt(bool has_tilt); @@ -45,8 +46,8 @@ class TemplateCover : public cover::Cover, public Component { void stop_prev_trigger_(); TemplateCoverRestoreMode restore_mode_{COVER_RESTORE}; - optional (*)()> state_f_; - optional (*)()> tilt_f_; + TemplateLambda state_f_; + TemplateLambda tilt_f_; bool assumed_state_{false}; bool optimistic_{false}; Trigger<> *open_trigger_; diff --git a/esphome/components/template/datetime/template_date.cpp b/esphome/components/template/datetime/template_date.cpp index 2fa8016802..3f6626e847 100644 --- a/esphome/components/template/datetime/template_date.cpp +++ b/esphome/components/template/datetime/template_date.cpp @@ -40,14 +40,13 @@ void TemplateDate::update() { if (!this->f_.has_value()) return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - this->year_ = val->year; - this->month_ = val->month; - this->day_ = val->day_of_month; - this->publish_state(); + auto val = this->f_(); + if (val.has_value()) { + this->year_ = val->year; + this->month_ = val->month; + this->day_ = val->day_of_month; + this->publish_state(); + } } void TemplateDate::control(const datetime::DateCall &call) { diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h index 2a0967fc94..7fed704d0e 100644 --- a/esphome/components/template/datetime/template_date.h +++ b/esphome/components/template/datetime/template_date.h @@ -9,13 +9,14 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { class TemplateDate : public datetime::DateEntity, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -35,7 +36,7 @@ class TemplateDate : public datetime::DateEntity, public PollingComponent { ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_; + TemplateLambda f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/datetime/template_datetime.cpp b/esphome/components/template/datetime/template_datetime.cpp index a4a4e47d65..62f842a7ad 100644 --- a/esphome/components/template/datetime/template_datetime.cpp +++ b/esphome/components/template/datetime/template_datetime.cpp @@ -43,17 +43,16 @@ void TemplateDateTime::update() { if (!this->f_.has_value()) return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - this->year_ = val->year; - this->month_ = val->month; - this->day_ = val->day_of_month; - this->hour_ = val->hour; - this->minute_ = val->minute; - this->second_ = val->second; - this->publish_state(); + auto val = this->f_(); + if (val.has_value()) { + this->year_ = val->year; + this->month_ = val->month; + this->day_ = val->day_of_month; + this->hour_ = val->hour; + this->minute_ = val->minute; + this->second_ = val->second; + this->publish_state(); + } } void TemplateDateTime::control(const datetime::DateTimeCall &call) { diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h index d917015b67..ec45bf0326 100644 --- a/esphome/components/template/datetime/template_datetime.h +++ b/esphome/components/template/datetime/template_datetime.h @@ -9,13 +9,14 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -35,7 +36,7 @@ class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponen ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_; + TemplateLambda f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/datetime/template_time.cpp b/esphome/components/template/datetime/template_time.cpp index 349700f187..dab28d01cc 100644 --- a/esphome/components/template/datetime/template_time.cpp +++ b/esphome/components/template/datetime/template_time.cpp @@ -40,14 +40,13 @@ void TemplateTime::update() { if (!this->f_.has_value()) return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - this->hour_ = val->hour; - this->minute_ = val->minute; - this->second_ = val->second; - this->publish_state(); + auto val = this->f_(); + if (val.has_value()) { + this->hour_ = val->hour; + this->minute_ = val->minute; + this->second_ = val->second; + this->publish_state(); + } } void TemplateTime::control(const datetime::TimeCall &call) { diff --git a/esphome/components/template/datetime/template_time.h b/esphome/components/template/datetime/template_time.h index 2f05ba0737..ea7474c0ba 100644 --- a/esphome/components/template/datetime/template_time.h +++ b/esphome/components/template/datetime/template_time.h @@ -9,13 +9,14 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" #include "esphome/core/time.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { class TemplateTime : public datetime::TimeEntity, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -35,7 +36,7 @@ class TemplateTime : public datetime::TimeEntity, public PollingComponent { ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_; + TemplateLambda f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/lock/template_lock.cpp b/esphome/components/template/lock/template_lock.cpp index c2e227c26d..8ed87b9736 100644 --- a/esphome/components/template/lock/template_lock.cpp +++ b/esphome/components/template/lock/template_lock.cpp @@ -11,14 +11,16 @@ static const char *const TAG = "template.lock"; TemplateLock::TemplateLock() : lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {} -void TemplateLock::loop() { +void TemplateLock::setup() { if (!this->f_.has_value()) - return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; + this->disable_loop(); +} - this->publish_state(*val); +void TemplateLock::loop() { + auto val = this->f_(); + if (val.has_value()) { + this->publish_state(*val); + } } void TemplateLock::control(const lock::LockCall &call) { if (this->prev_trigger_ != nullptr) { @@ -45,7 +47,6 @@ void TemplateLock::open_latch() { this->open_trigger_->trigger(); } void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } -void TemplateLock::set_state_lambda(optional (*f)()) { this->f_ = f; } float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; } Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_trigger_; } diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index 428744a66f..14fca4635e 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/lock/lock.h" namespace esphome { @@ -11,9 +12,10 @@ class TemplateLock : public lock::Lock, public Component { public: TemplateLock(); + void setup() override; void dump_config() override; - void set_state_lambda(optional (*f)()); + template void set_state_lambda(F &&f) { this->f_.set(std::forward(f)); } Trigger<> *get_lock_trigger() const; Trigger<> *get_unlock_trigger() const; Trigger<> *get_open_trigger() const; @@ -26,7 +28,7 @@ class TemplateLock : public lock::Lock, public Component { void control(const lock::LockCall &call) override; void open_latch() override; - optional (*)()> f_; + TemplateLambda f_; bool optimistic_{false}; Trigger<> *lock_trigger_; Trigger<> *unlock_trigger_; diff --git a/esphome/components/template/number/template_number.cpp b/esphome/components/template/number/template_number.cpp index 187f426273..145a89a2f7 100644 --- a/esphome/components/template/number/template_number.cpp +++ b/esphome/components/template/number/template_number.cpp @@ -30,11 +30,10 @@ void TemplateNumber::update() { if (!this->f_.has_value()) return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - this->publish_state(*val); + auto val = this->f_(); + if (val.has_value()) { + this->publish_state(*val); + } } void TemplateNumber::control(float value) { diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index e77b181d25..a9307e9246 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -4,13 +4,14 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { class TemplateNumber : public number::Number, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -28,7 +29,7 @@ class TemplateNumber : public number::Number, public PollingComponent { float initial_value_{NAN}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_; + TemplateLambda f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index c7a1d8a344..3ea34c3c7c 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -31,16 +31,14 @@ void TemplateSelect::update() { if (!this->f_.has_value()) return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - if (!this->has_option(*val)) { - ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str()); - return; + auto val = this->f_(); + if (val.has_value()) { + if (!this->has_option(*val)) { + ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str()); + return; + } + this->publish_state(*val); } - - this->publish_state(*val); } void TemplateSelect::control(const std::string &value) { diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index e77e4d8f14..1c33153872 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -4,13 +4,14 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { class TemplateSelect : public select::Select, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -28,7 +29,7 @@ class TemplateSelect : public select::Select, public PollingComponent { size_t initial_option_index_{0}; bool restore_value_ = false; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_; + TemplateLambda f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/sensor/template_sensor.cpp b/esphome/components/template/sensor/template_sensor.cpp index 65f2417670..1558ea9b15 100644 --- a/esphome/components/template/sensor/template_sensor.cpp +++ b/esphome/components/template/sensor/template_sensor.cpp @@ -11,13 +11,14 @@ void TemplateSensor::update() { if (!this->f_.has_value()) return; - auto val = (*this->f_)(); + auto val = this->f_(); if (val.has_value()) { this->publish_state(*val); } } + float TemplateSensor::get_setup_priority() const { return setup_priority::HARDWARE; } -void TemplateSensor::set_template(optional (*f)()) { this->f_ = f; } + void TemplateSensor::dump_config() { LOG_SENSOR("", "Template Sensor", this); LOG_UPDATE_INTERVAL(this); diff --git a/esphome/components/template/sensor/template_sensor.h b/esphome/components/template/sensor/template_sensor.h index 369313d607..793d754a0f 100644 --- a/esphome/components/template/sensor/template_sensor.h +++ b/esphome/components/template/sensor/template_sensor.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/sensor/sensor.h" namespace esphome { @@ -8,7 +9,7 @@ namespace template_ { class TemplateSensor : public sensor::Sensor, public PollingComponent { public: - void set_template(optional (*f)()); + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void update() override; @@ -17,7 +18,7 @@ class TemplateSensor : public sensor::Sensor, public PollingComponent { float get_setup_priority() const override; protected: - optional (*)()> f_; + TemplateLambda f_; }; } // namespace template_ diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index 5aaf514b2a..95e8692da5 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -9,13 +9,10 @@ static const char *const TAG = "template.switch"; TemplateSwitch::TemplateSwitch() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {} void TemplateSwitch::loop() { - if (!this->f_.has_value()) - return; - auto s = (*this->f_)(); - if (!s.has_value()) - return; - - this->publish_state(*s); + auto s = this->f_(); + if (s.has_value()) { + this->publish_state(*s); + } } void TemplateSwitch::write_state(bool state) { if (this->prev_trigger_ != nullptr) { @@ -35,11 +32,13 @@ void TemplateSwitch::write_state(bool state) { } void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } bool TemplateSwitch::assumed_state() { return this->assumed_state_; } -void TemplateSwitch::set_state_lambda(optional (*f)()) { this->f_ = f; } float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE - 2.0f; } Trigger<> *TemplateSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } void TemplateSwitch::setup() { + if (!this->f_.has_value()) + this->disable_loop(); + optional initial_state = this->get_initial_state_with_restore_mode(); if (initial_state.has_value()) { diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index 0fba66b9bd..18a374df35 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/switch/switch.h" namespace esphome { @@ -14,7 +15,7 @@ class TemplateSwitch : public switch_::Switch, public Component { void setup() override; void dump_config() override; - void set_state_lambda(optional (*f)()); + template void set_state_lambda(F &&f) { this->f_.set(std::forward(f)); } Trigger<> *get_turn_on_trigger() const; Trigger<> *get_turn_off_trigger() const; void set_optimistic(bool optimistic); @@ -28,7 +29,7 @@ class TemplateSwitch : public switch_::Switch, public Component { void write_state(bool state) override; - optional (*)()> f_; + TemplateLambda f_; bool optimistic_{false}; bool assumed_state_{false}; Trigger<> *turn_on_trigger_; diff --git a/esphome/components/template/text/template_text.cpp b/esphome/components/template/text/template_text.cpp index d8e840ba7e..a917c72a14 100644 --- a/esphome/components/template/text/template_text.cpp +++ b/esphome/components/template/text/template_text.cpp @@ -7,10 +7,8 @@ namespace template_ { static const char *const TAG = "template.text"; void TemplateText::setup() { - if (!(this->f_ == nullptr)) { - if (this->f_.has_value()) - return; - } + if (this->f_.has_value()) + return; std::string value = this->initial_value_; if (!this->pref_) { ESP_LOGD(TAG, "State from initial: %s", value.c_str()); @@ -26,17 +24,13 @@ void TemplateText::setup() { } void TemplateText::update() { - if (this->f_ == nullptr) - return; - if (!this->f_.has_value()) return; - auto val = (*this->f_)(); - if (!val.has_value()) - return; - - this->publish_state(*val); + auto val = this->f_(); + if (val.has_value()) { + this->publish_state(*val); + } } void TemplateText::control(const std::string &value) { diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index 6c17d2016a..c12021f80e 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -4,6 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" +#include "esphome/core/template_lambda.h" namespace esphome { namespace template_ { @@ -61,7 +62,7 @@ template class TextSaver : public TemplateTextSaverBase { class TemplateText : public text::Text, public PollingComponent { public: - void set_template(optional (*f)()) { this->f_ = f; } + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; void update() override; @@ -78,7 +79,7 @@ class TemplateText : public text::Text, public PollingComponent { bool optimistic_ = false; std::string initial_value_; Trigger *set_trigger_ = new Trigger(); - optional (*)()> f_{nullptr}; + TemplateLambda f_{}; TemplateTextSaverBase *pref_ = nullptr; }; diff --git a/esphome/components/template/text_sensor/template_text_sensor.cpp b/esphome/components/template/text_sensor/template_text_sensor.cpp index 2b0297d62f..024d0093a2 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.cpp +++ b/esphome/components/template/text_sensor/template_text_sensor.cpp @@ -10,13 +10,14 @@ void TemplateTextSensor::update() { if (!this->f_.has_value()) return; - auto val = (*this->f_)(); + auto val = this->f_(); if (val.has_value()) { this->publish_state(*val); } } + float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } -void TemplateTextSensor::set_template(optional (*f)()) { this->f_ = f; } + void TemplateTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Template Sensor", this); } } // namespace template_ diff --git a/esphome/components/template/text_sensor/template_text_sensor.h b/esphome/components/template/text_sensor/template_text_sensor.h index 48e40c2493..0d01c72023 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.h +++ b/esphome/components/template/text_sensor/template_text_sensor.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/text_sensor/text_sensor.h" namespace esphome { @@ -9,7 +10,7 @@ namespace template_ { class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent { public: - void set_template(optional (*f)()); + template void set_template(F &&f) { this->f_.set(std::forward(f)); } void update() override; @@ -18,7 +19,7 @@ class TemplateTextSensor : public text_sensor::TextSensor, public PollingCompone void dump_config() override; protected: - optional (*)()> f_{}; + TemplateLambda f_{}; }; } // namespace template_ diff --git a/esphome/components/template/valve/template_valve.cpp b/esphome/components/template/valve/template_valve.cpp index b27cc00968..b91b32473e 100644 --- a/esphome/components/template/valve/template_valve.cpp +++ b/esphome/components/template/valve/template_valve.cpp @@ -33,19 +33,19 @@ void TemplateValve::setup() { break; } } + if (!this->state_f_.has_value()) + this->disable_loop(); } void TemplateValve::loop() { bool changed = false; - if (this->state_f_.has_value()) { - auto s = (*this->state_f_)(); - if (s.has_value()) { - auto pos = clamp(*s, 0.0f, 1.0f); - if (pos != this->position) { - this->position = pos; - changed = true; - } + auto s = this->state_f_(); + if (s.has_value()) { + auto pos = clamp(*s, 0.0f, 1.0f); + if (pos != this->position) { + this->position = pos; + changed = true; } } @@ -55,7 +55,6 @@ void TemplateValve::loop() { void TemplateValve::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateValve::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } -void TemplateValve::set_state_lambda(optional (*f)()) { this->state_f_ = f; } float TemplateValve::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateValve::get_open_trigger() const { return this->open_trigger_; } diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index 92c32f3487..d6235f8e5c 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/template_lambda.h" #include "esphome/components/valve/valve.h" namespace esphome { @@ -17,7 +18,7 @@ class TemplateValve : public valve::Valve, public Component { public: TemplateValve(); - void set_state_lambda(optional (*f)()); + template void set_state_lambda(F &&f) { this->state_f_.set(std::forward(f)); } Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; @@ -42,7 +43,7 @@ class TemplateValve : public valve::Valve, public Component { void stop_prev_trigger_(); TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE}; - optional (*)()> state_f_; + TemplateLambda state_f_; bool assumed_state_{false}; bool optimistic_{false}; Trigger<> *open_trigger_; diff --git a/esphome/core/template_lambda.h b/esphome/core/template_lambda.h new file mode 100644 index 0000000000..7b8f4374aa --- /dev/null +++ b/esphome/core/template_lambda.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/optional.h" + +namespace esphome { + +/** Lightweight wrapper for template platform lambdas (stateless function pointers only). + * + * This optimizes template platforms by storing only a function pointer (4 bytes on ESP32) + * instead of std::function (16-32 bytes). + * + * IMPORTANT: This only supports stateless lambdas (no captures). The set_template() method + * is an internal API used by YAML codegen, not intended for external use. + * + * Lambdas must return optional to support the pattern: + * return {}; // Don't publish a value + * return 42.0; // Publish this value + * + * operator() returns optional, returning nullopt when no lambda is set (nullptr check). + * + * @tparam T The return type (e.g., float for sensor values) + * @tparam Args Optional arguments for the lambda + */ +template class TemplateLambda { + public: + TemplateLambda() : f_(nullptr) {} + + /** Set the lambda function pointer. + * INTERNAL API: Only for use by YAML codegen. + * Only stateless lambdas (no captures) are supported. + */ + void set(optional (*f)(Args...)) { this->f_ = f; } + + /** Check if a lambda is set */ + bool has_value() const { return this->f_ != nullptr; } + + /** Call the lambda, returning nullopt if no lambda is set */ + optional operator()(Args &&...args) { + if (this->f_ == nullptr) + return nullopt; + return this->f_(std::forward(args)...); + } + + /** Alias for operator() for compatibility */ + optional call(Args &&...args) { return (*this)(std::forward(args)...); } + + protected: + optional (*f_)(Args...); // Function pointer (4 bytes on ESP32) +}; + +} // namespace esphome diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index b873af5207..f101eea942 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -9,6 +9,13 @@ esphome: id: template_sens state: !lambda "return 42.0;" + # Test C++ API: set_template() with stateless lambda (no captures) + # NOTE: set_template() is not intended to be a public API, but we test it to ensure it doesn't break. + - lambda: |- + id(template_sens).set_template([]() -> esphome::optional { + return 123.0f; + }); + - datetime.date.set: id: test_date date: @@ -215,6 +222,7 @@ cover: number: - platform: template + id: template_number name: "Template number" optimistic: true min_value: 0 diff --git a/tests/integration/fixtures/runtime_stats.yaml b/tests/integration/fixtures/runtime_stats.yaml index aad1c275fb..fd34cdb939 100644 --- a/tests/integration/fixtures/runtime_stats.yaml +++ b/tests/integration/fixtures/runtime_stats.yaml @@ -32,6 +32,7 @@ switch: name: "Test Switch" id: test_switch optimistic: true + lambda: return false; interval: - interval: 0.5s From bd87e56bc7bbdb2a1d7178536bd9f1c5b9232d45 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Oct 2025 21:14:03 -0500 Subject: [PATCH 7/7] [e131] Replace std::set with std::vector to reduce flash usage (#11598) --- esphome/components/e131/e131.cpp | 13 +++++++++---- esphome/components/e131/e131.h | 4 +--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index d18d945cec..c10c88faf2 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -3,6 +3,8 @@ #include "e131_addressable_light_effect.h" #include "esphome/core/log.h" +#include + namespace esphome { namespace e131 { @@ -76,14 +78,14 @@ void E131Component::loop() { } void E131Component::add_effect(E131AddressableLightEffect *light_effect) { - if (light_effects_.count(light_effect)) { + if (std::find(light_effects_.begin(), light_effects_.end(), light_effect) != light_effects_.end()) { return; } ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), light_effect->get_last_universe()); - light_effects_.insert(light_effect); + light_effects_.push_back(light_effect); for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { join_(universe); @@ -91,14 +93,17 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) { } void E131Component::remove_effect(E131AddressableLightEffect *light_effect) { - if (!light_effects_.count(light_effect)) { + auto it = std::find(light_effects_.begin(), light_effects_.end(), light_effect); + if (it == light_effects_.end()) { return; } ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), light_effect->get_last_universe()); - light_effects_.erase(light_effect); + // Swap with last element and pop for O(1) removal (order doesn't matter) + *it = light_effects_.back(); + light_effects_.pop_back(); for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { leave_(universe); diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h index d0e38fa98c..831138a545 100644 --- a/esphome/components/e131/e131.h +++ b/esphome/components/e131/e131.h @@ -7,7 +7,6 @@ #include #include #include -#include #include namespace esphome { @@ -47,9 +46,8 @@ class E131Component : public esphome::Component { E131ListenMethod listen_method_{E131_MULTICAST}; std::unique_ptr socket_; - std::set light_effects_; + std::vector light_effects_; std::map universe_consumers_; - std::map universe_packets_; }; } // namespace e131