mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Add calibrate_polynomial sensor filter (#642)
* Add calibrate_polynomial sensor filter * Fix * Lint * Format
This commit is contained in:
		| @@ -73,6 +73,7 @@ HeartbeatFilter = sensor_ns.class_('HeartbeatFilter', Filter, cg.Component) | |||||||
| DeltaFilter = sensor_ns.class_('DeltaFilter', Filter) | DeltaFilter = sensor_ns.class_('DeltaFilter', Filter) | ||||||
| OrFilter = sensor_ns.class_('OrFilter', Filter) | OrFilter = sensor_ns.class_('OrFilter', Filter) | ||||||
| CalibrateLinearFilter = sensor_ns.class_('CalibrateLinearFilter', Filter) | CalibrateLinearFilter = sensor_ns.class_('CalibrateLinearFilter', Filter) | ||||||
|  | CalibratePolynomialFilter = sensor_ns.class_('CalibratePolynomialFilter', Filter) | ||||||
| SensorInRangeCondition = sensor_ns.class_('SensorInRangeCondition', Filter) | SensorInRangeCondition = sensor_ns.class_('SensorInRangeCondition', Filter) | ||||||
|  |  | ||||||
| unit_of_measurement = cv.string_strict | unit_of_measurement = cv.string_strict | ||||||
| @@ -194,6 +195,32 @@ def calibrate_linear_filter_to_code(config, filter_id): | |||||||
|     yield cg.new_Pvariable(filter_id, k, b) |     yield cg.new_Pvariable(filter_id, k, b) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | CONF_DATAPOINTS = 'datapoints' | ||||||
|  | CONF_DEGREE = 'degree' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_calibrate_polynomial(config): | ||||||
|  |     if config[CONF_DEGREE] >= len(config[CONF_DATAPOINTS]): | ||||||
|  |         raise cv.Invalid("Degree is too high! Maximum possible degree with given datapoints is " | ||||||
|  |                          "{}".format(len(config[CONF_DATAPOINTS]) - 1), [CONF_DEGREE]) | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @FILTER_REGISTRY.register('calibrate_polynomial', CalibratePolynomialFilter, cv.All(cv.Schema({ | ||||||
|  |     cv.Required(CONF_DATAPOINTS): cv.All(cv.ensure_list(validate_datapoint), cv.Length(min=1)), | ||||||
|  |     cv.Required(CONF_DEGREE): cv.positive_int, | ||||||
|  | }), validate_calibrate_polynomial)) | ||||||
|  | def calibrate_polynomial_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]] | ||||||
|  |     degree = config[CONF_DEGREE] | ||||||
|  |     a = [[1] + [x_**(i+1) for i in range(degree)] for x_ in x] | ||||||
|  |     # Column vector | ||||||
|  |     b = [[v] for v in y] | ||||||
|  |     res = [v[0] for v in _lstsq(a, b)] | ||||||
|  |     yield cg.new_Pvariable(filter_id, res) | ||||||
|  |  | ||||||
|  |  | ||||||
| @coroutine | @coroutine | ||||||
| def build_filters(config): | def build_filters(config): | ||||||
|     yield cg.build_registry_list(FILTER_REGISTRY, config) |     yield cg.build_registry_list(FILTER_REGISTRY, config) | ||||||
| @@ -303,6 +330,66 @@ def fit_linear(x, y): | |||||||
|     return k, b |     return k, b | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _mat_copy(m): | ||||||
|  |     return [list(row) for row in m] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _mat_transpose(m): | ||||||
|  |     return _mat_copy(zip(*m)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _mat_identity(n): | ||||||
|  |     return [[int(i == j) for j in range(n)] for i in range(n)] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _mat_dot(a, b): | ||||||
|  |     b_t = _mat_transpose(b) | ||||||
|  |     return [[sum(x*y for x, y in zip(row_a, col_b)) for col_b in b_t] for row_a in a] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _mat_inverse(m): | ||||||
|  |     n = len(m) | ||||||
|  |     m = _mat_copy(m) | ||||||
|  |     id = _mat_identity(n) | ||||||
|  |  | ||||||
|  |     for diag in range(n): | ||||||
|  |         # If diag element is 0, swap rows | ||||||
|  |         if m[diag][diag] == 0: | ||||||
|  |             for i in range(diag+1, n): | ||||||
|  |                 if m[i][diag] != 0: | ||||||
|  |                     break | ||||||
|  |             else: | ||||||
|  |                 raise ValueError("Singular matrix, inverse cannot be calculated!") | ||||||
|  |  | ||||||
|  |             # Swap rows | ||||||
|  |             m[diag], m[i] = m[i], m[diag] | ||||||
|  |             id[diag], id[i] = id[i], id[diag] | ||||||
|  |  | ||||||
|  |         # Scale row to 1 in diagonal | ||||||
|  |         scaler = 1.0 / m[diag][diag] | ||||||
|  |         for j in range(n): | ||||||
|  |             m[diag][j] *= scaler | ||||||
|  |             id[diag][j] *= scaler | ||||||
|  |  | ||||||
|  |         # Subtract diag row | ||||||
|  |         for i in range(n): | ||||||
|  |             if i == diag: | ||||||
|  |                 continue | ||||||
|  |             scaler = m[i][diag] | ||||||
|  |             for j in range(n): | ||||||
|  |                 m[i][j] -= scaler * m[diag][j] | ||||||
|  |                 id[i][j] -= scaler * id[diag][j] | ||||||
|  |  | ||||||
|  |     return id | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _lstsq(a, b): | ||||||
|  |     # min_x ||b - ax||^2_2 => x = (a^T a)^{-1} a^T b | ||||||
|  |     a_t = _mat_transpose(a) | ||||||
|  |     x = _mat_inverse(_mat_dot(a_t, a)) | ||||||
|  |     return _mat_dot(_mat_dot(x, a_t), b) | ||||||
|  |  | ||||||
|  |  | ||||||
| @coroutine_with_priority(40.0) | @coroutine_with_priority(40.0) | ||||||
| def to_code(config): | def to_code(config): | ||||||
|     cg.add_define('USE_SENSOR') |     cg.add_define('USE_SENSOR') | ||||||
|   | |||||||
| @@ -228,5 +228,15 @@ float HeartbeatFilter::get_setup_priority() const { return setup_priority::HARDW | |||||||
| optional<float> CalibrateLinearFilter::new_value(float value) { return value * this->slope_ + this->bias_; } | optional<float> CalibrateLinearFilter::new_value(float value) { return value * this->slope_ + this->bias_; } | ||||||
| CalibrateLinearFilter::CalibrateLinearFilter(float slope, float bias) : slope_(slope), bias_(bias) {} | CalibrateLinearFilter::CalibrateLinearFilter(float slope, float bias) : slope_(slope), bias_(bias) {} | ||||||
|  |  | ||||||
|  | optional<float> CalibratePolynomialFilter::new_value(float value) { | ||||||
|  |   float res = 0.0f; | ||||||
|  |   float x = 1.0f; | ||||||
|  |   for (float coefficient : this->coefficients_) { | ||||||
|  |     res += x * coefficient; | ||||||
|  |     x *= value; | ||||||
|  |   } | ||||||
|  |   return res; | ||||||
|  | } | ||||||
|  |  | ||||||
| }  // namespace sensor | }  // namespace sensor | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -243,5 +243,14 @@ class CalibrateLinearFilter : public Filter { | |||||||
|   float bias_; |   float bias_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | class CalibratePolynomialFilter : public Filter { | ||||||
|  |  public: | ||||||
|  |   CalibratePolynomialFilter(const std::vector<float> &coefficients) : coefficients_(coefficients) {} | ||||||
|  |   optional<float> new_value(float value) override; | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   std::vector<float> coefficients_; | ||||||
|  | }; | ||||||
|  |  | ||||||
| }  // namespace sensor | }  // namespace sensor | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -133,6 +133,14 @@ sensor: | |||||||
|       - calibrate_linear: |       - calibrate_linear: | ||||||
|           - 0 -> 0 |           - 0 -> 0 | ||||||
|           - 100 -> 100 |           - 100 -> 100 | ||||||
|  |       - calibrate_polynomial: | ||||||
|  |           degree: 3 | ||||||
|  |           datapoints: | ||||||
|  |             - 0 -> 0 | ||||||
|  |             - 100 -> 200 | ||||||
|  |             - 400 -> 500 | ||||||
|  |             - -50 -> -1000 | ||||||
|  |             - -100 -> -10000 | ||||||
|   - platform: resistance |   - platform: resistance | ||||||
|     sensor: my_sensor |     sensor: my_sensor | ||||||
|     configuration: DOWNSTREAM |     configuration: DOWNSTREAM | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user