From 0fc267dfc734c16868c8801d4e85782cc4e19f1f Mon Sep 17 00:00:00 2001 From: Jasper van der Neut - Stulen Date: Tue, 27 Aug 2019 19:39:04 +0200 Subject: [PATCH] Implement median filter (#697) Add median filter to sensors component --- esphome/components/sensor/__init__.py | 14 ++++++++++ esphome/components/sensor/filter.cpp | 38 +++++++++++++++++++++++++++ esphome/components/sensor/filter.h | 30 +++++++++++++++++++++ tests/test1.yaml | 4 +++ 4 files changed, 86 insertions(+) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index c006dfad57..5211322615 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -61,6 +61,7 @@ SensorPublishAction = sensor_ns.class_('SensorPublishAction', automation.Action) # Filters Filter = sensor_ns.class_('Filter') +MedianFilter = sensor_ns.class_('MedianFilter', Filter) SlidingWindowMovingAverageFilter = sensor_ns.class_('SlidingWindowMovingAverageFilter', Filter) ExponentialMovingAverageFilter = sensor_ns.class_('ExponentialMovingAverageFilter', Filter) LambdaFilter = sensor_ns.class_('LambdaFilter', Filter) @@ -127,6 +128,19 @@ def filter_out_filter_to_code(config, filter_id): yield cg.new_Pvariable(filter_id, config) +MEDIAN_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('median', MedianFilter, MEDIAN_SCHEMA) +def median_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 79323bd8ab..3f0d39fcfc 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -39,6 +39,44 @@ uint32_t Filter::calculate_remaining_interval(uint32_t input) { } } +// MedianFilter +MedianFilter::MedianFilter(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 MedianFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } +void MedianFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; } +optional MedianFilter::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, "MedianFilter(%p)::new_value(%f)", this, value); + } + + if (++this->send_at_ >= this->send_every_) { + this->send_at_ = 0; + + float median = 0.0f; + if (!this->queue_.empty()) { + std::deque median_queue = this->queue_; + sort(median_queue.begin(), median_queue.end()); + + size_t queue_size = median_queue.size(); + if (queue_size % 2) { + median = median_queue[queue_size / 2]; + } else { + median = (median_queue[queue_size / 2] + median_queue[(queue_size / 2) - 1]) / 2.0f; + } + } + + ESP_LOGVV(TAG, "MedianFilter(%p)::new_value(%f) SENDING", this, median); + return median; + } + return {}; +} + +uint32_t MedianFilter::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 17a583e40e..4c61d4c0a2 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -46,6 +46,36 @@ class Filter { Sensor *parent_{nullptr}; }; +/** Simple median filter. + * + * Takes the median of the last values and pushes it out every . + */ +class MedianFilter : public Filter { + public: + /** Construct a MedianFilter. + * + * @param window_size The number of values that should be used in median calculation. + * @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 MedianFilter(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 d1329620dc..729ea2b2bf 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -191,6 +191,10 @@ sensor: - 100.0 -> 102.5 - filter_out: 42.0 - filter_out: nan + - median: + window_size: 5 + send_every: 5 + send_first_at: 3 - sliding_window_moving_average: window_size: 15 send_every: 15