Merge branch 'dev' into api/peel-first-write-iteration

This commit is contained in:
J. Nick Koston
2026-03-29 15:06:57 -10:00
committed by GitHub
14 changed files with 199 additions and 130 deletions
+15 -5
View File
@@ -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)
+11 -5
View File
@@ -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)
@@ -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);
+23 -4
View File
@@ -1,5 +1,6 @@
#pragma once
#include <array>
#include <cinttypes>
#include <utility>
@@ -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<MultiClickTriggerEvent> 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<MultiClickTriggerEvent> timing_;
const MultiClickTriggerEvent *timing_{nullptr};
uint32_t invalid_cooldown_{1000};
optional<size_t> 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<size_t N> class MultiClickTrigger : public MultiClickTriggerBase {
public:
MultiClickTrigger(BinarySensor *parent, std::initializer_list<MultiClickTriggerEvent> timing)
: MultiClickTriggerBase(parent) {
init_array_from(this->timing_storage_, timing);
this->timing_ = this->timing_storage_.data();
this->timing_count_ = N;
}
protected:
std::array<MultiClickTriggerEvent, N> timing_storage_{};
};
class StateTrigger : public Trigger<bool> {
public:
explicit StateTrigger(BinarySensor *parent) {
+8 -19
View File
@@ -76,14 +76,11 @@ float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARD
optional<bool> InvertFilter::new_value(bool value) { return !value; }
AutorepeatFilter::AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings) : timings_(timings) {}
optional<bool> AutorepeatFilter::new_value(bool value) {
// AutorepeatFilterBase
optional<bool> 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<bool> 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<optional<bool>(bool)> f) : f_(std::move(f)) {}
+24 -5
View File
@@ -3,6 +3,8 @@
#include "esphome/core/defines.h"
#ifdef USE_BINARY_SENSOR_FILTER
#include <array>
#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<AutorepeatFilterTiming> timings);
optional<bool> 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<AutorepeatFilterTiming> 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<size_t N> class AutorepeatFilter : public AutorepeatFilterBase {
public:
explicit AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings) {
init_array_from(this->timings_storage_, timings);
this->timings_ = this->timings_storage_.data();
this->timings_count_ = N;
}
protected:
std::array<AutorepeatFilterTiming, N> timings_storage_{};
};
class LambdaFilter : public Filter {
public:
explicit LambdaFilter(std::function<optional<bool>(bool)> f);
+2 -2
View File
@@ -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):
+12 -27
View File
@@ -295,32 +295,20 @@ optional<float> DeltaFilter::new_value(float value) {
return {};
}
// OrFilter
OrFilter::OrFilter(std::initializer_list<Filter *> filters) : filters_(filters), phi_(this) {}
OrFilter::PhiNode::PhiNode(OrFilter *or_parent) : or_parent_(or_parent) {}
optional<float> 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<float> OrFilter::new_value(float value) {
this->has_value_ = false;
for (auto *filter : this->filters_)
filter->input(value);
optional<float> 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<float> CalibrateLinearFilter::new_value(float value) {
return NAN;
}
CalibratePolynomialFilter::CalibratePolynomialFilter(std::initializer_list<float> coefficients)
: coefficients_(coefficients) {}
optional<float> CalibratePolynomialFilter::new_value(float value) {
optional<float> 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;
+36 -12
View File
@@ -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<float> 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<size_t N> class OrFilter : public Filter {
public:
explicit OrFilter(std::initializer_list<Filter *> filters);
explicit OrFilter(std::initializer_list<Filter *> 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<float> new_value(float value) override;
optional<float> 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<float> new_value(float value) override;
PhiNode(OrFilter *or_parent) : or_parent_(or_parent) {}
optional<float> 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<Filter *> filters_;
PhiNode phi_;
std::array<Filter *, N> filters_{};
PhiNode phi_{this};
bool has_value_{false};
};
@@ -521,13 +537,21 @@ class CalibrateLinearFilter : public Filter {
FixedVector<std::array<float, 3>> linear_functions_;
};
class CalibratePolynomialFilter : public Filter {
/// Non-template helper for polynomial calibration (implementation in filter.cpp)
optional<float> 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<size_t N> class CalibratePolynomialFilter : public Filter {
public:
explicit CalibratePolynomialFilter(std::initializer_list<float> coefficients);
optional<float> new_value(float value) override;
explicit CalibratePolynomialFilter(std::initializer_list<float> coefficients) {
init_array_from(this->coefficients_, coefficients);
}
optional<float> new_value(float value) override {
return calibrate_polynomial_compute(this->coefficients_.data(), N, value);
}
protected:
FixedVector<float> coefficients_;
std::array<float, N> coefficients_{};
};
class ClampFilter : public Filter {
+4 -2
View File
@@ -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="_")
+13 -21
View File
@@ -73,37 +73,29 @@ bool PrependFilter::new_value(std::string &value) {
return true;
}
// Substitute
SubstituteFilter::SubstituteFilter(const std::initializer_list<Substitution> &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<Substitution> &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
+24 -9
View File
@@ -3,6 +3,8 @@
#include "esphome/core/defines.h"
#ifdef USE_TEXT_SENSOR_FILTER
#include <array>
#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<size_t N> class SubstituteFilter : public Filter {
public:
explicit SubstituteFilter(const std::initializer_list<Substitution> &substitutions);
bool new_value(std::string &value) override;
explicit SubstituteFilter(const std::initializer_list<Substitution> &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<Substitution> substitutions_;
std::array<Substitution, N> 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<size_t N> class MapFilter : public Filter {
public:
explicit MapFilter(const std::initializer_list<Substitution> &mappings);
bool new_value(std::string &value) override;
explicit MapFilter(const std::initializer_list<Substitution> &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<Substitution> mappings_;
std::array<Substitution, N> mappings_{};
};
} // namespace esphome::text_sensor
+16 -9
View File
@@ -9,14 +9,17 @@
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include <array>
#include <list>
#include <vector>
namespace esphome {
template<typename... Ts> class AndCondition : public Condition<Ts...> {
template<size_t N, typename... Ts> class AndCondition : public Condition<Ts...> {
public:
explicit AndCondition(std::initializer_list<Condition<Ts...> *> conditions) : conditions_(conditions) {}
explicit AndCondition(std::initializer_list<Condition<Ts...> *> 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<typename... Ts> class AndCondition : public Condition<Ts...> {
}
protected:
FixedVector<Condition<Ts...> *> conditions_;
std::array<Condition<Ts...> *, N> conditions_{};
};
template<typename... Ts> class OrCondition : public Condition<Ts...> {
template<size_t N, typename... Ts> class OrCondition : public Condition<Ts...> {
public:
explicit OrCondition(std::initializer_list<Condition<Ts...> *> conditions) : conditions_(conditions) {}
explicit OrCondition(std::initializer_list<Condition<Ts...> *> 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<typename... Ts> class OrCondition : public Condition<Ts...> {
}
protected:
FixedVector<Condition<Ts...> *> conditions_;
std::array<Condition<Ts...> *, N> conditions_{};
};
template<typename... Ts> class NotCondition : public Condition<Ts...> {
@@ -55,9 +60,11 @@ template<typename... Ts> class NotCondition : public Condition<Ts...> {
Condition<Ts...> *condition_;
};
template<typename... Ts> class XorCondition : public Condition<Ts...> {
template<size_t N, typename... Ts> class XorCondition : public Condition<Ts...> {
public:
explicit XorCondition(std::initializer_list<Condition<Ts...> *> conditions) : conditions_(conditions) {}
explicit XorCondition(std::initializer_list<Condition<Ts...> *> 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<typename... Ts> class XorCondition : public Condition<Ts...> {
}
protected:
FixedVector<Condition<Ts...> *> conditions_;
std::array<Condition<Ts...> *, N> conditions_{};
};
template<typename... Ts> class LambdaCondition : public Condition<Ts...> {
+2 -1
View File
@@ -500,7 +500,8 @@ template<typename T, size_t MAX_CAPACITY = std::numeric_limits<uint16_t>::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<typename T, size_t N> inline void init_array_from(std::array<T, N> &dest, std::initializer_list<T> src) {
#ifdef ESPHOME_DEBUG
assert(src.size() == N);