Add 'map_linear' and 'clamp' sensor filters (#5040)

This commit is contained in:
Mat931 2023-07-30 21:09:09 +00:00 committed by GitHub
parent 794a4bd9a1
commit cd46a69f2c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 137 additions and 23 deletions

View file

@ -31,6 +31,9 @@ from esphome.const import (
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_FORCE_UPDATE, CONF_FORCE_UPDATE,
CONF_VALUE, CONF_VALUE,
CONF_MIN_VALUE,
CONF_MAX_VALUE,
CONF_METHOD,
DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_AQI, DEVICE_CLASS_AQI,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE, DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
@ -227,6 +230,7 @@ OrFilter = sensor_ns.class_("OrFilter", Filter)
CalibrateLinearFilter = sensor_ns.class_("CalibrateLinearFilter", Filter) CalibrateLinearFilter = sensor_ns.class_("CalibrateLinearFilter", Filter)
CalibratePolynomialFilter = sensor_ns.class_("CalibratePolynomialFilter", Filter) CalibratePolynomialFilter = sensor_ns.class_("CalibratePolynomialFilter", Filter)
SensorInRangeCondition = sensor_ns.class_("SensorInRangeCondition", Filter) SensorInRangeCondition = sensor_ns.class_("SensorInRangeCondition", Filter)
ClampFilter = sensor_ns.class_("ClampFilter", Filter)
validate_unit_of_measurement = cv.string_strict validate_unit_of_measurement = cv.string_strict
validate_accuracy_decimals = cv.int_ validate_accuracy_decimals = cv.int_
@ -557,30 +561,60 @@ async def debounce_filter_to_code(config, filter_id):
return var return var
def validate_not_all_from_same(config): CONF_DATAPOINTS = "datapoints"
if all(conf[CONF_FROM] == config[0][CONF_FROM] for conf in config):
raise cv.Invalid(
"The 'from' values of the calibrate_linear filter cannot all point " def validate_calibrate_linear(config):
"to the same value! Please add more values to the filter." datapoints = config[CONF_DATAPOINTS]
) if config[CONF_METHOD] == "exact":
for i in range(len(datapoints) - 1):
if datapoints[i][CONF_FROM] > datapoints[i + 1][CONF_FROM]:
raise cv.Invalid(
"The 'from' values of the calibrate_linear filter must be sorted in ascending order."
)
for i in range(len(datapoints) - 1):
if datapoints[i][CONF_FROM] == datapoints[i + 1][CONF_FROM]:
raise cv.Invalid(
"The 'from' values of the calibrate_linear filter must not contain duplicates."
)
elif config[CONF_METHOD] == "least_squares":
if all(conf[CONF_FROM] == datapoints[0][CONF_FROM] for conf in datapoints):
raise cv.Invalid(
"The 'from' values of the calibrate_linear filter cannot all point "
"to the same value! Please add more values to the filter."
)
return config return config
@FILTER_REGISTRY.register( @FILTER_REGISTRY.register(
"calibrate_linear", "calibrate_linear",
CalibrateLinearFilter, CalibrateLinearFilter,
cv.All( cv.maybe_simple_value(
cv.ensure_list(validate_datapoint), cv.Length(min=2), validate_not_all_from_same {
cv.Required(CONF_DATAPOINTS): cv.All(
cv.ensure_list(validate_datapoint), cv.Length(min=2)
),
cv.Optional(CONF_METHOD, default="least_squares"): cv.one_of(
"least_squares", "exact", lower=True
),
},
validate_calibrate_linear,
key=CONF_DATAPOINTS,
), ),
) )
async def calibrate_linear_filter_to_code(config, filter_id): async def calibrate_linear_filter_to_code(config, filter_id):
x = [conf[CONF_FROM] for conf in config] x = [conf[CONF_FROM] for conf in config[CONF_DATAPOINTS]]
y = [conf[CONF_TO] for conf in config] y = [conf[CONF_TO] for conf in config[CONF_DATAPOINTS]]
k, b = fit_linear(x, y)
return cg.new_Pvariable(filter_id, k, b) 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 cg.new_Pvariable(filter_id, linear_functions)
CONF_DATAPOINTS = "datapoints"
CONF_DEGREE = "degree" CONF_DEGREE = "degree"
@ -619,6 +653,36 @@ async def calibrate_polynomial_filter_to_code(config, filter_id):
return cg.new_Pvariable(filter_id, res) return cg.new_Pvariable(filter_id, res)
def validate_clamp(config):
if not math.isfinite(config[CONF_MIN_VALUE]) and not math.isfinite(
config[CONF_MAX_VALUE]
):
raise cv.Invalid("Either 'min_value' or 'max_value' must be set to a number.")
if config[CONF_MIN_VALUE] > config[CONF_MAX_VALUE]:
raise cv.Invalid("The 'min_value' must not be larger than the 'max_value'.")
return config
CLAMP_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_MIN_VALUE, default="NaN"): cv.float_,
cv.Optional(CONF_MAX_VALUE, default="NaN"): cv.float_,
}
),
validate_clamp,
)
@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],
)
async def build_filters(config): async def build_filters(config):
return await cg.build_registry_list(FILTER_REGISTRY, config) return await cg.build_registry_list(FILTER_REGISTRY, config)
@ -730,6 +794,22 @@ def fit_linear(x, y):
return k, b return k, b
def map_linear(x, y):
assert len(x) == len(y)
f = []
for i in range(len(x) - 1):
slope = (y[i + 1] - y[i]) / (x[i + 1] - x[i])
bias = y[i] - (slope * x[i])
next_x = x[i + 1]
if i == len(x) - 2:
next_x = float("NaN")
if f and f[-1][0] == slope and f[-1][1] == bias:
f[-1][2] = next_x
else:
f.append([slope, bias, next_x])
return f
def _mat_copy(m): def _mat_copy(m):
return [list(row) for row in m] return [list(row) for row in m]

View file

@ -416,8 +416,13 @@ void HeartbeatFilter::setup() {
} }
float HeartbeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; } float HeartbeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<float> CalibrateLinearFilter::new_value(float value) { return value * this->slope_ + this->bias_; } optional<float> CalibrateLinearFilter::new_value(float value) {
CalibrateLinearFilter::CalibrateLinearFilter(float slope, float bias) : slope_(slope), bias_(bias) {} for (std::array<float, 3> f : this->linear_functions_) {
if (!std::isfinite(f[2]) || value < f[2])
return (value * f[0]) + f[1];
}
return NAN;
}
optional<float> CalibratePolynomialFilter::new_value(float value) { optional<float> CalibratePolynomialFilter::new_value(float value) {
float res = 0.0f; float res = 0.0f;
@ -429,5 +434,16 @@ optional<float> CalibratePolynomialFilter::new_value(float value) {
return res; return res;
} }
ClampFilter::ClampFilter(float min, float max) : min_(min), max_(max) {}
optional<float> ClampFilter::new_value(float value) {
if (std::isfinite(value)) {
if (std::isfinite(this->min_) && value < this->min_)
return this->min_;
if (std::isfinite(this->max_) && value > this->max_)
return this->max_;
}
return value;
}
} // namespace sensor } // namespace sensor
} // namespace esphome } // namespace esphome

View file

@ -390,12 +390,12 @@ class OrFilter : public Filter {
class CalibrateLinearFilter : public Filter { class CalibrateLinearFilter : public Filter {
public: public:
CalibrateLinearFilter(float slope, float bias); CalibrateLinearFilter(std::vector<std::array<float, 3>> linear_functions)
: linear_functions_(std::move(linear_functions)) {}
optional<float> new_value(float value) override; optional<float> new_value(float value) override;
protected: protected:
float slope_; std::vector<std::array<float, 3>> linear_functions_;
float bias_;
}; };
class CalibratePolynomialFilter : public Filter { class CalibratePolynomialFilter : public Filter {
@ -407,5 +407,15 @@ class CalibratePolynomialFilter : public Filter {
std::vector<float> coefficients_; std::vector<float> coefficients_;
}; };
class ClampFilter : public Filter {
public:
ClampFilter(float min, float max);
optional<float> new_value(float value) override;
protected:
float min_{NAN};
float max_{NAN};
};
} // namespace sensor } // namespace sensor
} // namespace esphome } // namespace esphome

View file

@ -379,9 +379,13 @@ sensor:
- offset: 2.0 - offset: 2.0
- multiply: 1.2 - multiply: 1.2
- calibrate_linear: - calibrate_linear:
- 0.0 -> 0.0 datapoints:
- 40.0 -> 45.0 - 0.0 -> 0.0
- 100.0 -> 102.5 - 40.0 -> 45.0
- 100.0 -> 102.5
- clamp:
min_value: -100
max_value: 100
- filter_out: 42.0 - filter_out: 42.0
- filter_out: nan - filter_out: nan
- median: - median:

View file

@ -88,8 +88,12 @@ sensor:
- debounce: 500s - debounce: 500s
- timeout: 10min - timeout: 10min
- calibrate_linear: - calibrate_linear:
- 0 -> 0 method: exact
- 100 -> 100 datapoints:
- -1 -> 3
- 0.0 -> 1.0
- 1.0 -> 2.0
- 2.0 -> 3.0
- calibrate_polynomial: - calibrate_polynomial:
degree: 3 degree: 3
datapoints: datapoints: