mirror of
https://github.com/esphome/esphome.git
synced 2026-09-15 00:58:40 +00:00
Merge branch 'dev' into api/peel-first-write-iteration
This commit is contained in:
+15
-5
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)) {}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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="_")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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...> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user