diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 1cc744e3b5..1eb0b84964 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -65,32 +65,41 @@ optional SlidingWindowFilter::new_value(float value) { } // SortedWindowFilter -FixedVector SortedWindowFilter::get_sorted_values_() { +FixedVector SortedWindowFilter::get_window_values_() { // Copy window without NaN values using FixedVector (no heap allocation) - FixedVector sorted_values; - sorted_values.init(this->window_count_); + // Returns unsorted values - caller will use std::nth_element for partial sorting as needed + FixedVector values; + values.init(this->window_count_); for (size_t i = 0; i < this->window_count_; i++) { float v = this->window_[i]; if (!std::isnan(v)) { - sorted_values.push_back(v); + values.push_back(v); } } - std::sort(sorted_values.begin(), sorted_values.end()); - return sorted_values; + return values; } // MedianFilter float MedianFilter::compute_result() { - FixedVector sorted_values = this->get_sorted_values_(); - if (sorted_values.empty()) + FixedVector values = this->get_window_values_(); + if (values.empty()) return NAN; - size_t size = sorted_values.size(); + size_t size = values.size(); + size_t mid = size / 2; + if (size % 2) { - return sorted_values[size / 2]; - } else { - return (sorted_values[size / 2] + sorted_values[(size / 2) - 1]) / 2.0f; + // Odd number of elements - use nth_element to find middle element + std::nth_element(values.begin(), values.begin() + mid, values.end()); + return values[mid]; } + // Even number of elements - need both middle elements + // Use nth_element to find upper middle element + std::nth_element(values.begin(), values.begin() + mid, values.end()); + float upper = values[mid]; + // Find the maximum of the lower half (which is now everything before mid) + float lower = *std::max_element(values.begin(), values.begin() + mid); + return (lower + upper) / 2.0f; } // SkipInitialFilter @@ -111,13 +120,16 @@ QuantileFilter::QuantileFilter(size_t window_size, size_t send_every, size_t sen : SortedWindowFilter(window_size, send_every, send_first_at), quantile_(quantile) {} float QuantileFilter::compute_result() { - FixedVector sorted_values = this->get_sorted_values_(); - if (sorted_values.empty()) + FixedVector values = this->get_window_values_(); + if (values.empty()) return NAN; - size_t position = ceilf(sorted_values.size() * this->quantile_) - 1; - ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %zu/%zu", this, position + 1, sorted_values.size()); - return sorted_values[position]; + size_t position = ceilf(values.size() * this->quantile_) - 1; + ESP_LOGVV(TAG, "QuantileFilter(%p)::position: %zu/%zu", this, position + 1, values.size()); + + // Use nth_element to find the quantile element (O(n) instead of O(n log n)) + std::nth_element(values.begin(), values.begin() + position, values.end()); + return values[position]; } // MinFilter diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index d99cd79f05..57bb06b517 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -95,17 +95,17 @@ class MinMaxFilter : public SlidingWindowFilter { /** Base class for filters that need a sorted window (Median, Quantile). * - * Extends SlidingWindowFilter to provide a helper that creates a sorted copy - * of non-NaN values from the window. + * Extends SlidingWindowFilter to provide a helper that filters out NaN values. + * Derived classes use std::nth_element for efficient partial sorting. */ class SortedWindowFilter : public SlidingWindowFilter { public: using SlidingWindowFilter::SlidingWindowFilter; protected: - /// Helper to get sorted non-NaN values from the window + /// Helper to get non-NaN values from the window (not sorted - caller will use nth_element) /// Returns empty FixedVector if all values are NaN - FixedVector get_sorted_values_(); + FixedVector get_window_values_(); }; /** Simple quantile filter.