From 88b46b748756cda0bd6cac5a780c087a2526b07f Mon Sep 17 00:00:00 2001 From: Gabe Cook Date: Fri, 5 Mar 2021 07:10:06 -0600 Subject: [PATCH] Add min/max filters (#1569) --- esphome/components/sensor/__init__.py | 28 ++++++++++++ esphome/components/sensor/filter.cpp | 62 +++++++++++++++++++++++++++ esphome/components/sensor/filter.h | 60 ++++++++++++++++++++++++++ tests/test1.yaml | 8 ++++ 4 files changed, 158 insertions(+) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index c22edf0855..a1de877d78 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -72,6 +72,8 @@ SensorPublishAction = sensor_ns.class_('SensorPublishAction', automation.Action) # Filters Filter = sensor_ns.class_('Filter') MedianFilter = sensor_ns.class_('MedianFilter', Filter) +MinFilter = sensor_ns.class_('MinFilter', Filter) +MaxFilter = sensor_ns.class_('MaxFilter', Filter) SlidingWindowMovingAverageFilter = sensor_ns.class_('SlidingWindowMovingAverageFilter', Filter) ExponentialMovingAverageFilter = sensor_ns.class_('ExponentialMovingAverageFilter', Filter) LambdaFilter = sensor_ns.class_('LambdaFilter', Filter) @@ -165,6 +167,32 @@ def median_filter_to_code(config, filter_id): config[CONF_SEND_FIRST_AT]) +MIN_SCHEMA = cv.All(cv.Schema({ + cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int, + cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int, + cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int, +}), validate_send_first_at) + + +@FILTER_REGISTRY.register('min', MinFilter, MIN_SCHEMA) +def min_filter_to_code(config, filter_id): + yield cg.new_Pvariable(filter_id, config[CONF_WINDOW_SIZE], config[CONF_SEND_EVERY], + config[CONF_SEND_FIRST_AT]) + + +MAX_SCHEMA = cv.All(cv.Schema({ + cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int, + cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int, + cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int, +}), validate_send_first_at) + + +@FILTER_REGISTRY.register('max', MaxFilter, MAX_SCHEMA) +def max_filter_to_code(config, filter_id): + yield cg.new_Pvariable(filter_id, config[CONF_WINDOW_SIZE], config[CONF_SEND_EVERY], + config[CONF_SEND_FIRST_AT]) + + SLIDING_AVERAGE_SCHEMA = cv.All(cv.Schema({ cv.Optional(CONF_WINDOW_SIZE, default=15): cv.positive_not_null_int, cv.Optional(CONF_SEND_EVERY, default=15): cv.positive_not_null_int, diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index f7a5b5d7ad..6dfb11b9c9 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -77,6 +77,68 @@ optional MedianFilter::new_value(float value) { uint32_t MedianFilter::expected_interval(uint32_t input) { return input * this->send_every_; } +// MinFilter +MinFilter::MinFilter(size_t window_size, size_t send_every, size_t send_first_at) + : send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {} +void MinFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } +void MinFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; } +optional MinFilter::new_value(float value) { + if (!isnan(value)) { + while (this->queue_.size() >= this->window_size_) { + this->queue_.pop_front(); + } + this->queue_.push_back(value); + ESP_LOGVV(TAG, "MinFilter(%p)::new_value(%f)", this, value); + } + + if (++this->send_at_ >= this->send_every_) { + this->send_at_ = 0; + + float min = 0.0f; + if (!this->queue_.empty()) { + std::deque::iterator it = std::min_element(queue_.begin(), queue_.end()); + min = *it; + } + + ESP_LOGVV(TAG, "MinFilter(%p)::new_value(%f) SENDING", this, min); + return min; + } + return {}; +} + +uint32_t MinFilter::expected_interval(uint32_t input) { return input * this->send_every_; } + +// MaxFilter +MaxFilter::MaxFilter(size_t window_size, size_t send_every, size_t send_first_at) + : send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {} +void MaxFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } +void MaxFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; } +optional MaxFilter::new_value(float value) { + if (!isnan(value)) { + while (this->queue_.size() >= this->window_size_) { + this->queue_.pop_front(); + } + this->queue_.push_back(value); + ESP_LOGVV(TAG, "MaxFilter(%p)::new_value(%f)", this, value); + } + + if (++this->send_at_ >= this->send_every_) { + this->send_at_ = 0; + + float max = 0.0f; + if (!this->queue_.empty()) { + std::deque::iterator it = std::max_element(queue_.begin(), queue_.end()); + max = *it; + } + + ESP_LOGVV(TAG, "MaxFilter(%p)::new_value(%f) SENDING", this, max); + return max; + } + return {}; +} + +uint32_t MaxFilter::expected_interval(uint32_t input) { return input * this->send_every_; } + // SlidingWindowMovingAverageFilter SlidingWindowMovingAverageFilter::SlidingWindowMovingAverageFilter(size_t window_size, size_t send_every, size_t send_first_at) diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 4c61d4c0a2..651d2a8986 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -76,6 +76,66 @@ class MedianFilter : public Filter { size_t window_size_; }; +/** Simple min filter. + * + * Takes the min of the last values and pushes it out every . + */ +class MinFilter : public Filter { + public: + /** Construct a MinFilter. + * + * @param window_size The number of values that the min should be returned from. + * @param send_every After how many sensor values should a new one be pushed out. + * @param send_first_at After how many values to forward the very first value. Defaults to the first value + * on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to + * send_every. + */ + explicit MinFilter(size_t window_size, size_t send_every, size_t send_first_at); + + optional new_value(float value) override; + + void set_send_every(size_t send_every); + void set_window_size(size_t window_size); + + uint32_t expected_interval(uint32_t input) override; + + protected: + std::deque queue_; + size_t send_every_; + size_t send_at_; + size_t window_size_; +}; + +/** Simple max filter. + * + * Takes the max of the last values and pushes it out every . + */ +class MaxFilter : public Filter { + public: + /** Construct a MaxFilter. + * + * @param window_size The number of values that the max should be returned from. + * @param send_every After how many sensor values should a new one be pushed out. + * @param send_first_at After how many values to forward the very first value. Defaults to the first value + * on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to + * send_every. + */ + explicit MaxFilter(size_t window_size, size_t send_every, size_t send_first_at); + + optional new_value(float value) override; + + void set_send_every(size_t send_every); + void set_window_size(size_t window_size); + + uint32_t expected_interval(uint32_t input) override; + + protected: + std::deque queue_; + size_t send_every_; + size_t send_at_; + size_t window_size_; +}; + /** Simple sliding window moving average filter. * * Essentially just takes takes the average of the last window_size values and pushes them out diff --git a/tests/test1.yaml b/tests/test1.yaml index ff7971d901..8842b4b3b3 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -262,6 +262,14 @@ sensor: window_size: 5 send_every: 5 send_first_at: 3 + - min: + window_size: 5 + send_every: 5 + send_first_at: 3 + - max: + window_size: 5 + send_every: 5 + send_first_at: 3 - sliding_window_moving_average: window_size: 15 send_every: 15