mirror of
https://github.com/esphome/esphome.git
synced 2024-11-25 16:38:16 +01:00
Add 'map_linear' and 'clamp' sensor filters (#5040)
This commit is contained in:
parent
794a4bd9a1
commit
cd46a69f2c
5 changed files with 137 additions and 23 deletions
|
@ -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,8 +561,24 @@ 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):
|
|
||||||
|
|
||||||
|
def validate_calibrate_linear(config):
|
||||||
|
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(
|
raise cv.Invalid(
|
||||||
"The 'from' values of the calibrate_linear filter cannot all point "
|
"The 'from' values of the calibrate_linear filter cannot all point "
|
||||||
"to the same value! Please add more values to the filter."
|
"to the same value! Please add more values to the filter."
|
||||||
|
@ -569,18 +589,32 @@ def validate_not_all_from_same(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]]
|
||||||
|
|
||||||
|
linear_functions = []
|
||||||
|
if config[CONF_METHOD] == "least_squares":
|
||||||
k, b = fit_linear(x, y)
|
k, b = fit_linear(x, y)
|
||||||
return cg.new_Pvariable(filter_id, k, b)
|
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]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -379,9 +379,13 @@ sensor:
|
||||||
- offset: 2.0
|
- offset: 2.0
|
||||||
- multiply: 1.2
|
- multiply: 1.2
|
||||||
- calibrate_linear:
|
- calibrate_linear:
|
||||||
|
datapoints:
|
||||||
- 0.0 -> 0.0
|
- 0.0 -> 0.0
|
||||||
- 40.0 -> 45.0
|
- 40.0 -> 45.0
|
||||||
- 100.0 -> 102.5
|
- 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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue