Compare commits

..

13 Commits

Author SHA1 Message Date
J. Nick Koston
9913d7d9e9 Merge branch 'determine_jobs_comp_split' into determine_jobs_comp_split_test 2025-10-29 19:01:26 -05:00
J. Nick Koston
119dc8b530 flatten 2025-10-29 19:01:12 -05:00
J. Nick Koston
3c8f4c1d5d Merge branch 'determine_jobs_comp_split' into determine_jobs_comp_split_test 2025-10-29 18:58:30 -05:00
J. Nick Koston
4892638768 flatten 2025-10-29 18:58:07 -05:00
J. Nick Koston
306448c02c Merge branch 'determine_jobs_comp_split' into determine_jobs_comp_split_test 2025-10-29 18:54:31 -05:00
J. Nick Koston
eb73a23ab1 flatten 2025-10-29 18:53:50 -05:00
J. Nick Koston
22cb0fbb70 flatten 2025-10-29 18:53:35 -05:00
J. Nick Koston
04d22becd0 Merge branch 'determine_jobs_comp_split' into determine_jobs_comp_split_test 2025-10-29 18:48:14 -05:00
J. Nick Koston
36a7951f7a fix 2025-10-29 18:48:00 -05:00
J. Nick Koston
81382db3d0 DNM: ci testing 2025-10-29 18:45:29 -05:00
J. Nick Koston
e0b02ccfc5 [ci] Consolidate component splitting into determine-jobs 2025-10-29 18:43:49 -05:00
Stuart Parmenter
918650f15a [lvgl] memset canvas buffer to prevent display of random garbage (#11582)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2025-10-29 21:06:45 +00:00
Stuart Parmenter
287f65cbaf [lvgl] fix typo from previous refactor (#11596) 2025-10-30 07:27:31 +11:00
35 changed files with 192 additions and 345 deletions

View File

@@ -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

View File

@@ -1,3 +1,5 @@
// random comment
#include "api_connection.h"
#ifdef USE_API
#ifdef USE_API_NOISE

View File

@@ -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;
}

View File

@@ -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()

View File

@@ -6,21 +6,17 @@ namespace template_ {
static const char *const TAG = "template.binary_sensor";
void TemplateBinarySensor::setup() {
if (!this->f_.has_value()) {
this->disable_loop();
} else {
this->loop();
}
}
void TemplateBinarySensor::setup() { this->loop(); }
void TemplateBinarySensor::loop() {
auto s = this->f_();
if (!this->f_.has_value())
return;
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_

View File

@@ -1,7 +1,6 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/template_lambda.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome {
@@ -9,10 +8,7 @@ namespace template_ {
class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor {
public:
template<typename F> void set_template(F &&f) {
this->f_.set(std::forward<F>(f));
this->enable_loop();
}
void set_template(optional<bool> (*f)()) { this->f_ = f; }
void setup() override;
void loop() override;
@@ -21,7 +17,7 @@ class TemplateBinarySensor : public Component, public binary_sensor::BinarySenso
float get_setup_priority() const override { return setup_priority::HARDWARE; }
protected:
TemplateLambda<bool> f_;
optional<optional<bool> (*)()> f_;
};
} // namespace template_

View File

@@ -33,27 +33,28 @@ void TemplateCover::setup() {
break;
}
}
if (!this->state_f_.has_value() && !this->tilt_f_.has_value())
this->disable_loop();
}
void TemplateCover::loop() {
bool changed = false;
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->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 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;
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;
}
}
}
@@ -62,6 +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(optional<float> (*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_; }
@@ -122,6 +124,7 @@ CoverTraits TemplateCover::get_traits() {
}
Trigger<float> *TemplateCover::get_position_trigger() const { return this->position_trigger_; }
Trigger<float> *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; }
void TemplateCover::set_tilt_lambda(optional<float> (*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; }

View File

@@ -2,7 +2,6 @@
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/core/template_lambda.h"
#include "esphome/components/cover/cover.h"
namespace esphome {
@@ -18,14 +17,7 @@ class TemplateCover : public cover::Cover, public Component {
public:
TemplateCover();
template<typename F> void set_state_lambda(F &&f) {
this->state_f_.set(std::forward<F>(f));
this->enable_loop();
}
template<typename F> void set_tilt_lambda(F &&f) {
this->tilt_f_.set(std::forward<F>(f));
this->enable_loop();
}
void set_state_lambda(optional<float> (*f)());
Trigger<> *get_open_trigger() const;
Trigger<> *get_close_trigger() const;
Trigger<> *get_stop_trigger() const;
@@ -34,6 +26,7 @@ class TemplateCover : public cover::Cover, public Component {
Trigger<float> *get_tilt_trigger() const;
void set_optimistic(bool optimistic);
void set_assumed_state(bool assumed_state);
void set_tilt_lambda(optional<float> (*tilt_f)());
void set_has_stop(bool has_stop);
void set_has_position(bool has_position);
void set_has_tilt(bool has_tilt);
@@ -52,8 +45,8 @@ class TemplateCover : public cover::Cover, public Component {
void stop_prev_trigger_();
TemplateCoverRestoreMode restore_mode_{COVER_RESTORE};
TemplateLambda<float> state_f_;
TemplateLambda<float> tilt_f_;
optional<optional<float> (*)()> state_f_;
optional<optional<float> (*)()> tilt_f_;
bool assumed_state_{false};
bool optimistic_{false};
Trigger<> *open_trigger_;

View File

@@ -40,13 +40,14 @@ void TemplateDate::update() {
if (!this->f_.has_value())
return;
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();
}
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();
}
void TemplateDate::control(const datetime::DateCall &call) {

View File

@@ -9,14 +9,13 @@
#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:
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
void set_template(optional<ESPTime> (*f)()) { this->f_ = f; }
void setup() override;
void update() override;
@@ -36,7 +35,7 @@ class TemplateDate : public datetime::DateEntity, public PollingComponent {
ESPTime initial_value_{};
bool restore_value_{false};
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
TemplateLambda<ESPTime> f_;
optional<optional<ESPTime> (*)()> f_;
ESPPreferenceObject pref_;
};

View File

@@ -43,16 +43,17 @@ void TemplateDateTime::update() {
if (!this->f_.has_value())
return;
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();
}
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();
}
void TemplateDateTime::control(const datetime::DateTimeCall &call) {

View File

@@ -9,14 +9,13 @@
#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:
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
void set_template(optional<ESPTime> (*f)()) { this->f_ = f; }
void setup() override;
void update() override;
@@ -36,7 +35,7 @@ class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponen
ESPTime initial_value_{};
bool restore_value_{false};
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
TemplateLambda<ESPTime> f_;
optional<optional<ESPTime> (*)()> f_;
ESPPreferenceObject pref_;
};

View File

@@ -40,13 +40,14 @@ void TemplateTime::update() {
if (!this->f_.has_value())
return;
auto val = this->f_();
if (val.has_value()) {
this->hour_ = val->hour;
this->minute_ = val->minute;
this->second_ = val->second;
this->publish_state();
}
auto val = (*this->f_)();
if (!val.has_value())
return;
this->hour_ = val->hour;
this->minute_ = val->minute;
this->second_ = val->second;
this->publish_state();
}
void TemplateTime::control(const datetime::TimeCall &call) {

View File

@@ -9,14 +9,13 @@
#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:
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
void set_template(optional<ESPTime> (*f)()) { this->f_ = f; }
void setup() override;
void update() override;
@@ -36,7 +35,7 @@ class TemplateTime : public datetime::TimeEntity, public PollingComponent {
ESPTime initial_value_{};
bool restore_value_{false};
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
TemplateLambda<ESPTime> f_;
optional<optional<ESPTime> (*)()> f_;
ESPPreferenceObject pref_;
};

View File

@@ -12,10 +12,13 @@ TemplateLock::TemplateLock()
: lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {}
void TemplateLock::loop() {
auto val = this->f_();
if (val.has_value()) {
this->publish_state(*val);
}
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->publish_state(*val);
}
void TemplateLock::control(const lock::LockCall &call) {
if (this->prev_trigger_ != nullptr) {
@@ -42,6 +45,7 @@ void TemplateLock::open_latch() {
this->open_trigger_->trigger();
}
void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void TemplateLock::set_state_lambda(optional<lock::LockState> (*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_; }

View File

@@ -2,7 +2,6 @@
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/core/template_lambda.h"
#include "esphome/components/lock/lock.h"
namespace esphome {
@@ -14,10 +13,7 @@ class TemplateLock : public lock::Lock, public Component {
void dump_config() override;
template<typename F> void set_state_lambda(F &&f) {
this->f_.set(std::forward<F>(f));
this->enable_loop();
}
void set_state_lambda(optional<lock::LockState> (*f)());
Trigger<> *get_lock_trigger() const;
Trigger<> *get_unlock_trigger() const;
Trigger<> *get_open_trigger() const;
@@ -30,7 +26,7 @@ class TemplateLock : public lock::Lock, public Component {
void control(const lock::LockCall &call) override;
void open_latch() override;
TemplateLambda<lock::LockState> f_;
optional<optional<lock::LockState> (*)()> f_;
bool optimistic_{false};
Trigger<> *lock_trigger_;
Trigger<> *unlock_trigger_;

View File

@@ -30,10 +30,11 @@ void TemplateNumber::update() {
if (!this->f_.has_value())
return;
auto val = this->f_();
if (val.has_value()) {
this->publish_state(*val);
}
auto val = (*this->f_)();
if (!val.has_value())
return;
this->publish_state(*val);
}
void TemplateNumber::control(float value) {

View File

@@ -4,14 +4,13 @@
#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:
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
void set_template(optional<float> (*f)()) { this->f_ = f; }
void setup() override;
void update() override;
@@ -29,7 +28,7 @@ class TemplateNumber : public number::Number, public PollingComponent {
float initial_value_{NAN};
bool restore_value_{false};
Trigger<float> *set_trigger_ = new Trigger<float>();
TemplateLambda<float> f_;
optional<optional<float> (*)()> f_;
ESPPreferenceObject pref_;
};

View File

@@ -31,14 +31,16 @@ void TemplateSelect::update() {
if (!this->f_.has_value())
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);
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;
}
this->publish_state(*val);
}
void TemplateSelect::control(const std::string &value) {

View File

@@ -4,14 +4,13 @@
#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:
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
void set_template(optional<std::string> (*f)()) { this->f_ = f; }
void setup() override;
void update() override;
@@ -29,7 +28,7 @@ class TemplateSelect : public select::Select, public PollingComponent {
size_t initial_option_index_{0};
bool restore_value_ = false;
Trigger<std::string> *set_trigger_ = new Trigger<std::string>();
TemplateLambda<std::string> f_;
optional<optional<std::string> (*)()> f_;
ESPPreferenceObject pref_;
};

View File

@@ -11,14 +11,13 @@ 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<float> (*f)()) { this->f_ = f; }
void TemplateSensor::dump_config() {
LOG_SENSOR("", "Template Sensor", this);
LOG_UPDATE_INTERVAL(this);

View File

@@ -1,7 +1,6 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/template_lambda.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
@@ -9,7 +8,7 @@ namespace template_ {
class TemplateSensor : public sensor::Sensor, public PollingComponent {
public:
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
void set_template(optional<float> (*f)());
void update() override;
@@ -18,7 +17,7 @@ class TemplateSensor : public sensor::Sensor, public PollingComponent {
float get_setup_priority() const override;
protected:
TemplateLambda<float> f_;
optional<optional<float> (*)()> f_;
};
} // namespace template_

View File

@@ -9,10 +9,13 @@ static const char *const TAG = "template.switch";
TemplateSwitch::TemplateSwitch() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {}
void TemplateSwitch::loop() {
auto s = this->f_();
if (s.has_value()) {
this->publish_state(*s);
}
if (!this->f_.has_value())
return;
auto s = (*this->f_)();
if (!s.has_value())
return;
this->publish_state(*s);
}
void TemplateSwitch::write_state(bool state) {
if (this->prev_trigger_ != nullptr) {
@@ -32,13 +35,11 @@ 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<bool> (*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<bool> initial_state = this->get_initial_state_with_restore_mode();
if (initial_state.has_value()) {

View File

@@ -2,7 +2,6 @@
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/core/template_lambda.h"
#include "esphome/components/switch/switch.h"
namespace esphome {
@@ -15,10 +14,7 @@ class TemplateSwitch : public switch_::Switch, public Component {
void setup() override;
void dump_config() override;
template<typename F> void set_state_lambda(F &&f) {
this->f_.set(std::forward<F>(f));
this->enable_loop();
}
void set_state_lambda(optional<bool> (*f)());
Trigger<> *get_turn_on_trigger() const;
Trigger<> *get_turn_off_trigger() const;
void set_optimistic(bool optimistic);
@@ -32,7 +28,7 @@ class TemplateSwitch : public switch_::Switch, public Component {
void write_state(bool state) override;
TemplateLambda<bool> f_;
optional<optional<bool> (*)()> f_;
bool optimistic_{false};
bool assumed_state_{false};
Trigger<> *turn_on_trigger_;

View File

@@ -7,8 +7,10 @@ namespace template_ {
static const char *const TAG = "template.text";
void TemplateText::setup() {
if (this->f_.has_value())
return;
if (!(this->f_ == nullptr)) {
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());
@@ -24,13 +26,17 @@ 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()) {
this->publish_state(*val);
}
auto val = (*this->f_)();
if (!val.has_value())
return;
this->publish_state(*val);
}
void TemplateText::control(const std::string &value) {

View File

@@ -4,7 +4,6 @@
#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_ {
@@ -62,7 +61,7 @@ template<uint8_t SZ> class TextSaver : public TemplateTextSaverBase {
class TemplateText : public text::Text, public PollingComponent {
public:
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
void set_template(optional<std::string> (*f)()) { this->f_ = f; }
void setup() override;
void update() override;
@@ -79,7 +78,7 @@ class TemplateText : public text::Text, public PollingComponent {
bool optimistic_ = false;
std::string initial_value_;
Trigger<std::string> *set_trigger_ = new Trigger<std::string>();
TemplateLambda<std::string> f_{};
optional<optional<std::string> (*)()> f_{nullptr};
TemplateTextSaverBase *pref_ = nullptr;
};

View File

@@ -10,14 +10,13 @@ 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<std::string> (*f)()) { this->f_ = f; }
void TemplateTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Template Sensor", this); }
} // namespace template_

View File

@@ -2,7 +2,6 @@
#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 {
@@ -10,7 +9,7 @@ namespace template_ {
class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent {
public:
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
void set_template(optional<std::string> (*f)());
void update() override;
@@ -19,7 +18,7 @@ class TemplateTextSensor : public text_sensor::TextSensor, public PollingCompone
void dump_config() override;
protected:
TemplateLambda<std::string> f_{};
optional<optional<std::string> (*)()> f_{};
};
} // namespace template_

View File

@@ -33,19 +33,19 @@ void TemplateValve::setup() {
break;
}
}
if (!this->state_f_.has_value())
this->disable_loop();
}
void TemplateValve::loop() {
bool changed = false;
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->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;
}
}
}
@@ -55,6 +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(optional<float> (*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_; }

View File

@@ -2,7 +2,6 @@
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/core/template_lambda.h"
#include "esphome/components/valve/valve.h"
namespace esphome {
@@ -18,10 +17,7 @@ class TemplateValve : public valve::Valve, public Component {
public:
TemplateValve();
template<typename F> void set_state_lambda(F &&f) {
this->state_f_.set(std::forward<F>(f));
this->enable_loop();
}
void set_state_lambda(optional<float> (*f)());
Trigger<> *get_open_trigger() const;
Trigger<> *get_close_trigger() const;
Trigger<> *get_stop_trigger() const;
@@ -46,7 +42,7 @@ class TemplateValve : public valve::Valve, public Component {
void stop_prev_trigger_();
TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE};
TemplateLambda<float> state_f_;
optional<optional<float> (*)()> state_f_;
bool assumed_state_{false};
bool optimistic_{false};
Trigger<> *open_trigger_;

View File

@@ -1,120 +0,0 @@
#pragma once
#include <functional>
#include <concepts>
#include "esphome/core/optional.h"
namespace esphome {
/** Helper class for template platforms that stores either a stateless lambda (function pointer)
* or a stateful lambda (std::function pointer).
*
* This provides backward compatibility with PR #11555 while maintaining the optimization:
* - Stateless lambdas (no capture) → function pointer (4 bytes on ESP32)
* - Stateful lambdas (with capture) → pointer to std::function (4 bytes on ESP32)
* Total size: enum (1 byte) + union (4 bytes) + padding = 8 bytes (same as PR #11555)
*
* Both lambda types must return optional<T> (as YAML codegen does) to support the pattern:
* return {}; // Don't publish a value
* return 42.0; // Publish this value
*
* operator() returns optional<T>, returning nullopt when no lambda is set (type == NONE).
* This eliminates redundant "is lambda set" checks by reusing optional's discriminator.
*
* @tparam T The return type (e.g., float for TemplateLambda<optional<float>>)
* @tparam Args Optional arguments for the lambda
*/
template<typename T, typename... Args> class TemplateLambda {
public:
TemplateLambda() : type_(NONE) {}
// For stateless lambdas: use function pointer
template<typename F>
requires std::invocable<F, Args...> && std::convertible_to < F, optional<T>(*)
(Args...) > void set(F f) {
this->reset_();
this->type_ = STATELESS_LAMBDA;
this->stateless_f_ = f; // Implicit conversion to function pointer
}
// For stateful lambdas: use std::function pointer
template<typename F>
requires std::invocable<F, Args...> &&
(!std::convertible_to<F, optional<T> (*)(Args...)>) &&std::convertible_to<std::invoke_result_t<F, Args...>,
optional<T>> void set(F &&f) {
this->reset_();
this->type_ = LAMBDA;
this->f_ = new std::function<optional<T>(Args...)>(std::forward<F>(f));
}
~TemplateLambda() { this->reset_(); }
// Copy constructor
TemplateLambda(const TemplateLambda &) = delete;
TemplateLambda &operator=(const TemplateLambda &) = delete;
// Move constructor
TemplateLambda(TemplateLambda &&other) noexcept : type_(other.type_) {
if (type_ == LAMBDA) {
this->f_ = other.f_;
other.f_ = nullptr;
} else if (type_ == STATELESS_LAMBDA) {
this->stateless_f_ = other.stateless_f_;
}
other.type_ = NONE;
}
TemplateLambda &operator=(TemplateLambda &&other) noexcept {
if (this != &other) {
this->reset_();
this->type_ = other.type_;
if (type_ == LAMBDA) {
this->f_ = other.f_;
other.f_ = nullptr;
} else if (type_ == STATELESS_LAMBDA) {
this->stateless_f_ = other.stateless_f_;
}
other.type_ = NONE;
}
return *this;
}
bool has_value() const { return this->type_ != NONE; }
// Returns optional<T>, returning nullopt if no lambda is set
optional<T> operator()(Args... args) {
switch (this->type_) {
case STATELESS_LAMBDA:
return this->stateless_f_(args...); // Direct function pointer call
case LAMBDA:
return (*this->f_)(args...); // std::function call via pointer
case NONE:
default:
return nullopt; // No lambda set
}
}
optional<T> call(Args... args) { return (*this)(args...); }
protected:
void reset_() {
if (this->type_ == LAMBDA) {
delete this->f_;
this->f_ = nullptr;
}
this->type_ = NONE;
}
enum : uint8_t {
NONE,
STATELESS_LAMBDA,
LAMBDA,
} type_;
union {
optional<T> (*stateless_f_)(Args...); // Function pointer (4 bytes on ESP32)
std::function<optional<T>(Args...)> *f_; // Pointer to std::function (4 bytes on ESP32)
};
};
} // namespace esphome

View File

@@ -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

View File

@@ -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

View File

@@ -9,27 +9,6 @@ esphome:
id: template_sens
state: !lambda "return 42.0;"
# Test C++ API: set_template() with stateless lambda (no captures)
- lambda: |-
id(template_sens).set_template([]() -> esphome::optional<float> {
return 123.0f;
});
# Test C++ API: set_template() with stateful lambda (with captures)
# This is the regression test for issue #11555
- lambda: |-
float captured_value = 456.0f;
id(template_sens).set_template([captured_value]() -> esphome::optional<float> {
return captured_value;
});
# Test C++ API: set_template() with more complex capture
- lambda: |-
auto sensor_id = id(template_sens);
id(template_number).set_template([sensor_id]() -> esphome::optional<float> {
return sensor_id->state * 2.0f;
});
- datetime.date.set:
id: test_date
date:
@@ -236,7 +215,6 @@ cover:
number:
- platform: template
id: template_number
name: "Template number"
optimistic: true
min_value: 0

View File

@@ -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(