diff --git a/esphome/automation.py b/esphome/automation.py index 7b1d6ceca13..94d64086ec0 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -250,7 +250,9 @@ async def and_condition_to_code( args: TemplateArgsType, ) -> MockObj: conditions = await build_condition_list(config, template_arg, args) - return cg.new_Pvariable(condition_id, template_arg, conditions) + return cg.new_Pvariable( + condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions + ) @register_condition("or", OrCondition, validate_condition_list) @@ -261,7 +263,9 @@ async def or_condition_to_code( args: TemplateArgsType, ) -> MockObj: conditions = await build_condition_list(config, template_arg, args) - return cg.new_Pvariable(condition_id, template_arg, conditions) + return cg.new_Pvariable( + condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions + ) @register_condition("all", AndCondition, validate_condition_list) @@ -272,7 +276,9 @@ async def all_condition_to_code( args: TemplateArgsType, ) -> MockObj: conditions = await build_condition_list(config, template_arg, args) - return cg.new_Pvariable(condition_id, template_arg, conditions) + return cg.new_Pvariable( + condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions + ) @register_condition("any", OrCondition, validate_condition_list) @@ -283,7 +289,9 @@ async def any_condition_to_code( args: TemplateArgsType, ) -> MockObj: conditions = await build_condition_list(config, template_arg, args) - return cg.new_Pvariable(condition_id, template_arg, conditions) + return cg.new_Pvariable( + condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions + ) @register_condition("not", NotCondition, validate_potentially_and_condition) @@ -305,7 +313,9 @@ async def xor_condition_to_code( args: TemplateArgsType, ) -> MockObj: conditions = await build_condition_list(config, template_arg, args) - return cg.new_Pvariable(condition_id, template_arg, conditions) + return cg.new_Pvariable( + condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions + ) @register_condition("lambda", LambdaCondition, cv.returning_lambda) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 4705f1675d5..660f75ccd9e 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -124,9 +124,10 @@ ClickTrigger = binary_sensor_ns.class_("ClickTrigger", automation.Trigger.templa DoubleClickTrigger = binary_sensor_ns.class_( "DoubleClickTrigger", automation.Trigger.template() ) -MultiClickTrigger = binary_sensor_ns.class_( - "MultiClickTrigger", automation.Trigger.template(), cg.Component +MultiClickTriggerBase = binary_sensor_ns.class_( + "MultiClickTriggerBase", automation.Trigger.template(), cg.Component ) +MultiClickTrigger = binary_sensor_ns.class_("MultiClickTrigger", MultiClickTriggerBase) MultiClickTriggerEvent = binary_sensor_ns.struct("MultiClickTriggerEvent") BinarySensorPublishAction = binary_sensor_ns.class_( @@ -255,6 +256,7 @@ async def delayed_off_filter_to_code(config, filter_id): ): cv.positive_time_period_milliseconds, } ), + cv.Length(max=254), ), ) async def autorepeat_filter_to_code(config, filter_id): @@ -283,7 +285,7 @@ async def autorepeat_filter_to_code(config, filter_id): ), ) ] - var = cg.new_Pvariable(filter_id, timings) + var = cg.new_Pvariable(filter_id, cg.TemplateArguments(len(timings)), timings) await cg.register_component(var, {}) return var @@ -483,7 +485,9 @@ _BINARY_SENSOR_SCHEMA = ( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MultiClickTrigger), cv.Required(CONF_TIMING): cv.All( - [parse_multi_click_timing_str], validate_multi_click_timing + [parse_multi_click_timing_str], + validate_multi_click_timing, + cv.Length(min=1, max=255), ), cv.Optional( CONF_INVALID_COOLDOWN, default="1s" @@ -560,7 +564,9 @@ async def _build_binary_sensor_automations(var, config): ) for tim in conf[CONF_TIMING] ] - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, timings) + trigger = cg.new_Pvariable( + conf[CONF_TRIGGER_ID], cg.TemplateArguments(len(timings)), var, timings + ) if CONF_INVALID_COOLDOWN in conf: cg.add(trigger.set_invalid_cooldown(conf[CONF_INVALID_COOLDOWN])) await cg.register_component(trigger, conf) diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index 7e43d42357c..eb68abce3b7 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -13,7 +13,7 @@ constexpr uint32_t MULTICLICK_COOLDOWN_ID = 1; constexpr uint32_t MULTICLICK_IS_VALID_ID = 2; constexpr uint32_t MULTICLICK_IS_NOT_VALID_ID = 3; -void MultiClickTrigger::on_state_(bool state) { +void MultiClickTriggerBase::on_state_(bool state) { // Handle duplicate events if (state == this->last_state_) { return; @@ -32,7 +32,7 @@ void MultiClickTrigger::on_state_(bool state) { ESP_LOGV(TAG, "START min=%" PRIu32 " max=%" PRIu32, evt.min_length, evt.max_length); ESP_LOGV(TAG, "Multi Click: Starting multi click action!"); this->at_index_ = 1; - if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) { + if (this->timing_count_ == 1 && evt.max_length == 4294967294UL) { this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); }); } else { this->schedule_is_valid_(evt.min_length); @@ -50,7 +50,7 @@ void MultiClickTrigger::on_state_(bool state) { return; } - if (*this->at_index_ == this->timing_.size()) { + if (*this->at_index_ == this->timing_count_) { this->trigger_(); return; } @@ -61,7 +61,7 @@ void MultiClickTrigger::on_state_(bool state) { ESP_LOGV(TAG, "A i=%zu min=%" PRIu32 " max=%" PRIu32, *this->at_index_, evt.min_length, evt.max_length); // NOLINT this->schedule_is_valid_(evt.min_length); this->schedule_is_not_valid_(evt.max_length); - } else if (*this->at_index_ + 1 != this->timing_.size()) { + } else if (*this->at_index_ + 1 != this->timing_count_) { ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID); this->schedule_is_valid_(evt.min_length); @@ -74,7 +74,7 @@ void MultiClickTrigger::on_state_(bool state) { *this->at_index_ = *this->at_index_ + 1; } -void MultiClickTrigger::schedule_cooldown_() { +void MultiClickTriggerBase::schedule_cooldown_() { ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_); this->is_in_cooldown_ = true; this->set_timeout(MULTICLICK_COOLDOWN_ID, this->invalid_cooldown_, [this]() { @@ -86,7 +86,7 @@ void MultiClickTrigger::schedule_cooldown_() { this->cancel_timeout(MULTICLICK_IS_VALID_ID); this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID); } -void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { +void MultiClickTriggerBase::schedule_is_valid_(uint32_t min_length) { if (min_length == 0) { this->is_valid_ = true; return; @@ -97,19 +97,19 @@ void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { this->is_valid_ = true; }); } -void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) { +void MultiClickTriggerBase::schedule_is_not_valid_(uint32_t max_length) { this->set_timeout(MULTICLICK_IS_NOT_VALID_ID, max_length, [this]() { ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS"); this->is_valid_ = false; this->schedule_cooldown_(); }); } -void MultiClickTrigger::cancel() { +void MultiClickTriggerBase::cancel() { ESP_LOGV(TAG, "Multi Click: Sequence explicitly cancelled."); this->is_valid_ = false; this->schedule_cooldown_(); } -void MultiClickTrigger::trigger_() { +void MultiClickTriggerBase::trigger_() { ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!"); this->at_index_.reset(); this->cancel_timeout(MULTICLICK_TRIGGER_ID); diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index f30f9d32792..1875910affd 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -89,10 +90,10 @@ class DoubleClickTrigger : public Trigger<> { uint32_t max_length_; /// Maximum length of click. 0 means no maximum. }; -class MultiClickTrigger : public Trigger<>, public Component { +/// Non-template base for MultiClickTrigger (keeps large method bodies out of the header). +class MultiClickTriggerBase : public Trigger<>, public Component { public: - explicit MultiClickTrigger(BinarySensor *parent, std::initializer_list timing) - : parent_(parent), timing_(timing) {} + explicit MultiClickTriggerBase(BinarySensor *parent) : parent_(parent) {} void setup() override { this->last_state_ = this->parent_->get_state_default(false); @@ -104,6 +105,8 @@ class MultiClickTrigger : public Trigger<>, public Component { void set_invalid_cooldown(uint32_t invalid_cooldown) { this->invalid_cooldown_ = invalid_cooldown; } void cancel(); + MultiClickTriggerBase(const MultiClickTriggerBase &) = delete; + MultiClickTriggerBase &operator=(const MultiClickTriggerBase &) = delete; protected: void on_state_(bool state); @@ -113,14 +116,30 @@ class MultiClickTrigger : public Trigger<>, public Component { void trigger_(); BinarySensor *parent_; - FixedVector timing_; + const MultiClickTriggerEvent *timing_{nullptr}; uint32_t invalid_cooldown_{1000}; optional at_index_{}; + uint8_t timing_count_{0}; bool last_state_{false}; bool is_in_cooldown_{false}; bool is_valid_{false}; }; +/// Template wrapper that provides inline std::array storage for timing events. +/// N is set by code generation to match the exact number of timing events configured in YAML. +template class MultiClickTrigger : public MultiClickTriggerBase { + public: + MultiClickTrigger(BinarySensor *parent, std::initializer_list timing) + : MultiClickTriggerBase(parent) { + init_array_from(this->timing_storage_, timing); + this->timing_ = this->timing_storage_.data(); + this->timing_count_ = N; + } + + protected: + std::array timing_storage_{}; +}; + class StateTrigger : public Trigger { public: explicit StateTrigger(BinarySensor *parent) { diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp index 5d525e967db..914060ce138 100644 --- a/esphome/components/binary_sensor/filter.cpp +++ b/esphome/components/binary_sensor/filter.cpp @@ -76,14 +76,11 @@ float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARD optional InvertFilter::new_value(bool value) { return !value; } -AutorepeatFilter::AutorepeatFilter(std::initializer_list timings) : timings_(timings) {} - -optional AutorepeatFilter::new_value(bool value) { +// AutorepeatFilterBase +optional AutorepeatFilterBase::new_value(bool value) { if (value) { - // Ignore if already running if (this->active_timing_ != 0) return {}; - this->next_timing_(); return true; } else { @@ -94,34 +91,26 @@ optional AutorepeatFilter::new_value(bool value) { } } -void AutorepeatFilter::next_timing_() { - // Entering this method - // 1st time: starts waiting the first delay - // 2nd time: starts waiting the second delay and starts toggling with the first time_off / _on - // last time: no delay to start but have to bump the index to reflect the last - if (this->active_timing_ < this->timings_.size()) { +void AutorepeatFilterBase::next_timing_() { + if (this->active_timing_ < this->timings_count_) { this->set_timeout(AUTOREPEAT_TIMING_ID, this->timings_[this->active_timing_].delay, [this]() { this->next_timing_(); }); } - - if (this->active_timing_ <= this->timings_.size()) { + if (this->active_timing_ <= this->timings_count_) { this->active_timing_++; } - if (this->active_timing_ == 2) this->next_value_(false); - - // Leaving this method: if the toggling is started, it has to use [active_timing_ - 2] for the intervals } -void AutorepeatFilter::next_value_(bool val) { +void AutorepeatFilterBase::next_value_(bool val) { const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2]; - this->output(val); // This is at least the second one so not initial + this->output(val); this->set_timeout(AUTOREPEAT_ON_OFF_ID, val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); }); } -float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; } +float AutorepeatFilterBase::get_setup_priority() const { return setup_priority::HARDWARE; } LambdaFilter::LambdaFilter(std::function(bool)> f) : f_(std::move(f)) {} diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 0813847ca21..37c6bf0092c 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -3,6 +3,8 @@ #include "esphome/core/defines.h" #ifdef USE_BINARY_SENSOR_FILTER +#include + #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -86,22 +88,39 @@ struct AutorepeatFilterTiming { uint32_t time_on; }; -class AutorepeatFilter : public Filter, public Component { +/// Non-template base for AutorepeatFilter — all methods in filter.cpp. +/// Lambdas capture this base pointer, so set_timeout/cancel_timeout are instantiated once. +class AutorepeatFilterBase : public Filter, public Component { public: - explicit AutorepeatFilter(std::initializer_list timings); - optional new_value(bool value) override; - float get_setup_priority() const override; + AutorepeatFilterBase(const AutorepeatFilterBase &) = delete; + AutorepeatFilterBase &operator=(const AutorepeatFilterBase &) = delete; protected: + AutorepeatFilterBase() = default; void next_timing_(); void next_value_(bool val); - FixedVector timings_; + const AutorepeatFilterTiming *timings_{nullptr}; + uint8_t timings_count_{0}; uint8_t active_timing_{0}; }; +/// Template wrapper that provides inline std::array storage for timings. +/// N is set by code generation to match the exact number of timings configured in YAML. +template class AutorepeatFilter : public AutorepeatFilterBase { + public: + explicit AutorepeatFilter(std::initializer_list timings) { + init_array_from(this->timings_storage_, timings); + this->timings_ = this->timings_storage_.data(); + this->timings_count_ = N; + } + + protected: + std::array timings_storage_{}; +}; + class LambdaFilter : public Filter { public: explicit LambdaFilter(std::function(bool)> f); diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 5569567de12..8abba17ff9f 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -620,7 +620,7 @@ async def delta_filter_to_code(config, filter_id): @FILTER_REGISTRY.register("or", OrFilter, validate_filters) async def or_filter_to_code(config, filter_id): filters = await build_filters(config) - return cg.new_Pvariable(filter_id, filters) + return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(filters)), filters) @FILTER_REGISTRY.register( @@ -808,7 +808,7 @@ async def calibrate_polynomial_filter_to_code(config, filter_id): # Column vector b = [[v] for v in y] res = [v[0] for v in _lstsq(a, b)] - return cg.new_Pvariable(filter_id, res) + return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(res)), res) def validate_clamp(config): diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 66a9e9555bc..7b7a968f48f 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -295,32 +295,20 @@ optional DeltaFilter::new_value(float value) { return {}; } -// OrFilter -OrFilter::OrFilter(std::initializer_list filters) : filters_(filters), phi_(this) {} -OrFilter::PhiNode::PhiNode(OrFilter *or_parent) : or_parent_(or_parent) {} - -optional OrFilter::PhiNode::new_value(float value) { - if (!this->or_parent_->has_value_) { - this->or_parent_->output(value); - this->or_parent_->has_value_ = true; +// OrFilter helpers +void or_filter_initialize(Filter **filters, size_t count, Sensor *parent, Filter *phi) { + for (size_t i = 0; i < count; i++) { + filters[i]->initialize(parent, phi); } - - return {}; + phi->initialize(parent, nullptr); } -optional OrFilter::new_value(float value) { - this->has_value_ = false; - for (auto *filter : this->filters_) - filter->input(value); +optional or_filter_new_value(Filter **filters, size_t count, float value, bool &has_value) { + has_value = false; + for (size_t i = 0; i < count; i++) + filters[i]->input(value); return {}; } -void OrFilter::initialize(Sensor *parent, Filter *next) { - Filter::initialize(parent, next); - for (auto *filter : this->filters_) { - filter->initialize(parent, &this->phi_); - } - this->phi_.initialize(parent, nullptr); -} // TimeoutFilterBase - shared loop logic void TimeoutFilterBase::loop() { @@ -408,14 +396,11 @@ optional CalibrateLinearFilter::new_value(float value) { return NAN; } -CalibratePolynomialFilter::CalibratePolynomialFilter(std::initializer_list coefficients) - : coefficients_(coefficients) {} - -optional CalibratePolynomialFilter::new_value(float value) { +optional calibrate_polynomial_compute(const float *coefficients, size_t count, float value) { float res = 0.0f; float x = 1.0f; - for (const auto &coefficient : this->coefficients_) { - res += x * coefficient; + for (size_t i = 0; i < count; i++) { + res += x * coefficients[i]; x *= value; } return res; diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 80fa14742c2..26a03acde57 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -489,26 +489,42 @@ class DeltaFilter : public Filter { float last_value_{NAN}; }; -class OrFilter : public Filter { +/// Non-template helpers for OrFilter (implementation in filter.cpp) +void or_filter_initialize(Filter **filters, size_t count, Sensor *parent, Filter *phi); +optional or_filter_new_value(Filter **filters, size_t count, float value, bool &has_value); + +/// N is set by code generation to match the exact number of filters configured in YAML. +template class OrFilter : public Filter { public: - explicit OrFilter(std::initializer_list filters); + explicit OrFilter(std::initializer_list filters) { init_array_from(this->filters_, filters); } - void initialize(Sensor *parent, Filter *next) override; + void initialize(Sensor *parent, Filter *next) override { + Filter::initialize(parent, next); + or_filter_initialize(this->filters_.data(), N, parent, &this->phi_); + } - optional new_value(float value) override; + optional new_value(float value) override { + return or_filter_new_value(this->filters_.data(), N, value, this->has_value_); + } protected: class PhiNode : public Filter { public: - PhiNode(OrFilter *or_parent); - optional new_value(float value) override; + PhiNode(OrFilter *or_parent) : or_parent_(or_parent) {} + optional new_value(float value) override { + if (!this->or_parent_->has_value_) { + this->or_parent_->output(value); + this->or_parent_->has_value_ = true; + } + return {}; + } protected: OrFilter *or_parent_; }; - FixedVector filters_; - PhiNode phi_; + std::array filters_{}; + PhiNode phi_{this}; bool has_value_{false}; }; @@ -521,13 +537,21 @@ class CalibrateLinearFilter : public Filter { FixedVector> linear_functions_; }; -class CalibratePolynomialFilter : public Filter { +/// Non-template helper for polynomial calibration (implementation in filter.cpp) +optional calibrate_polynomial_compute(const float *coefficients, size_t count, float value); + +/// N is set by code generation to match the exact number of polynomial coefficients. +template class CalibratePolynomialFilter : public Filter { public: - explicit CalibratePolynomialFilter(std::initializer_list coefficients); - optional new_value(float value) override; + explicit CalibratePolynomialFilter(std::initializer_list coefficients) { + init_array_from(this->coefficients_, coefficients); + } + optional new_value(float value) override { + return calibrate_polynomial_compute(this->coefficients_.data(), N, value); + } protected: - FixedVector coefficients_; + std::array coefficients_{}; }; class ClampFilter : public Filter { diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 51eedf9a956..5b07dd29156 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -116,7 +116,9 @@ async def substitute_filter_to_code(config, filter_id): ) for conf in config ] - return cg.new_Pvariable(filter_id, substitutions) + return cg.new_Pvariable( + filter_id, cg.TemplateArguments(len(substitutions)), substitutions + ) @FILTER_REGISTRY.register("map", MapFilter, cv.ensure_list(validate_mapping)) @@ -129,7 +131,7 @@ async def map_filter_to_code(config, filter_id): ) for conf in config ] - return cg.new_Pvariable(filter_id, mappings) + return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(mappings)), mappings) validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") diff --git a/esphome/components/text_sensor/filter.cpp b/esphome/components/text_sensor/filter.cpp index f7c6a695fba..d4e6b5b9bbf 100644 --- a/esphome/components/text_sensor/filter.cpp +++ b/esphome/components/text_sensor/filter.cpp @@ -73,37 +73,29 @@ bool PrependFilter::new_value(std::string &value) { return true; } -// Substitute -SubstituteFilter::SubstituteFilter(const std::initializer_list &substitutions) - : substitutions_(substitutions) {} - -bool SubstituteFilter::new_value(std::string &value) { - for (const auto &sub : this->substitutions_) { - // Compute lengths once per substitution (strlen is fast, called infrequently) - const size_t from_len = strlen(sub.from); - const size_t to_len = strlen(sub.to); +// Substitute — non-template helper +bool substitute_filter_apply(const Substitution *substitutions, size_t count, std::string &value) { + for (size_t i = 0; i < count; i++) { + const size_t from_len = strlen(substitutions[i].from); + const size_t to_len = strlen(substitutions[i].to); std::size_t pos = 0; - while ((pos = value.find(sub.from, pos, from_len)) != std::string::npos) { - value.replace(pos, from_len, sub.to, to_len); - // Advance past the replacement to avoid infinite loop when - // the replacement contains the search pattern (e.g., f -> foo) + while ((pos = value.find(substitutions[i].from, pos, from_len)) != std::string::npos) { + value.replace(pos, from_len, substitutions[i].to, to_len); pos += to_len; } } return true; } -// Map -MapFilter::MapFilter(const std::initializer_list &mappings) : mappings_(mappings) {} - -bool MapFilter::new_value(std::string &value) { - for (const auto &mapping : this->mappings_) { - if (value == mapping.from) { - value.assign(mapping.to); +// Map — non-template helper +bool map_filter_apply(const Substitution *mappings, size_t count, std::string &value) { + for (size_t i = 0; i < count; i++) { + if (value == mappings[i].from) { + value.assign(mappings[i].to); return true; } } - return true; // Pass through if no match + return true; } } // namespace esphome::text_sensor diff --git a/esphome/components/text_sensor/filter.h b/esphome/components/text_sensor/filter.h index 8a8bc55c8e1..6db76dcb640 100644 --- a/esphome/components/text_sensor/filter.h +++ b/esphome/components/text_sensor/filter.h @@ -3,6 +3,8 @@ #include "esphome/core/defines.h" #ifdef USE_TEXT_SENSOR_FILTER +#include + #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -121,16 +123,25 @@ struct Substitution { const char *to; }; -/// A simple filter that replaces a substring with another substring -class SubstituteFilter : public Filter { +/// Non-template helper (implementation in filter.cpp) +bool substitute_filter_apply(const Substitution *substitutions, size_t count, std::string &value); + +/// A simple filter that replaces a substring with another substring. +/// N is set by code generation to match the exact number of substitutions configured in YAML. +template class SubstituteFilter : public Filter { public: - explicit SubstituteFilter(const std::initializer_list &substitutions); - bool new_value(std::string &value) override; + explicit SubstituteFilter(const std::initializer_list &substitutions) { + init_array_from(this->substitutions_, substitutions); + } + bool new_value(std::string &value) override { return substitute_filter_apply(this->substitutions_.data(), N, value); } protected: - FixedVector substitutions_; + std::array substitutions_{}; }; +/// Non-template helper (implementation in filter.cpp) +bool map_filter_apply(const Substitution *mappings, size_t count, std::string &value); + /** A filter that maps values from one set to another * * Uses linear search instead of std::map for typical small datasets (2-20 mappings). @@ -154,14 +165,18 @@ class SubstituteFilter : public Filter { * - Faster for typical ESPHome usage (2-10 mappings common, 20+ rare) * * Break-even point: ~35-40 mappings, but ESPHome configs rarely exceed 20 + * + * N is set by code generation to match the exact number of mappings configured in YAML. */ -class MapFilter : public Filter { +template class MapFilter : public Filter { public: - explicit MapFilter(const std::initializer_list &mappings); - bool new_value(std::string &value) override; + explicit MapFilter(const std::initializer_list &mappings) { + init_array_from(this->mappings_, mappings); + } + bool new_value(std::string &value) override { return map_filter_apply(this->mappings_.data(), N, value); } protected: - FixedVector mappings_; + std::array mappings_{}; }; } // namespace esphome::text_sensor diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index efcffa8824d..11133d39739 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -9,14 +9,17 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" +#include #include #include namespace esphome { -template class AndCondition : public Condition { +template class AndCondition : public Condition { public: - explicit AndCondition(std::initializer_list *> conditions) : conditions_(conditions) {} + explicit AndCondition(std::initializer_list *> conditions) { + init_array_from(this->conditions_, conditions); + } bool check(const Ts &...x) override { for (auto *condition : this->conditions_) { if (!condition->check(x...)) @@ -27,12 +30,14 @@ template class AndCondition : public Condition { } protected: - FixedVector *> conditions_; + std::array *, N> conditions_{}; }; -template class OrCondition : public Condition { +template class OrCondition : public Condition { public: - explicit OrCondition(std::initializer_list *> conditions) : conditions_(conditions) {} + explicit OrCondition(std::initializer_list *> conditions) { + init_array_from(this->conditions_, conditions); + } bool check(const Ts &...x) override { for (auto *condition : this->conditions_) { if (condition->check(x...)) @@ -43,7 +48,7 @@ template class OrCondition : public Condition { } protected: - FixedVector *> conditions_; + std::array *, N> conditions_{}; }; template class NotCondition : public Condition { @@ -55,9 +60,11 @@ template class NotCondition : public Condition { Condition *condition_; }; -template class XorCondition : public Condition { +template class XorCondition : public Condition { public: - explicit XorCondition(std::initializer_list *> conditions) : conditions_(conditions) {} + explicit XorCondition(std::initializer_list *> conditions) { + init_array_from(this->conditions_, conditions); + } bool check(const Ts &...x) override { size_t result = 0; for (auto *condition : this->conditions_) { @@ -68,7 +75,7 @@ template class XorCondition : public Condition { } protected: - FixedVector *> conditions_; + std::array *, N> conditions_{}; }; template class LambdaCondition : public Condition { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 913614f5641..66ba166445f 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -500,7 +500,8 @@ template::max()> /// Initialize a std::array from an initializer_list. Uses memcpy for trivially copyable types (optimal codegen), /// falls back to element-wise copy for non-trivially copyable types (e.g. TemplatableValue). -/// N is set by code generation; assert catches mismatches in debug/integration tests. +/// N is always set by code generation — the caller is responsible for ensuring src.size() == N. +/// The debug assert is a safety net for development, not a runtime check. template inline void init_array_from(std::array &dest, std::initializer_list src) { #ifdef ESPHOME_DEBUG assert(src.size() == N);