Lowpass_dt - Time-aware low-pass filter with adaptive deadband - reduce recorder traffic by 10× without losing sensor dynamics

lowpass_dt is a Home Assistant custom integration that filters any numeric sensor with a time-aware low-pass filter (Δt-aware) and publishes only when the signal has moved enough to matter, cutting recorder writes by ~10× with no loss of dynamics.

Unlike existings filters, the filter time constant tau is always expressed in real seconds, so it behaves correctly like a real 1st order low-pass filter even on sensors that update irregularly or slowly. When the source stops updating, automatic silence detection triggers synthetic injections at the natural source rate to keep the filter converged, without flooding the recorder.

The adaptive sigma-based deadband learns the signal variability over time and sets its own threshold. Manual tuning remain possible but is not required, default value works reasonably well.

Key features

  • Correct Δt-aware filtering on any update rate
  • Self-tuning deadband — adapts to signal noise automatically
  • Silence detection with synthetic injection
  • Circular mode for angular sensors (wind direction, 0–360°)
  • Pattern matching — one config line covers dozens of sensors
  • Full state restore across reboots — no warmup after restart
  • Zero dependencies

Minimal config

lowpass_dt:
  patterns:
    - match: "sensor.temperature_*"

:github: GitHub - Cook23/lowpass_dt: Lowpass DT – Deterministic Time-Aware 1st order Low Pass Filter for Home Assistant · GitHub

Green is a noisy source (wind speed), red is a very stable one (outdoor temperature) — note the multiple updates for nearly identical temperature values.

Stepped representation is used here to make updates visible. A curve representation would of course be smoother and more visually appealing.

Before:

After:

2 Likes

Wow this is gamechanging. It might even need to become a default feature, with a setting that can be enabled as an aspect of each numeric sensor.

Update — what’s new since the initial post

Several improvements have been made since the initial release:

Improved graph representation during silence periods

When a source resumes after silence, an end-of-silence marker is now published just before the first real measurement. This ensures graph renderers draw a flat horizontal line at the last known value during the silence period, rather than interpolating diagonally from the last known point to the new measurement.

A new silence parameter controls what value is published after the filter converges during silence:

  • last (default) — holds the last known value
  • zero — useful for power/current sensors where silence means the device is off and the source failed to transmit that final zero
  • unknown — when the value during silence is genuinely indeterminate

total and total_increasing sensors are not affected — diagonal interpolation is the correct representation for accumulating quantities.

Improved handling of malformed source values

Sources that concatenate the unit to the state value (e.g. 40%, 23°C) are now parsed correctly. The numeric value is extracted and the unit set automatically if not already provided by the source attributes.

Designed to work with history-explorer-card fork

This fork of History Explorer Card is specifically adapted to render filtered sensor history with full awareness of lowpass_dt silence markers — flat lines during silence periods instead of misleading diagonal interpolation. The two are designed to work together.


Configuration examples

The simplest possible config — one line covers an entire integration, all parameters auto-configured:

lowpass_dt:
  patterns:
    - match: "sensor.ecowitt_*"

For power sensors where silence should publish zero (device turned off):

lowpass_dt:
  patterns:
    - match: "sensor.puissance_*"
      silence: zero

Circular mode for wind direction, everything else auto:

lowpass_dt:
  sensors:
    - source: sensor.wind_direction
      circular: 360
  patterns:
    - match: "sensor.ecowitt_*"
    - match: "sensor.air_quality_*"

My full production config — 174 sensors, no per-sensor tuning:

lowpass_dt:
  sensors:
    - source: sensor.ecowitt_wind_direction
      circular: 360
    - source: sensor.ecowitt_10min_avg_wind_direction
      circular: 360
  patterns:
    - match: sensor.ecowitt_*
    - match: sensor.air_quality_*
    - match: sensor.energie_*
    - match: sensor.mesure_*
    - match: sensor.pertes_*
    - match: sensor.puissance_*

And the recorder configured to only record filtered entities — the raw sources are excluded entirely:

recorder:
  purge_keep_days: 90
  commit_interval: 60
  include:
    entity_globs:
      - sensor.lp_*

Last version works reasonably well in my Home Assistant instance with 174 sensor variables filtered in batch mode with no specific tuning. Even malformed sources are now handled as gracefully as possible.