Skip to content

[sensor] Enhance delta filter with a max value#12605

Merged
clydebarrow merged 14 commits intoesphome:devfrom
polyfloyd:max-delta-filter
Jan 20, 2026
Merged

[sensor] Enhance delta filter with a max value#12605
clydebarrow merged 14 commits intoesphome:devfrom
polyfloyd:max-delta-filter

Conversation

@polyfloyd
Copy link
Contributor

@polyfloyd polyfloyd commented Dec 21, 2025

What does this implement/fix?

This filter rejects measurement differences that exceed some configurable threshold, effectively removing outliers.

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Developer breaking change (an API change that could break external components)
  • Code quality improvements to existing code or addition of tests
  • Other
**Related issue or feature (if applicable):**
  • fixes

Pull request in esphome-docs with documentation (if applicable):

Test Environment

  • ESP32
  • ESP32 IDF
  • ESP8266
  • RP2040
  • BK72xx
  • RTL87xx
  • nRF52840
  • Host

Example entry for config.yaml:

sensor:
  - platform: template
    filters:
      - max_delta:
          value: 10

Checklist:

  • The code change is tested and works locally.
  • Tests have been added to verify that the new code works (under tests/ folder).

If user exposed functionality or configuration variables are added/changed:

@polyfloyd polyfloyd requested a review from a team as a code owner December 21, 2025 13:25
@github-actions
Copy link
Contributor

To use the changes from this PR as an external component, add the following to your ESPHome configuration YAML file:

external_components:
  - source: github://pr#12605
    components: [sensor]
    refresh: 1h

(Added by the PR bot)

@github-actions
Copy link
Contributor

👋 Hi there! This PR modifies 3 file(s) with codeowners.

@esphome/core - As codeowner(s) of the affected files, your review would be appreciated! 🙏

Note: Automatic review request may have failed, but you're still welcome to review.

@codecov-commenter
Copy link

codecov-commenter commented Dec 21, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.60%. Comparing base (6edecd3) to head (a167760).

Additional details and impacted files
@@            Coverage Diff             @@
##              dev   #12605      +/-   ##
==========================================
- Coverage   74.62%   74.60%   -0.03%     
==========================================
  Files          53       53              
  Lines       11347    11347              
  Branches     1541     1541              
==========================================
- Hits         8468     8465       -3     
- Misses       2471     2473       +2     
- Partials      408      409       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@esphome esphome bot removed the needs-docs label Dec 21, 2025
@polyfloyd polyfloyd changed the title Draft: Add the max_delta filter Add the max_delta filter Dec 21, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Dec 21, 2025

Memory Impact Analysis

Components: sensor, template
Platform: esp8266-ard

Metric Target Branch This PR Change
RAM 31,716 bytes 31,716 bytes ➡️ +0 bytes (0.00%)
Flash 403,631 bytes 403,695 bytes 📈 🔸 +64 bytes (+0.02%)
📊 Component Memory Breakdown
Component Target Flash PR Flash Change
app_framework 14,600 bytes 14,640 bytes 📈 +40 bytes (+0.27%)
[esphome]sensor 8,776 bytes 8,788 bytes 📈 🔸 +12 bytes (+0.14%)
[esphome]core 12,637 bytes 12,633 bytes 📉 ✅ -4 bytes (-0.03%)
🔍 Symbol-Level Changes (click to expand)

Changed Symbols

Symbol Target Size PR Size Change
setup 12,329 bytes 12,369 bytes 📈 +40 bytes (+0.32%)
esphome::sensor::ValueListFilter::ValueListFilter(std::initializer_list<esphome::TemplatableValue...esphome::sensor::ValueListFilter::ValueListFilter(std::initializer_list<esphome::TemplatableValue >)
190 bytes 194 bytes 📈 +4 bytes (+2.11%)
esphome::TemplatableValue<float>::TemplatableValue(esphome::TemplatableValue<float>&&) 55 bytes 51 bytes 📉 -4 bytes (-7.27%)
esphome::sensor::DeltaFilter::new_value(float) 153 bytes 156 bytes 📈 +3 bytes (+1.96%)
std::_Function_handler<void (), esphome::sensor::HeartbeatFilter::setup()::{lambda()#1}>::_M_mana...std::_Function_handler<void (), esphome::sensor::HeartbeatFilter::setup()::{lambda()#1}>::_M_manager(std::_Any_data&, std::_Function_handler<void (), esphome::sensor::HeartbeatFilter::setup()::{lambda()#1}> const&, std::_Manager_operation)
20 bytes 17 bytes 📉 -3 bytes (-15.00%)
esphome::sensor::OrFilter::new_value(float) 70 bytes 69 bytes 📉 -1 bytes (-1.43%)
esphome::sensor::SortedWindowFilter::get_window_values_() 78 bytes 79 bytes 📈 +1 bytes (+1.28%)

New Symbols (top 15)

Symbol Size
esphome::sensor::DeltaFilter::DeltaFilter(float, float, float, float) 34 bytes
sensor_deltafilter_id_2 4 bytes
esphome::sensor::DeltaFilter::baseline_::{lambda(float)#1}::_FUN(float) 2 bytes

Removed Symbols (top 15)

Symbol Size
esphome::sensor::DeltaFilter::DeltaFilter(float, bool) 28 bytes

Note: This analysis measures static RAM and Flash usage only (compile-time allocation).
Dynamic memory (heap) cannot be measured automatically.
⚠️ You must test this PR on a real device to measure free heap and ensure no runtime memory issues.

This analysis runs automatically when components change. Memory usage is measured from a merged configuration with 2 components.

@swoboda1337
Copy link
Member

swoboda1337 commented Dec 21, 2025

I had a PR to do something similar: #8096

The danger with this PR is if there is a legitimate step (larger than the user expects) the filter never recovers and will never report a new value. If there is a step its ok to ignore it for a bit but if the value settles there shouldn't the filter start reporting values again?

@polyfloyd
Copy link
Contributor Author

Good point, that danger does exist. It would indeed be nice to be able to handle this.

We can introduce a timeout where the reference value is reset whenever it expires when no valid values are passed for the specified period.

Another approach could be to implement a sliding window that records all measurements and have the filter only produce values as long as all measurements are within bounds.

I think I would lean towards the second approach. What do you think?

@swoboda1337
Copy link
Member

swoboda1337 commented Dec 21, 2025

Good point, that danger does exist. It would indeed be nice to be able to handle this.

We can introduce a timeout where the reference value is reset whenever it expires when no valid values are passed for the specified period.

Another approach could be to implement a sliding window that records all measurements and have the filter only produce values as long as all measurements are within bounds.

I think I would lean towards the second approach. What do you think?

Instead of a sliding window or timeout could just have a counter? Would count the last number of measurements in bounds and would reset to zero if a measurement is out of bounds? Would pass the value forward if the counter is over a threshold.

This is sort of what I did in my attempt I always set the last value to the current value. That is another option instead of a counter.

bdraco
bdraco previously requested changes Dec 21, 2025
Copy link
Member

@bdraco bdraco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add integration tests in tests/integration

There are some examples of other sensor filters in there already

@esphome esphome bot marked this pull request as draft December 21, 2025 22:23
@esphome
Copy link

esphome bot commented Dec 21, 2025

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@clydebarrow
Copy link
Contributor

clydebarrow commented Dec 21, 2025

Instead of a sliding window or timeout could just have a counter? Would count the last number of measurements in bounds and would reset to zero if a measurement is out of bounds?

I would suggest just making the delta templatable (if it's not already) then it can be the output of another filter using whatever smoothing technique the user determines, rather than trying to turn this filter into a swiss army knife.

Added - probably need two values: a reference (by default the previous value) and a delta, both templatable.

@swoboda1337
Copy link
Member

Could also just add an inverted flag to the regular delta filter and always set last in inverted mode. Simple

@clydebarrow
Copy link
Contributor

clydebarrow commented Dec 22, 2025

Could also just add an inverted flag to the regular delta filter and always set last in inverted mode. Simple

Doesn't solve the problem of what to use as the reference point. Maybe enhance the delta filter so it can take a dict like:

   - delta:
      max_delta: 20%
      min_delta: 10
      baseline: !lambda return id(smoothed_value).state;

Requiring at least one of max_delta and min_delta and where smoothed_value could be a template sensor consuming the raw value of the base sensor with some kind of average. Or enhance the copy sensor to allow choosing the raw value of the source. Or just use two copy sensors, one for the baseline, one that has the delta filter.

The default for baseline would be the previous value of course.

@swoboda1337
Copy link
Member

Yeah having it default to the previous and allowing it to be templateable sounds good to me.

Could do:

   - delta:
      max: 20%
      min: 10%
      baseline: !lambda return id(smoothed_value).state;

@polyfloyd
Copy link
Contributor Author

Instead of a sliding window or timeout could just have a counter? Would count the last number of measurements in bounds and would reset to zero if a measurement is out of bounds? Would pass the value forward if the counter is over a threshold.

It would be dependent on the polling rate, so maybe I would opt for a timeout as a time period but this sounds good to me.

Could also just add an inverted flag to the regular delta filter and always set last in inverted mode. Simple

I am hesitant to do this since it effectively tightly couples the implementations of the delta filter and this filter. It's fine for the implementation I now proposed, but we are discussing timeouts and baselines which would warrant too much complexity to also burden the delta filter with imo.

    - delta:
       max: 20%
       min: 10%
       baseline: !lambda return id(smoothed_value).state;

The baseline sounds neat, but how would we get the smoothed_value? Do we expect users to apply a median before the new delta limit filter? That would force the output of the whole chain to be smoothed too. Or is there a kind of branching of filter chains happening? If so, how would that work?

This filter rejects measurement differences that exceed some
configurable threshold, effectively removing outliers.
@clydebarrow
Copy link
Contributor

clydebarrow commented Dec 22, 2025

Something like this will work. While it's tempting to include features in the filter itself to simplify the usage for your immediate objective, the drawback is that it limits what is possible, whereas this approach makes it infinitely configurable for use-cases that we haven't thought of yet.

sensor:
  - platform: adc
    pin: GPIO4
    id: input_id

  - platform: copy
    id: smoothed_value
    source_id: input_id
    filters:
      - sliding_window_moving_average:
          window_size: 20
          send_every: 1

  - platform: copy
    id: output_value
    source_id: input_id
    filters:
      - delta:
          max: 20%
          min: 1%
          baseline: !lambda return id(smoothed_value).state;

@clydebarrow
Copy link
Contributor

There is a lot of information online about outlier detection, but most apply to batch processing of captured data, not real-time streaming. I did find this one and it could be implemented with two copy sensors to calculate the moving average and standard deviation.

Alternatively it could be implemented as a dedicated filter with just a threshold parameter for the Z-score.

But I think the enhancements as discussed for the delta filter would be useful, and easy to implement.

@polyfloyd
Copy link
Contributor Author

Agreed, this looks like a good solution. I'll see what I can do

@swoboda1337
Copy link
Member

swoboda1337 commented Dec 24, 2025

I also still have concerns about how this is not stable. If say a wireless sensor goes offline for a bit then comes back online. The delta could exceed max and will never be passed though again until the value comes back down (could be never) as the filter never recovers.

@clydebarrow
Copy link
Contributor

I also still have concerns about how this is not stable. If say a wireless sensor goes offline for a bit then comes back online. The delta could exceed max and will never be passed though again until the value comes back down (could be never) as the filter never recovers.

Any thoughts on how to deal with that? If a custom baseline is provided, it's not a problem. Perhaps if the value becomes unknown (NaN) that should be stored into last_value so the next valid value will re-establish the baseline.

@polyfloyd
Copy link
Contributor Author

Regarding the stability for the max_value, the baseline is meant to account for this. I have included a full example in the documentation update linked in the OP

@polyfloyd polyfloyd marked this pull request as ready for review December 26, 2025 19:02
@esphome esphome bot requested review from bdraco and clydebarrow December 26, 2025 19:02
@polyfloyd
Copy link
Contributor Author

Happy new year, all!

Is there anything left that needs to happen before this can be merged?

@clydebarrow
Copy link
Contributor

Is there anything left that needs to happen before this can be merged?

Marking the change requests as resolved is useful as it tells the reviewer that you have addressed the feedback (either made changes or verified they're not required.)

@bdraco bdraco dismissed their stale review January 6, 2026 20:02

Tests added as requested

@polyfloyd polyfloyd requested a review from clydebarrow January 20, 2026 20:37
@clydebarrow clydebarrow merged commit 3c0f43d into esphome:dev Jan 20, 2026
36 checks passed
@clydebarrow clydebarrow changed the title Add the max_delta filter [sensor] Enhance delta filter with a max value Jan 20, 2026
@polyfloyd
Copy link
Contributor Author

Thanks all :)

@polyfloyd polyfloyd deleted the max-delta-filter branch January 21, 2026 09:03
@github-actions github-actions bot locked and limited conversation to collaborators Jan 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants