Best way to get the derivative (rate of change) of a signal which is noisy and sparse?

Hello everyone

tl;dr I want the derivative (rate of change) of a signal, and I’m doing it terribly wrong

I have a sensor which provides the temperature of my Domestic Hot Water (DHW). What I’m trying to do is to get the rate of change of this temperature, so as to detect whenever I’m using DHW (thus lowering the temperature) or when I’m re-heating the water.

The raw data itself is a bit noisy, especially when the sensor “hesitates” between two values within its threshold. As a result, when I take the derivative of that, I get something that is super jumpy and not really usable. Here is a graph of the raw data, and of its derivative:

As you can see, there is a lot of jumpiness, and also I have a non-zero derivative when the temperature is not moving (which is wrong: when the temperature is not moving, its derivative should be zero).

These are the settings I used for the derivative:

  - platform: derivative
    source: sensor.espaltherma_temp_ecs
    name: Changement température ECS brute
    round: 3
    unit_time: h # the resulting "unit_of_measurement" will be °C/h if the sensor.temperate has set °C as its unit
    time_window: "00:01:00" # we look at the change over the last minute

First attempt

So my first idea was to smooth out the data before applying the derivative.

These are the settings I used for the filter integration:

  - platform: filter
    name: "Température ECS filtrée sans helper"
    entity_id: sensor.espaltherma_temp_ecs
    filters:
      - filter: outlier
        window_size: 4
        radius: 1.0
      - filter: lowpass
        time_constant: 15
        precision: 6
      - filter: time_simple_moving_average
        window_size: "00:02:00"
        precision: 6

The filtering itself was disappointing. The filtered values were often quite far from the actual data, and when taking the derivative of the filtered data, the values weren’t much more meaningful than previously.

This is when I started to believe that part of my problem is that I only have raw data points when there is a change of value of the DHW temperature. I THINK (?) that as a result, the filter integration had an inconsistent number of points to work with, yielding bad results.

Second attempt

This time I went completely overboard in my opinion. I told myself that since I couldn’t get good filtered values from sparse data, I would create a “continuous” version of my DHW data. With one point every 5 seconds, the filter integration could yield a smooth temperature curve, and then I could get a nice derivative of that.

Here is what I did:

  • Create a “Number” Helper, for storing DHW data
  • Create an automation that fires every 5 seconds, and that stores the current raw DHW value into the helper
  • Filter the data stored in the helper
  • Take the derivative of the filtered data

So this is super tedious, and also takes a huge hit on the volume of data stored in my recorder. However, the results are starting to look adequate.

Here’s a view of all three approaches at once:

My question(s)

What is the best way to get the derivative of a signal which is noisy and sparse?

I feel that what I’m doing here is terribly wrong.

Am I wrong in the method?

Am I wrong in the settings of the components I’m trying to use?

I feel that what I’m doing can’t possibly be the best way to do this.

Thank you for reading this far!

1 Like

I think what you are doing is close to the best solution. Instead of using an input number and an automation I would have used a triggered template sensor.

Five seconds may be a bit of overkill. Try with longer periods. 10 to 30 seconds should work.

Once you have checked the data fits well you can exclude the unwanted data from the 5 second triggered sensor from the recorder to decrease your database activity. The filter and trend sensors work by taking samples as they occur, not from the recorder database, so they will still work.

3 Likes

Thank you for your quick answer! Your tips look very promising, I can’t wait to give them a try!

Again, thank you very much for your answer!
Not only is the process more straightforward, but my recorder also doesn’t need to store nearly as much data!

1 Like

I’m very glad that I found this link because I’m struggling with exactly the same issue. However, I need a ‘sensitive’ sensor because I want to open the gate to the property when a ‘known’ car (with a BLE beacon) approaches the gate. The issue is that the measurement of the distance from the BLE beacon to the sensor is very noisy. I want to use Derivative to calculate the speed, but if I filter too much or take the derivative over a longer period the detection of change in speed, or location, will be too slow to open the gate automatically.

What I did was filter the Derivative rather than the sensor readings. This seems to work well. My code is below.

- platform: mqtt_room
  device_id: "xxxxxxxx9bbb"
  name: 'Car - Fiat'
  state_topic: 'espresense/rooms'
  timeout: 60
  away_timeout: 120 # number of seconds after which the enitity will get status not_home

- platform: mqtt
  state_topic: espresense/devices/xxxxxxxx9bbb/gate
  value_template: '{{ value_json.distance }}'
  unit_of_measurement: 'm'
  name: 'Distance - Car - Fiat - Gate'

- platform: mqtt
  state_topic: espresense/devices/xxxxxxxx9bbb/entrance
  value_template: '{{ value_json.distance }}'
  unit_of_measurement: 'm'
  name: 'Distance - Car - Fiat - Entrance'

- platform: mqtt
  state_topic: espresense/devices/xxxxxxxx9bbb/office
  value_template: '{{ value_json.distance }}'
  unit_of_measurement: 'm'
  name: 'Distance - Car - Fiat - Office'

- platform: mqtt_room
  device_id: "tile:xxxxxxxxc0ab"
  name: 'Car - Merc'
  state_topic: 'espresense/rooms'
  timeout: 60
  away_timeout: 120 # number of seconds after which the enitity will get status not_home

- platform: mqtt
  state_topic: espresense/devices/tile:xxxxxxxxc0ab/gate
  value_template: '{{ value_json.distance }}'
  unit_of_measurement: 'm'
  name: 'Distance - Car - Merc - Gate'

- platform: mqtt
  state_topic: espresense/devices/tile:xxxxxxxxc0ab/entrance
  value_template: '{{ value_json.distance }}'
  unit_of_measurement: 'm'
  name: 'Distance - Car - Merc - Entrance'

- platform: mqtt
  state_topic: espresense/devices/tile:xxxxxxxxc0ab/office
  value_template: '{{ value_json.distance }}'
  unit_of_measurement: 'm'
  name: 'Distance - Car - Merc - Office'

- platform: derivative
  name: Fiat Speed
  source: sensor.distance_car_fiat_gate
  round: 1
  unit_time: s 
  time_window: "00:00:05"

- platform: derivative
  name: Merc Speed
  source: sensor.distance_car_merc_gate
  round: 1
  unit_time: s
  time_window: "00:00:05"

- platform: filter
  unique_id: filtered_fiat_speed
  name: Filtered Fiat Speed
  entity_id: sensor.fiat_speed
  filters:
  - filter: outlier
    window_size: 4
    radius: 4.0
  - filter: lowpass
    time_constant: 10
    precision: 2

- platform: filter
  unique_id: filtered_merc_speed
  name: Filtered Merc Speed
  entity_id: sensor.merc_speed
  filters:
  - filter: outlier
    window_size: 4
    radius: 4.0
  - filter: lowpass
    time_constant: 10
    precision: 2

1 Like

I’m just started with HA, but experienced the same problem when setting up a PID towards a Dalla 1-wire sensor. As you said, the problem is where when the sensor shall decide what level to send out when it’s exactly between two discrete levels.

As the sensors was connected to a SEP module and I used ESPHOME, I filtered the sensor already at the ESP level using this code:

dallas:
#  - pin: GPIO1
  - pin: GPIO3
    update_interval: 5s
# Individual sensors

sensor:
  - platform: dallas
    address: 0x8b3c01d0751cac28
    name: "Temp sensor"
    filters:
      - sliding_window_moving_average:
          window_size: 12
          send_every: 12

As I remember. This will give an average window of 60 seconds (update interval of 5 seconds and windows size of 12 equal 60 seconds).

I could have used send_every=1, as this would have sent out a temperature at every 5’th second (at each sample), but I decided to decrease the output rate to 60 seconds. For the PID specifically, it’s advised to send out data with as high frequency as possible, but I never got that to work since the derivate contribution then just got wild and crazy

1 Like