Compare commits

...
Author SHA1 Message Date
J. Nick Koston 222d330464 Merge remote-tracking branch 'origin/dev' into HEAD 2026-11-14 08:39:15 -06:00
J. Nick Koston 0fffaa5c4f [sensor] Cover single segment exact calibrate_linear 2026-11-11 18:43:11 -06:00
J. Nick Koston 55a6c1183c Merge branch 'dev' into apply-filter-sensor2 2026-11-11 18:15:45 -06:00
J. Nick Koston a0ab3d2634 [sensor] Emit clamp bounds as written so the compiler rounds them 2026-11-11 18:08:46 -06:00
J. Nick Koston f8598751bc Merge remote-tracking branch 'upstream/dev' into apply-filter-sensor2
# Conflicts:
#	esphome/components/sensor/__init__.py
#	esphome/components/sensor/filter.h
2026-11-11 18:01:54 -06:00
J. Nick Koston 89961864cf Merge remote-tracking branch 'origin/apply-filter-helper2' into apply-filter-sensor2 2026-11-11 13:02:30 -06:00
J. Nick Koston 7a51ca87b4 Merge remote-tracking branch 'origin/dev' into apply-filter-helper2 2026-11-11 13:01:08 -06:00
J. Nick Koston 87e7acdc2b Pass the schema to APPLY_FILTER.build for calibrate_linear 2026-11-10 11:02:34 -08:00
J. Nick Koston 247e31e4d2 Merge remote-tracking branch 'origin/apply-filter-helper2' into st-apply-filter-sensor2 2026-11-10 11:00:10 -08:00
J. Nick Koston 6521dd8269 Require the schema in ApplyFilterDomain.build so a misspelled key always fails 2026-11-10 10:58:38 -08:00
J. Nick Koston bdf4643b97 Sync the shared function helper: accept Statement lines in the body 2026-11-09 21:34:14 -08:00
J. Nick Koston bb9ec63e6b Reject a filter with no statements 2026-11-09 21:23:58 -08:00
J. Nick Koston aeb77b1e6b Keep the value range function name; sync the shared function helper; trim docstrings and tests 2026-11-09 21:08:48 -08:00
J. Nick Koston e627f13b42 Merge remote-tracking branch 'upstream/dev' into apply-filter-helper2
# Conflicts:
#	esphome/automation.py
2026-11-09 20:50:25 -08:00
J. Nick Koston c4ebe611dd [atm90e32] Use ApplyFilter in the threshold test now MultiplyFilter is gone 2026-11-04 19:07:44 -06:00
J. Nick Koston 3061f9a9c1 Merge branch 'apply-filter-helper2' into apply-filter-sensor2 2026-11-04 18:35:35 -06:00
J. Nick Koston c4133a1af5 Merge remote-tracking branch 'origin/dev' into apply-filter-helper2
# Conflicts:
#	tests/unit_tests/test_cpp_generator.py
2026-11-04 18:33:16 -06:00
J. Nick Koston c2f651b803 Test that build() with a schema accepts valid keys 2026-11-03 19:43:43 -06:00
J. Nick Koston c7472e9068 Merge remote-tracking branch 'origin/dev' into apply-filter-helper2 2026-11-03 19:30:00 -06:00
J. Nick Koston 2fdd4434dd Reject a skipped last apply filter statement and let build check schema keys 2026-11-03 19:09:25 -06:00
J. Nick Koston acc7fd2143 [core] Adapt apply filters to the dev apply function changes
Filter functions keep the parameters the filter class passes, so they are
emitted directly instead of through _apply_function, which now turns value
parameters into const references. The filter body renders through
_render_call, which replaced _render_values.
2026-11-03 18:52:25 -06:00
J. Nick Koston fc9743e5c7 Merge remote-tracking branch 'origin/dev' into apply-filter-helper2
# Conflicts:
#	esphome/automation.py
2026-11-03 18:50:21 -06:00
J. Nick Koston 08880dda0a Merge branch 'apply-filter-helper2' into apply-filter-sensor2 2026-11-02 20:41:58 -06:00
J. Nick Koston a26ab932e2 Merge remote-tracking branch 'origin/dev' into apply-filter-helper2
# Conflicts:
#	tests/unit_tests/test_automation.py
2026-11-02 20:41:39 -06:00
J. Nick Koston 3fd62a666c Merge dev 2026-11-02 09:36:15 -06:00
J. Nick Koston 79f1c56ca8 Merge remote-tracking branch 'origin/apply-filter-helper2' into apply-filter-sensor2 2026-10-30 20:11:45 -10:00
J. Nick Koston 3ebb022f55 Merge remote-tracking branch 'origin/dev' into apply-filter-helper2
# Conflicts:
#	esphome/automation.py
#	tests/unit_tests/test_automation.py
2026-10-30 20:11:25 -10:00
J. Nick Koston cf5c08c07b Merge branch 'apply-filter-helper2' into apply-filter-sensor2 2026-10-28 18:45:20 -10:00
J. Nick Koston e732191329 Merge remote-tracking branch 'origin/codegen-shared-static-function' into apply-filter-helper2 2026-10-28 18:45:19 -10:00
J. Nick Koston 2be6a24507 Record apply functions where shared_static_function emits them 2026-10-28 18:43:42 -10:00
J. Nick Koston 2b355fcb14 Merge branch 'apply-filter-helper2' into apply-filter-sensor2 2026-10-28 18:34:42 -10:00
J. Nick Koston 5a838cf0e6 Name the binary sensor apply filter's parameter plain filter
The generated function only holds statements from code generation; user
lambda code is always in functions of its own.
2026-10-28 18:34:13 -10:00
J. Nick Koston ff8a3ea2be Round clamp bounds to float as the old filter did, and spell generated calls as plain strings
A bound that rounds to FLT_MAX stays a bound and one beyond float range
is no bound, matching the float literals ClampFilter took. The clamp,
NTC and linear calibration statements are plain ApplyCall strings.
2026-10-28 18:31:45 -10:00
J. Nick Koston 3a8e90615a Merge branch 'apply-filter-helper2' into apply-filter-sensor2 2026-10-28 18:31:05 -10:00
J. Nick Koston d3963a82db Merge remote-tracking branch 'origin/codegen-shared-static-function' into apply-filter-helper2
# Conflicts:
#	esphome/automation.py
2026-10-28 18:29:57 -10:00
J. Nick Koston 206ab92115 Give unshared apply functions config-id-safe names too
apply_static_function always goes through cg.shared_static_function, so
an unshared function also gets a name no config id uses.
2026-10-28 18:29:30 -10:00
J. Nick Koston 426efc1666 Merge branch 'apply-filter-helper2' into apply-filter-sensor2 2026-10-28 18:22:58 -10:00
J. Nick Koston 2c7748606e Merge remote-tracking branch 'origin/codegen-shared-static-function' into apply-filter-helper2 2026-10-28 18:22:56 -10:00
J. Nick Koston 907b3c6d6a [core] Key shared static functions on the rendered body text 2026-10-28 18:06:43 -10:00
J. Nick Koston 967c61da4c Merge branch 'apply-filter-helper2' into apply-filter-sensor2 2026-10-28 18:00:21 -10:00
J. Nick Koston 7f7e4af3ea Name the binary sensor apply filter's filter parameter in one constant 2026-10-28 18:00:05 -10:00
J. Nick Koston a230ffd928 Merge branch 'apply-filter-helper2' into apply-filter-sensor2 2026-10-28 17:58:05 -10:00
J. Nick Koston 62d074aae6 Merge branch 'filter-behaviour-tests' into apply-filter-helper2 2026-10-28 17:58:04 -10:00
J. Nick Koston 250ed963cc Merge commit 'e6bebc298f7ddf77c4d7d6322f79b22a08170ba5' into filter-behaviour-tests 2026-10-28 17:58:04 -10:00
J. Nick Koston 007eadedb3 Generate the clamp filter as one call and drop OffsetFilter and MultiplyFilter
clamp calls clamp_filter_value, the old ClampFilter logic, with a bound
beyond float range passed as NaN. The NTC calls name their functions
through sensor_ns, and a least squares calibration returns its apply
filter directly. OffsetFilter and MultiplyFilter only existed for
codegen; the benchmark builds the generated shape instead.
2026-10-28 17:29:41 -10:00
J. Nick Koston 50f206f9ff Merge branch 'apply-filter-helper2' into apply-filter-sensor2 2026-10-28 17:28:09 -10:00
J. Nick Koston ed076e9210 Share apply filter functions through cg.shared_static_function
The private cache goes; _apply_function passes share on to
apply_static_function. ApplyFilterDomain.new shares only when asked,
value lambda functions are named by apply_static_function, statements
become ApplyCall in one place and render with _render_values, and one
helper checks a call's keys against the schema.
2026-10-28 17:27:28 -10:00
J. Nick Koston 9aa0a835c4 Merge branch 'codegen-shared-static-function' into apply-filter-helper2
# Conflicts:
#	tests/unit_tests/test_automation.py
2026-10-28 17:26:36 -10:00
J. Nick Koston 8c9d117be5 Merge branch 'filter-behaviour-tests' into apply-filter-helper2 2026-10-28 17:26:24 -10:00
J. Nick Koston 1c33d6b960 Pin value lambdas reading an id named x, and keep only the filters the apply stack changes
A sensor offset lambda and a binary delayed_on_off lambda read an entity
with id x. The round, round_to_multiple_of and calibrate_polynomial cases
go, since no change touches those filters; calibrate_linear exact stays
as a control. Each test reads the log lines once.
2026-10-28 17:26:20 -10:00
J. Nick Koston 79ccc2b5c1 Merge branch 'apply-filter-helper2' into apply-filter-sensor2 2026-10-28 16:47:06 -10:00
J. Nick Koston be88427d00 Merge branch 'filter-behaviour-tests' into apply-filter-helper2 2026-10-28 16:46:08 -10:00
J. Nick Koston fbeb90208b Explain why the b_constant to_ntc_resistance case stays NaN 2026-10-28 16:46:04 -10:00
J. Nick Koston 7d5757e78e Note in the description why removing the codegen-only classes is not breaking 2026-10-28 16:36:59 -10:00
J. Nick Koston b593034a2f Make shared static function names unique and add share to apply_static_function 2026-10-28 16:34:16 -10:00
J. Nick Koston 576c635138 Treat a clamp bound that overflows a C++ float as no bound
The filter compares in float, so a bound such as 3.5e38 is infinite
there and was ignored before; check finiteness after the conversion.
2026-10-28 16:28:20 -10:00
J. Nick Koston 1dd7f6045d Merge branch 'apply-filter-helper2' into apply-filter-sensor2 2026-10-28 16:27:35 -10:00
J. Nick Koston a644d8d9a0 Check the timing of the delayed_on_off filter that reads an id x 2026-10-28 16:27:31 -10:00
J. Nick Koston f2651d794d Merge branch 'filter-behaviour-tests' into apply-filter-helper2 2026-10-28 16:27:16 -10:00
J. Nick Koston 2ed6453467 Check when each delayed binary sensor output lands
Each output must come its delay after the source edge it follows, within
a margin for scheduler jitter, so a lost delay fails the test.
2026-10-28 16:27:13 -10:00
J. Nick Koston 9f6b78930b [core] Add cg.shared_static_function to emit one function per distinct body 2026-10-28 16:25:42 -10:00
J. Nick Koston 1bca56ba04 Merge branch 'apply-filter-helper2' into apply-filter-sensor2 2026-10-28 16:22:05 -10:00
J. Nick Koston cc7a9db510 Merge branch 'filter-behaviour-tests' into apply-filter-helper2 2026-10-28 16:21:59 -10:00
J. Nick Koston a20755a857 Fix the fixture indentation for yamllint 2026-10-28 16:21:57 -10:00
J. Nick Koston 9389b5b078 Merge branch 'apply-filter-helper2' into apply-filter-sensor2 2026-10-28 16:20:59 -10:00
J. Nick Koston 5859db98cb Parenthesize the multi-line strings in the shared value test 2026-10-28 16:20:54 -10:00
J. Nick Koston 3161162207 Register the NTC filters as apply filters and drop their classes
The coefficients are ApplyCall key paths into the calibration, and the
conversions are free Steinhart-Hart functions called fully qualified;
ToNTCResistanceFilter and ToNTCTemperatureFilter only existed for
codegen.
2026-10-28 16:02:53 -10:00
J. Nick Koston 12ee725395 Merge branch 'apply-filter-helper2' into apply-filter-sensor2
# Conflicts:
#	esphome/components/sensor/__init__.py
2026-10-28 16:01:52 -10:00
J. Nick Koston 9a8fbf2e2b Let an apply filter builder pick its statements, and keep shared functions in a dataclass
ApplyFilterDomain.build renders the statements over a config, for a
builder that picks them by the shape of its config; prepare is dropped.
The shared functions live in a CORE.data dataclass. Each domain's
APPLY_FILTER sits under its ApplyFilter, and every domain's test checks
the filter has no bytes beyond its base.
2026-10-28 16:01:34 -10:00
J. Nick Koston c0ff8d7e98 Pin text_sensor lambda filters that replace and drop a value
Also mark the to_ntc_resistance b_constant NaN outputs as a known bug.
2026-10-28 16:00:42 -10:00
J. Nick Koston 15aaaae719 Merge branch 'filter-behaviour-tests' into apply-filter-helper2 2026-10-28 16:00:42 -10:00
J. Nick Koston 88eaaa86c8 Give each apply filter value lambda its own function
Inlined into the filter's function, a value lambda saw the function's x
parameter, so an id named x, such as id(x).state, no longer named that
entity and failed to compile. Each value lambda is now a function
without parameters, called from the filter's function, as it was its
own lambda before. The integration tests cover an id named x in a
sensor offset lambda and a binary sensor delayed_on_off lambda.
2026-10-28 15:59:06 -10:00
J. Nick Koston a493403ef5 Generate offset, multiply, clamp, NTC and single segment calibrate_linear filters as apply filters
Each filter is now a generated function with its constants baked in and
value lambdas called inline, and the filter object is only the Filter
base; equal constant filters share one function. The NTC conversions
move to static convert() methods. ClampFilter only existed for codegen
and is removed.
2026-10-28 14:44:33 -10:00
J. Nick Koston aac2f28df2 Let an apply filter map its config before rendering
prepare turns a shorthand value into the mapping the statements read.
2026-10-28 14:33:08 -10:00
J. Nick Koston 5daa9693d0 Add an apply filter for sensor, binary_sensor and text_sensor
ApplyFilter<Fn> in each domain is a filter whose work and constants are
one generated function, so the object is only the Filter base; the
binary sensor one passes the filter to the function so it can schedule
output. ApplyFilterDomain declares a domain's filters: register() builds
the function from ApplyCall statements, with constants baked in and
value lambdas called inline, and new() takes the statements directly.
Identical functions without user code are emitted once, so equal
constant filters share one class. LambdaExpression.body_lines gives a
lambda's statements with their #line marks for a generated function.
2026-10-28 14:31:25 -10:00
J. Nick Koston 1e6a3c55b6 Pin the output of the stateless sensor filters and the binary sensor delay filters
Integration tests run offset, multiply, clamp, NTC, calibrate_linear,
calibrate_polynomial, lambda and round filters over finite, NaN and
infinite inputs, and the binary sensor delayed_on_off, delayed_on,
delayed_off and lambda filters over one timed input sequence.
2026-10-28 13:58:08 -10:00
9 changed files with 186 additions and 160 deletions

No files matched your search

+67 -62
View File
@@ -1,5 +1,6 @@
import logging
import math
import struct
from typing import Any
from esphome import automation
@@ -301,8 +302,6 @@ ApplyFilter = sensor_ns.class_("ApplyFilter", Filter)
APPLY_FILTER = automation.ApplyFilterDomain(
FILTER_REGISTRY, ApplyFilter, cg.optional.template(cg.float_), ((cg.float_, "x"),)
)
OffsetFilter = sensor_ns.class_("OffsetFilter", Filter)
MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter)
ValueListFilter = sensor_ns.class_("ValueListFilter", Filter)
FilterOutValueFilter = sensor_ns.class_("FilterOutValueFilter", ValueListFilter)
ThrottleFilter = sensor_ns.class_("ThrottleFilter", Filter)
@@ -321,11 +320,8 @@ HeartbeatFilter = sensor_ns.class_("HeartbeatFilter", Filter)
DeltaFilter = sensor_ns.class_("DeltaFilter", Filter)
OrFilter = sensor_ns.class_("OrFilter", Filter)
CalibrateLinearFilter = sensor_ns.class_("CalibrateLinearFilter", Filter)
ToNTCResistanceFilter = sensor_ns.class_("ToNTCResistanceFilter", Filter)
ToNTCTemperatureFilter = sensor_ns.class_("ToNTCTemperatureFilter", Filter)
RHCorrectionFilter = sensor_ns.class_("RHCorrectionFilter", Filter)
CalibratePolynomialFilter = sensor_ns.class_("CalibratePolynomialFilter", Filter)
ClampFilter = sensor_ns.class_("ClampFilter", Filter)
RoundFilter = sensor_ns.class_("RoundFilter", Filter)
RoundMultipleFilter = sensor_ns.class_("RoundMultipleFilter", Filter)
RoundSignificantDigitsFilter = sensor_ns.class_("RoundSignificantDigitsFilter", Filter)
@@ -424,16 +420,16 @@ def sensor_schema(
return _SENSOR_SCHEMA.extend(schema)
@FILTER_REGISTRY.register("offset", OffsetFilter, cv.templatable(cv.float_))
async def offset_filter_to_code(config, filter_id):
template_ = await cg.templatable(config, [], cg.float_)
return cg.new_Pvariable(filter_id, template_)
@FILTER_REGISTRY.register("multiply", MultiplyFilter, cv.templatable(cv.float_))
async def multiply_filter_to_code(config, filter_id):
template_ = await cg.templatable(config, [], cg.float_)
return cg.new_Pvariable(filter_id, template_)
APPLY_FILTER.register(
"offset",
cv.templatable(cv.float_),
automation.ApplyCall("return x + {};", ((automation.WHOLE_CONFIG, cg.float_),)),
)
APPLY_FILTER.register(
"multiply",
cv.templatable(cv.float_),
automation.ApplyCall("return x * {};", ((automation.WHOLE_CONFIG, cg.float_),)),
)
TemplatableFloat = cg.esphome_ns.class_("TemplatableFn").template(cg.float_)
@@ -838,6 +834,18 @@ def validate_calibrate_linear(config):
LINEAR_SEGMENT = cg.std_ns.class_("array").template(cg.float_, 3)
_LINEAR = automation.ApplyCall(
"return x * {} + {};", (("k", cg.float_), ("b", cg.float_))
)
_LINEAR_SCHEMA = cv.Schema({cv.Required("k"): cv.float_, cv.Required("b"): cv.float_})
async def _linear_filter(filter_id: ID, k: float, b: float) -> MockObj:
return await APPLY_FILTER.build(
"calibrate_linear", {"k": k, "b": b}, filter_id, _LINEAR, schema=_LINEAR_SCHEMA
)
@FILTER_REGISTRY.register(
"calibrate_linear",
CalibrateLinearFilter,
@@ -858,12 +866,12 @@ async def calibrate_linear_filter_to_code(config, filter_id):
x = [conf[CONF_FROM] for conf in config[CONF_DATAPOINTS]]
y = [conf[CONF_TO] for conf in config[CONF_DATAPOINTS]]
linear_functions = []
if config[CONF_METHOD] == "least_squares":
k, b = fit_linear(x, y)
linear_functions = [[k, b, float("NaN")]]
elif config[CONF_METHOD] == "exact":
linear_functions = map_linear(x, y)
return await _linear_filter(filter_id, *fit_linear(x, y))
linear_functions = map_linear(x, y)
if len(linear_functions) == 1:
# One segment needs no table
return await _linear_filter(filter_id, *linear_functions[0][:2])
# The last segment always has a NaN boundary, so the filter needs no count.
table = cg.shared_progmem_array(
"sensor_calibrate_linear", LINEAR_SEGMENT, linear_functions
@@ -935,14 +943,31 @@ CLAMP_SCHEMA = cv.All(
)
@FILTER_REGISTRY.register("clamp", ClampFilter, CLAMP_SCHEMA)
async def clamp_filter_to_code(config, filter_id):
return cg.new_Pvariable(
filter_id,
config[CONF_MIN_VALUE],
config[CONF_MAX_VALUE],
config[CONF_IGNORE_OUT_OF_RANGE],
)
def _clamp_bound(config: ConfigType, value: float) -> str:
"""The bound as written, NAN for no bound.
A literal beyond float range was infinite in the old ClampFilter, so no bound; one that
rounds to FLT_MAX stays a bound. The compiler rounds the literal, as it always did.
"""
try:
bound = struct.unpack("f", struct.pack("f", value))[0]
except OverflowError:
return "NAN"
return str(cg.safe_exp(value)) if math.isfinite(bound) else "NAN"
APPLY_FILTER.register(
"clamp",
CLAMP_SCHEMA,
automation.ApplyCall(
"return sensor::clamp_filter_value(x, {}, {}, {});",
(
(CONF_MIN_VALUE, cg.float_, _clamp_bound),
(CONF_MAX_VALUE, cg.float_, _clamp_bound),
(CONF_IGNORE_OUT_OF_RANGE, cg.bool_),
),
),
)
@FILTER_REGISTRY.register(
@@ -1235,46 +1260,26 @@ def ntc_process_calibration(value):
}
@FILTER_REGISTRY.register(
_NTC_SCHEMA = cv.Schema({cv.Required(CONF_CALIBRATION): ntc_process_calibration})
_NTC_COEFFICIENTS = tuple(
((CONF_CALIBRATION, key), cg.double) for key in (CONF_A, CONF_B, CONF_C)
)
APPLY_FILTER.register(
"to_ntc_resistance",
ToNTCResistanceFilter,
cv.All(
cv.Schema(
{
cv.Required(CONF_CALIBRATION): ntc_process_calibration,
}
),
_NTC_SCHEMA,
automation.ApplyCall(
"return sensor::steinhart_hart_resistance(x, {}, {}, {});",
_NTC_COEFFICIENTS,
),
)
async def calibrate_ntc_resistance_filter_to_code(config, filter_id):
calib = config[CONF_CALIBRATION]
return cg.new_Pvariable(
filter_id,
calib[CONF_A],
calib[CONF_B],
calib[CONF_C],
)
@FILTER_REGISTRY.register(
APPLY_FILTER.register(
"to_ntc_temperature",
ToNTCTemperatureFilter,
cv.All(
cv.Schema(
{
cv.Required(CONF_CALIBRATION): ntc_process_calibration,
}
),
_NTC_SCHEMA,
automation.ApplyCall(
"return sensor::steinhart_hart_temperature(x, {}, {}, {});",
_NTC_COEFFICIENTS,
),
)
async def calibrate_ntc_temperature_filter_to_code(config, filter_id):
calib = config[CONF_CALIBRATION]
return cg.new_Pvariable(
filter_id,
calib[CONF_A],
calib[CONF_B],
calib[CONF_C],
)
@FILTER_REGISTRY.register(
+17 -38
View File
@@ -215,16 +215,6 @@ optional<float> LambdaFilter::new_value(float value) {
return it;
}
// OffsetFilter
OffsetFilter::OffsetFilter(TemplatableFn<float> offset) : offset_(offset) {}
optional<float> OffsetFilter::new_value(float value) { return value + this->offset_.value(); }
// MultiplyFilter
MultiplyFilter::MultiplyFilter(TemplatableFn<float> multiplier) : multiplier_(multiplier) {}
optional<float> MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); }
// ValueListFilter
bool ValueListFilter::value_matches_any_(float sensor_value) {
int8_t accuracy = this->parent_->get_accuracy_decimals();
@@ -433,25 +423,6 @@ optional<float> CalibratePolynomialFilter::new_value(float value) {
return res;
}
ClampFilter::ClampFilter(float min, float max, bool ignore_out_of_range)
: min_(min), max_(max), ignore_out_of_range_(ignore_out_of_range) {}
optional<float> ClampFilter::new_value(float value) {
if (std::isfinite(this->min_) && !(value >= this->min_)) {
if (this->ignore_out_of_range_) {
return {};
}
return this->min_;
}
if (std::isfinite(this->max_) && !(value <= this->max_)) {
if (this->ignore_out_of_range_) {
return {};
}
return this->max_;
}
return value;
}
RoundFilter::RoundFilter(uint8_t precision) : precision_(precision) {}
optional<float> RoundFilter::new_value(float value) {
if (std::isfinite(value)) {
@@ -469,25 +440,33 @@ optional<float> RoundMultipleFilter::new_value(float value) {
return value;
}
optional<float> ToNTCResistanceFilter::new_value(float value) {
if (!std::isfinite(value)) {
optional<float> clamp_filter_value(float value, float min, float max, bool ignore_out_of_range) {
if (std::isfinite(min) && !(value >= min))
return ignore_out_of_range ? optional<float>{} : min;
if (std::isfinite(max) && !(value <= max))
return ignore_out_of_range ? optional<float>{} : max;
return value;
}
float steinhart_hart_resistance(float temperature, double a, double b, double c) {
if (!std::isfinite(temperature)) {
return NAN;
}
double k = 273.15;
// https://de.wikipedia.org/wiki/Steinhart-Hart-Gleichung#cite_note-stein2_s4-3
double t = static_cast<double>(value) + k;
double y = (this->a_ - 1 / (t)) / (2 * this->c_);
double x = sqrt(pow(this->b_ / (3 * this->c_), 3) + y * y);
double t = static_cast<double>(temperature) + k;
double y = (a - 1 / (t)) / (2 * c);
double x = sqrt(pow(b / (3 * c), 3) + y * y);
double resistance = exp(pow(x - y, 1 / 3.0) - pow(x + y, 1 / 3.0));
return resistance;
}
optional<float> ToNTCTemperatureFilter::new_value(float value) {
if (!std::isfinite(value)) {
float steinhart_hart_temperature(float resistance, double a, double b, double c) {
if (!std::isfinite(resistance)) {
return NAN;
}
double lr = log(double(value));
double v = this->a_ + this->b_ * lr + this->c_ * lr * lr * lr;
double lr = log(double(resistance));
double v = a + b * lr + c * lr * lr * lr;
double temp = float(1.0 / v - 273.15);
return temp;
}
+6 -52
View File
@@ -300,27 +300,6 @@ template<auto Fn> class ApplyFilter final : public Filter {
optional<float> new_value(float value) override { return Fn(value); }
};
/// A simple filter that adds `offset` to each value it receives.
class OffsetFilter : public Filter {
public:
explicit OffsetFilter(TemplatableFn<float> offset);
optional<float> new_value(float value) override;
protected:
TemplatableFn<float> offset_;
};
/// A simple filter that multiplies to each value it receives by `multiplier`.
class MultiplyFilter : public Filter {
public:
explicit MultiplyFilter(TemplatableFn<float> multiplier);
optional<float> new_value(float value) override;
protected:
TemplatableFn<float> multiplier_;
};
// Value tables are read in place from flash, which ESP8266 only allows for whole words.
static_assert(sizeof(TemplatableFn<float>) == sizeof(void *), "TemplatableFn<float> must stay a single pointer");
@@ -574,17 +553,6 @@ class CalibratePolynomialFilter : public Filter {
const float *coefficients_; // [count, c0, c1, ...]
};
class ClampFilter : public Filter {
public:
ClampFilter(float min, float max, bool ignore_out_of_range);
optional<float> new_value(float value) override;
protected:
float min_{NAN};
float max_{NAN};
bool ignore_out_of_range_;
};
class RoundFilter : public Filter {
public:
explicit RoundFilter(uint8_t precision);
@@ -616,27 +584,13 @@ template<uint8_t Digits> class RoundSignificantDigitsFilter : public Filter {
}
};
class ToNTCResistanceFilter : public Filter {
public:
ToNTCResistanceFilter(double a, double b, double c) : a_(a), b_(b), c_(c) {}
optional<float> new_value(float value) override;
/// Clamp to [min, max] for the clamp filter; a non-finite bound is no bound, and NaN input fails the first set bound.
/// Out of range gives the bound, or empty to drop the value with `ignore_out_of_range`.
optional<float> clamp_filter_value(float value, float min, float max, bool ignore_out_of_range);
protected:
double a_;
double b_;
double c_;
};
class ToNTCTemperatureFilter : public Filter {
public:
ToNTCTemperatureFilter(double a, double b, double c) : a_(a), b_(b), c_(c) {}
optional<float> new_value(float value) override;
protected:
double a_;
double b_;
double c_;
};
/// Steinhart-Hart conversions in double precision for the to_ntc_* filters; non-finite input gives NaN.
float steinhart_hart_resistance(float temperature, double a, double b, double c);
float steinhart_hart_temperature(float resistance, double a, double b, double c);
/** RH Temperature correction filter
*
-1
View File
@@ -82,7 +82,6 @@ class Sensor : public EntityBase {
*
* sensor->add_filters({
* LambdaFilter([&](float value) -> optional<float> { return 42/value; }),
* OffsetFilter(1),
* SlidingWindowMovingAverageFilter(15, 15), // average over last 15 values
* });
*/
@@ -51,13 +51,17 @@ static void SensorFilter_ExponentialMovingAvg(benchmark::State &state) {
}
BENCHMARK(SensorFilter_ExponentialMovingAvg);
// Mirror the code generated for `offset: 1` and `multiply: 2`
static optional<float> offset_1(float x) { return x + 1.0f; }
static optional<float> multiply_2(float x) { return x * 2.0f; }
// Benchmark: sensor publish through a chain of 3 filters (offset + multiply + sliding window)
static void SensorFilter_Chain3(benchmark::State &state) {
Sensor sensor;
sensor.add_filters({
new OffsetFilter([]() -> float { return 1.0f; }),
new MultiplyFilter([]() -> float { return 2.0f; }),
new ApplyFilter<offset_1>(),
new ApplyFilter<multiply_2>(),
new SlidingWindowMovingAverageFilter(5, 1, 1),
});
@@ -33,6 +33,26 @@ sensor:
- calibrate_linear:
- 0.0 -> 1.0
- 10.0 -> 21.0
# Exact with one segment: two points, and collinear points that merge into one
- platform: template
id: exact_two_points
lambda: return 1.0;
filters:
- calibrate_linear:
method: exact
datapoints:
- 0.0 -> 3.0
- 10.0 -> 23.0
- platform: template
id: exact_collinear
lambda: return 1.0;
filters:
- calibrate_linear:
method: exact
datapoints:
- 0.0 -> 5.0
- 10.0 -> 35.0
- 20.0 -> 65.0
- platform: template
id: polynomial
lambda: return 1.0;
@@ -0,0 +1,46 @@
esphome:
name: test
esp8266:
board: d1_mini_lite
sensor:
# Bounds are compared as C++ floats: just beyond FLT_MAX is infinite there, so no bound
- platform: template
id: clamp_beyond_max
lambda: return 1.0;
filters:
- clamp:
min_value: 1.25
max_value: 3.4028236e38
- platform: template
id: clamp_beyond_min
lambda: return 1.0;
filters:
- clamp:
min_value: -3.4028236e38
max_value: 2.75
ignore_out_of_range: true
# 3.4028235e38 rounds to FLT_MAX, so it stays a bound
- platform: template
id: clamp_flt_max
lambda: return 1.0;
filters:
- clamp:
min_value: 0.5
max_value: 3.4028235e38
- platform: template
id: clamp_negative_flt_max
lambda: return 1.0;
filters:
- clamp:
min_value: -3.4028235e38
max_value: 5.5
# Rounded by the compiler from the literal, not by Python first
- platform: template
id: clamp_float_midpoint
lambda: return 1.0;
filters:
- clamp:
min_value: 1.0000000596046448
ignore_out_of_range: true
+21 -4
View File
@@ -29,10 +29,14 @@ def test_calibration_tables_are_shared_progmem(
linear = re.findall(
r"static constexpr std::array<float, 3> (\w+)\[\] PROGMEM", main_cpp
)
assert len(linear) == 2 # the two exact filters share one table
exact, least_squares = linear
assert len(linear) == 1 # only the exact filters have a table, and they share it
(exact,) = linear
assert main_cpp.count(f"sensor::CalibrateLinearFilter({exact});") == 2
assert f"sensor::CalibrateLinearFilter({least_squares});" in main_cpp
# A single segment needs no table: its function is baked into an apply filter
assert "return x * 2.0f + 1.0f;" in main_cpp
# So does an exact one, from two points or from collinear points
assert "return x * 2.0f + 3.0f;" in main_cpp
assert "return x * 3.0f + 5.0f;" in main_cpp
poly = re.search(
r"static constexpr float (\w+)\[\] PROGMEM = \{2\.0f, 1\.0f, 2\.0f\};", main_cpp
@@ -41,4 +45,17 @@ def test_calibration_tables_are_shared_progmem(
assert f"sensor::CalibratePolynomialFilter({poly.group(1)});" in main_cpp
# Table names never reuse a user id.
assert exact != "sensor_calibrate_linear"
assert "sensor_calibratelinearfilter_id_data" not in (exact, least_squares)
assert exact != "sensor_calibratelinearfilter_id_data"
def test_clamp_bounds_are_compared_as_float(
generate_main: Callable[[str], str],
) -> None:
"""A bound beyond float range is no bound; one that rounds to FLT_MAX stays a bound."""
main_cpp = generate_main("tests/component_tests/sensor/test_clamp.yaml")
assert "clamp_filter_value(x, 1.25f, NAN, false);" in main_cpp
assert "clamp_filter_value(x, NAN, 2.75f, true);" in main_cpp
assert "clamp_filter_value(x, 0.5f, 3.4028235e+38f, false);" in main_cpp
assert "clamp_filter_value(x, -3.4028235e+38f, 5.5f, false);" in main_cpp
assert "clamp_filter_value(x, 1.0000000596046448f, NAN, true);" in main_cpp
+3 -1
View File
@@ -212,9 +212,11 @@ TEST_F(ATM90E32ThresholdRegisters, RefreshesThresholdsAfterRestoringAndClearingS
}
#endif // USE_HOST
static optional<float> double_value(float value) { return value * 2.0f; }
TEST_F(ATM90E32ThresholdRegisters, CombinesNativeCurrentWithOtherPhaseAlarms) {
sensor::Sensor current;
sensor::MultiplyFilter multiply([]() { return 2.0f; });
sensor::ApplyFilter<double_value> multiply;
current.add_filter(&multiply);
current.publish_state(6.0f);
ASSERT_FLOAT_EQ(current.state, 12.0f);