From 1577a46efdb6ab828c5b3efb7caa85888380d54d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 25 Oct 2025 22:09:42 -0700 Subject: [PATCH 1/3] [gpio] Skip set_inverted() call for default false value (#11538) --- esphome/components/esp32/gpio.h | 6 +++--- esphome/components/esp32/gpio.py | 5 ++++- esphome/components/esp8266/gpio.h | 4 ++-- esphome/components/esp8266/gpio.py | 5 ++++- esphome/components/host/gpio.h | 4 ++-- esphome/components/host/gpio.py | 5 ++++- esphome/components/libretiny/gpio.py | 5 ++++- esphome/components/libretiny/gpio_arduino.h | 4 ++-- esphome/components/nrf52/gpio.py | 5 ++++- esphome/components/rp2040/gpio.h | 4 ++-- esphome/components/rp2040/gpio.py | 5 ++++- esphome/components/zephyr/gpio.h | 8 ++++---- 12 files changed, 39 insertions(+), 21 deletions(-) diff --git a/esphome/components/esp32/gpio.h b/esphome/components/esp32/gpio.h index 565e276ea8..d30f4bdcba 100644 --- a/esphome/components/esp32/gpio.h +++ b/esphome/components/esp32/gpio.h @@ -40,13 +40,13 @@ class ESP32InternalGPIOPin : public InternalGPIOPin { // - 3 bytes for members below // - 1 byte padding for alignment // - 4 bytes for vtable pointer - uint8_t pin_; // GPIO pin number (0-255, actual max ~54 on ESP32) - gpio::Flags flags_; // GPIO flags (1 byte) + uint8_t pin_; // GPIO pin number (0-255, actual max ~54 on ESP32) + gpio::Flags flags_{}; // GPIO flags (1 byte) struct PinFlags { uint8_t inverted : 1; // Invert pin logic (1 bit) uint8_t drive_strength : 2; // Drive strength 0-3 (2 bits) uint8_t reserved : 5; // Reserved for future use (5 bits) - } pin_flags_; // Total: 1 byte + } pin_flags_{}; // Total: 1 byte // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) static bool isr_service_installed; }; diff --git a/esphome/components/esp32/gpio.py b/esphome/components/esp32/gpio.py index 513f463d57..954891ea8d 100644 --- a/esphome/components/esp32/gpio.py +++ b/esphome/components/esp32/gpio.py @@ -223,7 +223,10 @@ async def esp32_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}"))) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) if CONF_DRIVE_STRENGTH in config: cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH])) cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) diff --git a/esphome/components/esp8266/gpio.h b/esphome/components/esp8266/gpio.h index dd6407885e..a1b6d79b3b 100644 --- a/esphome/components/esp8266/gpio.h +++ b/esphome/components/esp8266/gpio.h @@ -29,8 +29,8 @@ class ESP8266GPIOPin : public InternalGPIOPin { void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; uint8_t pin_; - bool inverted_; - gpio::Flags flags_; + bool inverted_{}; + gpio::Flags flags_{}; }; } // namespace esp8266 diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index e7492fc505..2e8d6496bc 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -165,7 +165,10 @@ async def esp8266_pin_to_code(config): num = config[CONF_NUMBER] mode = config[CONF_MODE] cg.add(var.set_pin(num)) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) cg.add(var.set_flags(pins.gpio_flags_expr(mode))) if num < 16: initial_state: PinInitialState = CORE.data[KEY_ESP8266][KEY_PIN_INITIAL_STATES][ diff --git a/esphome/components/host/gpio.h b/esphome/components/host/gpio.h index a60d535912..ae677291b9 100644 --- a/esphome/components/host/gpio.h +++ b/esphome/components/host/gpio.h @@ -28,8 +28,8 @@ class HostGPIOPin : public InternalGPIOPin { void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; uint8_t pin_; - bool inverted_; - gpio::Flags flags_; + bool inverted_{}; + gpio::Flags flags_{}; }; } // namespace host diff --git a/esphome/components/host/gpio.py b/esphome/components/host/gpio.py index 0f22a790bd..fcfb0b6c54 100644 --- a/esphome/components/host/gpio.py +++ b/esphome/components/host/gpio.py @@ -57,6 +57,9 @@ async def host_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(num)) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) return var diff --git a/esphome/components/libretiny/gpio.py b/esphome/components/libretiny/gpio.py index 07eb0ce133..9bad400eb7 100644 --- a/esphome/components/libretiny/gpio.py +++ b/esphome/components/libretiny/gpio.py @@ -199,6 +199,9 @@ async def component_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(num)) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) return var diff --git a/esphome/components/libretiny/gpio_arduino.h b/esphome/components/libretiny/gpio_arduino.h index 9adc425a41..3674748c18 100644 --- a/esphome/components/libretiny/gpio_arduino.h +++ b/esphome/components/libretiny/gpio_arduino.h @@ -27,8 +27,8 @@ class ArduinoInternalGPIOPin : public InternalGPIOPin { void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; uint8_t pin_; - bool inverted_; - gpio::Flags flags_; + bool inverted_{}; + gpio::Flags flags_{}; }; } // namespace libretiny diff --git a/esphome/components/nrf52/gpio.py b/esphome/components/nrf52/gpio.py index 260114f90e..17329042b2 100644 --- a/esphome/components/nrf52/gpio.py +++ b/esphome/components/nrf52/gpio.py @@ -74,6 +74,9 @@ async def nrf52_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(num)) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) return var diff --git a/esphome/components/rp2040/gpio.h b/esphome/components/rp2040/gpio.h index 9bc66d9e4b..47a6fe17f2 100644 --- a/esphome/components/rp2040/gpio.h +++ b/esphome/components/rp2040/gpio.h @@ -29,8 +29,8 @@ class RP2040GPIOPin : public InternalGPIOPin { void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; uint8_t pin_; - bool inverted_; - gpio::Flags flags_; + bool inverted_{}; + gpio::Flags flags_{}; }; } // namespace rp2040 diff --git a/esphome/components/rp2040/gpio.py b/esphome/components/rp2040/gpio.py index 58514f7db5..193e567d17 100644 --- a/esphome/components/rp2040/gpio.py +++ b/esphome/components/rp2040/gpio.py @@ -94,6 +94,9 @@ async def rp2040_pin_to_code(config): var = cg.new_Pvariable(config[CONF_ID]) num = config[CONF_NUMBER] cg.add(var.set_pin(num)) - cg.add(var.set_inverted(config[CONF_INVERTED])) + # Only set if true to avoid bloating setup() function + # (inverted bit in pin_flags_ bitfield is zero-initialized to false) + if config[CONF_INVERTED]: + cg.add(var.set_inverted(True)) cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) return var diff --git a/esphome/components/zephyr/gpio.h b/esphome/components/zephyr/gpio.h index f512ae4648..6e8f81857a 100644 --- a/esphome/components/zephyr/gpio.h +++ b/esphome/components/zephyr/gpio.h @@ -26,10 +26,10 @@ class ZephyrGPIOPin : public InternalGPIOPin { protected: void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; uint8_t pin_; - bool inverted_; - gpio::Flags flags_; - const device *gpio_ = nullptr; - bool value_ = false; + bool inverted_{}; + gpio::Flags flags_{}; + const device *gpio_{nullptr}; + bool value_{false}; }; } // namespace zephyr From 7394cbf77329f3b5a035e5ce334c9ac0ae19818c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 26 Oct 2025 09:00:08 -0400 Subject: [PATCH 2/3] [core] Don't allow python 3.14 (#11527) --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b7b4a48d7e..49598d434d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,9 @@ classifiers = [ "Programming Language :: Python :: 3", "Topic :: Home Automation", ] -requires-python = ">=3.11.0" + +# Python 3.14 is currently not supported by IDF <= 5.5.1, see https://github.com/esphome/esphome/issues/11502 +requires-python = ">=3.11.0,<3.14" dynamic = ["dependencies", "optional-dependencies", "version"] From 17d875c8e77d5049aa534ef0489843e0a546bf73 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 26 Oct 2025 19:39:56 -0500 Subject: [PATCH 3/3] [template] Optimize all template platforms to use function pointers for stateless lambdas --- .../template/binary_sensor/__init__.py | 8 ++- .../binary_sensor/template_binary_sensor.cpp | 4 +- .../binary_sensor/template_binary_sensor.h | 4 +- .../template/cover/template_cover.cpp | 4 +- .../template/cover/template_cover.h | 8 +-- .../template/datetime/template_date.h | 4 +- .../template/datetime/template_datetime.h | 4 +- .../template/datetime/template_time.h | 4 +- .../template/lock/template_lock.cpp | 2 +- .../components/template/lock/template_lock.h | 4 +- .../template/number/template_number.h | 4 +- .../template/select/template_select.h | 4 +- .../template/sensor/template_sensor.cpp | 2 +- .../template/sensor/template_sensor.h | 4 +- .../template/switch/template_switch.cpp | 2 +- .../template/switch/template_switch.h | 4 +- .../components/template/text/template_text.h | 4 +- .../text_sensor/template_text_sensor.cpp | 2 +- .../text_sensor/template_text_sensor.h | 4 +- .../template/valve/template_valve.cpp | 2 +- .../template/valve/template_valve.h | 4 +- esphome/cpp_generator.py | 8 +++ tests/component_tests/text/test_text.py | 5 +- tests/unit_tests/test_cpp_generator.py | 55 +++++++++++++++++++ 24 files changed, 110 insertions(+), 40 deletions(-) diff --git a/esphome/components/template/binary_sensor/__init__.py b/esphome/components/template/binary_sensor/__init__.py index c93876380d..9d4208dcca 100644 --- a/esphome/components/template/binary_sensor/__init__.py +++ b/esphome/components/template/binary_sensor/__init__.py @@ -38,8 +38,14 @@ async def to_code(config): condition = await automation.build_condition( condition, cg.TemplateArguments(), [] ) + # Generate a stateless lambda that calls condition.check() + # capture="" is safe because condition is a global variable in generated C++ code + # and doesn't need to be captured. This allows implicit conversion to function pointer. template_ = LambdaExpression( - f"return {condition.check()};", [], return_type=cg.optional.template(bool) + f"return {condition.check()};", + [], + return_type=cg.optional.template(bool), + capture="", ) cg.add(var.set_template(template_)) diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.cpp b/esphome/components/template/binary_sensor/template_binary_sensor.cpp index d1fb618695..8543dff4dc 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.cpp +++ b/esphome/components/template/binary_sensor/template_binary_sensor.cpp @@ -9,10 +9,10 @@ static const char *const TAG = "template.binary_sensor"; void TemplateBinarySensor::setup() { this->loop(); } void TemplateBinarySensor::loop() { - if (this->f_ == nullptr) + if (!this->f_.has_value()) return; - auto s = this->f_(); + auto s = (*this->f_)(); if (s.has_value()) { this->publish_state(*s); } diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index 5e5624d82e..2e0b216eb4 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -8,7 +8,7 @@ namespace template_ { class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void loop() override; @@ -17,7 +17,7 @@ class TemplateBinarySensor : public Component, public binary_sensor::BinarySenso float get_setup_priority() const override { return setup_priority::HARDWARE; } protected: - std::function()> f_{nullptr}; + optional (*)()> f_; }; } // namespace template_ diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index 84c687536e..bed3931e78 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -63,7 +63,7 @@ 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(std::function()> &&f) { this->state_f_ = f; } +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 +124,7 @@ 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(std::function()> &&tilt_f) { this->tilt_f_ = tilt_f; } +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 958c94b0a6..ed1ebf4e43 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -17,7 +17,7 @@ class TemplateCover : public cover::Cover, public Component { public: TemplateCover(); - void set_state_lambda(std::function()> &&f); + void set_state_lambda(optional (*f)()); Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; @@ -26,7 +26,7 @@ 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(std::function()> &&tilt_f); + 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 +45,8 @@ class TemplateCover : public cover::Cover, public Component { void stop_prev_trigger_(); TemplateCoverRestoreMode restore_mode_{COVER_RESTORE}; - optional()>> state_f_; - optional()>> tilt_f_; + optional (*)()> state_f_; + optional (*)()> tilt_f_; bool assumed_state_{false}; bool optimistic_{false}; Trigger<> *open_trigger_; diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h index 185c7ed49d..2a0967fc94 100644 --- a/esphome/components/template/datetime/template_date.h +++ b/esphome/components/template/datetime/template_date.h @@ -15,7 +15,7 @@ namespace template_ { class TemplateDate : public datetime::DateEntity, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -35,7 +35,7 @@ class TemplateDate : public datetime::DateEntity, public PollingComponent { ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + optional (*)()> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h index ef80ded89a..d917015b67 100644 --- a/esphome/components/template/datetime/template_datetime.h +++ b/esphome/components/template/datetime/template_datetime.h @@ -15,7 +15,7 @@ namespace template_ { class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -35,7 +35,7 @@ class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponen ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + optional (*)()> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/datetime/template_time.h b/esphome/components/template/datetime/template_time.h index 4a7c0098ec..2f05ba0737 100644 --- a/esphome/components/template/datetime/template_time.h +++ b/esphome/components/template/datetime/template_time.h @@ -15,7 +15,7 @@ namespace template_ { class TemplateTime : public datetime::TimeEntity, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -35,7 +35,7 @@ class TemplateTime : public datetime::TimeEntity, public PollingComponent { ESPTime initial_value_{}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + optional (*)()> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/lock/template_lock.cpp b/esphome/components/template/lock/template_lock.cpp index 87ba1046eb..c2e227c26d 100644 --- a/esphome/components/template/lock/template_lock.cpp +++ b/esphome/components/template/lock/template_lock.cpp @@ -45,7 +45,7 @@ void TemplateLock::open_latch() { this->open_trigger_->trigger(); } void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } -void TemplateLock::set_state_lambda(std::function()> &&f) { this->f_ = f; } +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 4f798eca81..428744a66f 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -13,7 +13,7 @@ class TemplateLock : public lock::Lock, public Component { void dump_config() override; - void set_state_lambda(std::function()> &&f); + void set_state_lambda(optional (*f)()); Trigger<> *get_lock_trigger() const; Trigger<> *get_unlock_trigger() const; Trigger<> *get_open_trigger() const; @@ -26,7 +26,7 @@ class TemplateLock : public lock::Lock, public Component { void control(const lock::LockCall &call) override; void open_latch() override; - optional()>> f_; + optional (*)()> f_; bool optimistic_{false}; Trigger<> *lock_trigger_; Trigger<> *unlock_trigger_; diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index 9a82e44339..e77b181d25 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -10,7 +10,7 @@ namespace template_ { class TemplateNumber : public number::Number, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -28,7 +28,7 @@ class TemplateNumber : public number::Number, public PollingComponent { float initial_value_{NAN}; bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + optional (*)()> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 2f00765c3d..c1b348b26a 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -10,7 +10,7 @@ namespace template_ { class TemplateSelect : public select::Select, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -28,7 +28,7 @@ class TemplateSelect : public select::Select, public PollingComponent { std::string initial_option_; bool restore_value_ = false; Trigger *set_trigger_ = new Trigger(); - optional()>> f_; + optional (*)()> f_; ESPPreferenceObject pref_; }; diff --git a/esphome/components/template/sensor/template_sensor.cpp b/esphome/components/template/sensor/template_sensor.cpp index f2d0e7363e..65f2417670 100644 --- a/esphome/components/template/sensor/template_sensor.cpp +++ b/esphome/components/template/sensor/template_sensor.cpp @@ -17,7 +17,7 @@ void TemplateSensor::update() { } } float TemplateSensor::get_setup_priority() const { return setup_priority::HARDWARE; } -void TemplateSensor::set_template(std::function()> &&f) { this->f_ = f; } +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 2630cb0b14..369313d607 100644 --- a/esphome/components/template/sensor/template_sensor.h +++ b/esphome/components/template/sensor/template_sensor.h @@ -8,7 +8,7 @@ namespace template_ { class TemplateSensor : public sensor::Sensor, public PollingComponent { public: - void set_template(std::function()> &&f); + void set_template(optional (*f)()); void update() override; @@ -17,7 +17,7 @@ class TemplateSensor : public sensor::Sensor, public PollingComponent { float get_setup_priority() const override; protected: - optional()>> f_; + optional (*)()> f_; }; } // namespace template_ diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index fa236f6364..5aaf514b2a 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -35,7 +35,7 @@ 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(std::function()> &&f) { this->f_ = f; } +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_; } diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index bfe9ac25d6..0fba66b9bd 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -14,7 +14,7 @@ class TemplateSwitch : public switch_::Switch, public Component { void setup() override; void dump_config() override; - void set_state_lambda(std::function()> &&f); + void set_state_lambda(optional (*f)()); Trigger<> *get_turn_on_trigger() const; Trigger<> *get_turn_off_trigger() const; void set_optimistic(bool optimistic); @@ -28,7 +28,7 @@ class TemplateSwitch : public switch_::Switch, public Component { void write_state(bool state) override; - optional()>> f_; + optional (*)()> f_; bool optimistic_{false}; bool assumed_state_{false}; Trigger<> *turn_on_trigger_; diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index bcfc54a2ba..6c17d2016a 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -61,7 +61,7 @@ template class TextSaver : public TemplateTextSaverBase { class TemplateText : public text::Text, public PollingComponent { public: - void set_template(std::function()> &&f) { this->f_ = f; } + void set_template(optional (*f)()) { this->f_ = f; } void setup() override; void update() override; @@ -78,7 +78,7 @@ class TemplateText : public text::Text, public PollingComponent { bool optimistic_ = false; std::string initial_value_; Trigger *set_trigger_ = new Trigger(); - optional()>> f_{nullptr}; + optional (*)()> f_{nullptr}; 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 885ad47bbf..2b0297d62f 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.cpp +++ b/esphome/components/template/text_sensor/template_text_sensor.cpp @@ -16,7 +16,7 @@ void TemplateTextSensor::update() { } } float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } -void TemplateTextSensor::set_template(std::function()> &&f) { this->f_ = f; } +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 07a2bd96fc..48e40c2493 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.h +++ b/esphome/components/template/text_sensor/template_text_sensor.h @@ -9,7 +9,7 @@ namespace template_ { class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent { public: - void set_template(std::function()> &&f); + void set_template(optional (*f)()); void update() override; @@ -18,7 +18,7 @@ class TemplateTextSensor : public text_sensor::TextSensor, public PollingCompone void dump_config() override; protected: - optional()>> f_{}; + optional (*)()> f_{}; }; } // namespace template_ diff --git a/esphome/components/template/valve/template_valve.cpp b/esphome/components/template/valve/template_valve.cpp index 5fa14a2de7..b27cc00968 100644 --- a/esphome/components/template/valve/template_valve.cpp +++ b/esphome/components/template/valve/template_valve.cpp @@ -55,7 +55,7 @@ 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(std::function()> &&f) { this->state_f_ = f; } +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 5e3fb6aff3..92c32f3487 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -17,7 +17,7 @@ class TemplateValve : public valve::Valve, public Component { public: TemplateValve(); - void set_state_lambda(std::function()> &&f); + void set_state_lambda(optional (*f)()); Trigger<> *get_open_trigger() const; Trigger<> *get_close_trigger() const; Trigger<> *get_stop_trigger() const; @@ -42,7 +42,7 @@ class TemplateValve : public valve::Valve, public Component { void stop_prev_trigger_(); TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE}; - optional()>> state_f_; + optional (*)()> state_f_; bool assumed_state_{false}; bool optimistic_{false}; Trigger<> *open_trigger_; diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index b2022c7ae6..a2da424e5a 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -198,6 +198,8 @@ class LambdaExpression(Expression): self.return_type = safe_exp(return_type) if return_type is not None else None def __str__(self): + # Stateless lambdas (empty capture) implicitly convert to function pointers + # when assigned to function pointer types - no unary + needed cpp = f"[{self.capture}]({self.parameters})" if self.return_type is not None: cpp += f" -> {self.return_type}" @@ -700,6 +702,12 @@ async def process_lambda( parts[i * 3 + 1] = var parts[i * 3 + 2] = "" + # All id() references are global variables in generated C++ code. + # Global variables should not be captured - they're accessible everywhere. + # Use empty capture instead of capture-by-value. + if capture == "=": + capture = "" + if isinstance(value, ESPHomeDataBase) and value.esp_range is not None: location = value.esp_range.start_mark location.line += value.content_offset diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 75f1c4b88b..1cc31a288b 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -58,7 +58,7 @@ def test_text_config_value_mode_set(generate_main): def test_text_config_lamda_is_set(generate_main): """ - Test if lambda is set for lambda mode + Test if lambda is set for lambda mode (optimized with stateless lambda) """ # Given @@ -66,5 +66,6 @@ def test_text_config_lamda_is_set(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert "it_4->set_template([=]() -> esphome::optional {" in main_cpp + # Stateless lambda optimization: empty capture list allows function pointer conversion + assert "it_4->set_template([]() -> esphome::optional {" in main_cpp assert 'return std::string{"Hello"};' in main_cpp diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 95633ca0c6..2c9f760c8e 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -173,6 +173,61 @@ class TestLambdaExpression: "}" ) + def test_str__stateless_no_return(self): + """Test stateless lambda (empty capture) generates correctly""" + target = cg.LambdaExpression( + ('ESP_LOGD("main", "Test message");',), + (), # No parameters + "", # Empty capture (stateless) + ) + + actual = str(target) + + assert actual == ('[]() {\n ESP_LOGD("main", "Test message");\n}') + + def test_str__stateless_with_return(self): + """Test stateless lambda with return type generates correctly""" + target = cg.LambdaExpression( + ("return global_value > 0;",), + (), # No parameters + "", # Empty capture (stateless) + bool, # Return type + ) + + actual = str(target) + + assert actual == ("[]() -> bool {\n return global_value > 0;\n}") + + def test_str__stateless_with_params(self): + """Test stateless lambda with parameters generates correctly""" + target = cg.LambdaExpression( + ("return foo + bar;",), + ((int, "foo"), (float, "bar")), + "", # Empty capture (stateless) + float, + ) + + actual = str(target) + + assert actual == ( + "[](int32_t foo, float bar) -> float {\n return foo + bar;\n}" + ) + + def test_str__with_capture(self): + """Test lambda with capture generates correctly""" + target = cg.LambdaExpression( + ("return captured_var + x;",), + ((int, "x"),), + "captured_var", # Has capture (not stateless) + int, + ) + + actual = str(target) + + assert actual == ( + "[captured_var](int32_t x) -> int32_t {\n return captured_var + x;\n}" + ) + class TestLiterals: @pytest.mark.parametrize(