Bayesian State Filter — fusing multiple numeric sensors into one robust estimate

I have been experimenting with combining multiple temperature sensors in Home Assistant for a while, and it eventually grew into a separate custom integration:

Bayesian State Filter

GitHub: GitHub - pterodaktil02/bayesian_state_filter: Bayesian State Filter for Home Assistant · GitHub

One important clarification first: this is not an alternative implementation of Home Assistant’s built-in bayesian integration.

The built-in bayesian integration estimates the probability of an event and produces a binary_sensor.

bayesian_state_filter, on the other hand, is intended for numeric sensors and continuous-valued quantities. For example, if you have several temperature sensors in the same room, it produces a single estimate of the shared latent “true” room state.

Conceptually:


sensor 1 ─┐
sensor 2 ─┼─> Bayesian State Filter ─> fused numeric sensor
sensor 3 ─┤
sensor 4 ─┘

The filter accounts for different sensor offsets, different effective noise levels, different update rates, and asynchronous measurements.

One of the main reasons I built it was outlier handling and sensors that temporarily drift away from the rest of the ensemble. State updates use Student-t weighting: a strongly inconsistent observation is not simply discarded, but its influence is automatically reduced.

Each source gets its own relative bias and effective sigma. Source uncertainty is calibrated from close-in-time pairs of measurements:


Var(sensor_i - sensor_j) ≈ sigma_i² + sigma_j²

This helps separate actual process dynamics from source-specific observation noise.

At startup, the integration can use Home Assistant Recorder history for initial calibration and pretraining. It also estimates a characteristic time scale of the observed process using a robust temporal semivariogram.

The current version supports both Gaussian and Poisson-family observation models. The latter is useful for count-based sensors. Automatic Gaussian/Poisson detection is not implemented yet: in version 0.2.1.6, noise_model: auto intentionally defaults to Gaussian.

Example configuration:


sensor:
  - platform: bayesian_state_filter
    name: "Room temperature"

    ensemble:
      sources:
        - sensor.temperature_1
        - sensor.temperature_2
        - sensor.temperature_3
        - sensor.temperature_4
      min_sources: 2

    bayes:
      noise_model: auto
      history_days: 7
      save_every_s: 60
      student_nu: 4
      diagnostics: compact

The resulting entity exposes diagnostics both for the overall filter and for each individual source, including bias, sigma, innovation, z_score, robust weight, outlier counters, and the most recent per-source update diagnostics.

The component is already being used and tested on a real Home Assistant installation. The repository also includes regression tests, including synthetic source-calibration tests and characteristic-time estimation tests.

Current version: 0.2.1.6

Configuration is YAML-only for now. Config Flow, broader Home Assistant integration tests, and automatic Poisson/Gaussian model detection are planned for later versions.

License: GPL-3.0.

Feedback is very welcome, especially real datasets where the filter behaves strangely. For this kind of estimator, one bad real-world example is often more useful than ten beautiful synthetic tests.

1 Like

Since the original post, the integration has changed quite a bit. The current version is now 0.3.2.

The main change is the internal process model.

Previously the filter mainly estimated the current level and local trend. It now always maintains a four-dimensional state:

[x, v, a, j]

where:

x = level
v = rate of change
a = acceleration
j = jerk

The higher-order terms are not always used at full strength. Their influence depends on how confidently they are observed in the posterior.

The effective weights are hierarchical:

w_v = c_v
w_a = c_v * c_a
w_j = c_v * c_a * c_j

So, for example, if the data clearly show a slope but do not support acceleration, the filter can use velocity strongly while suppressing acceleration and jerk.

This turned out to be useful in practice: pressure data often contain meaningful short-term dynamics, while very noisy signals such as radiation background may naturally collapse back toward a mostly level-only estimate.

Dynamics identification was also changed. Instead of validating derivatives over long arbitrary forecast horizons, the model is now fitted and checked on the natural sampling cadence of the process, typically using 1-step and 2-step local prediction errors.

Restart behavior has been reworked as well.

The integration now saves a checkpoint every 30 minutes containing:

state [x, v, a, j]
covariance matrix
learned process dynamics
source calibration
characteristic-time estimate
Recorder watermarks

After a Home Assistant restart, it restores the checkpoint and replays only the Recorder tail recorded since that checkpoint.

So instead of retraining from several days of history after every restart, normal startup is now effectively:

load checkpoint
-> replay recent Recorder tail
-> continue live

Source calibration has also been improved.

The pairwise bias calibration only determines relative offsets between sensors. The common offset of all biases is mathematically unidentifiable, so without an explicit constraint the whole ensemble could slowly drift in absolute level while all pairwise differences remained perfectly valid.

Version 0.3.2 fixes this by enforcing:

median(bias_i) = 0

This keeps the ensemble centered while preserving all relative bias corrections between sensors.

A few other changes since the original post:

  • YAML configuration can now be reloaded with bayesian_state_filter.reload
  • startup and live sigma calibration are now continuous
  • Recorder-derived calibration evidence ages out gradually instead of being replaced immediately by a short burst of live data
  • diagnostics now expose rate, curvature, jerk, their confidence weights and local prediction RMSE
  • Student-t handling was refined so good inliers may receive a weight slightly above 1, while outliers are still smoothly downweighted
  • the regression suite was updated for the new 4D state model, checkpoint restore and bias-gauge behavior