Asymmetrically-damped sensor

I have a hot water cylinder heated via a gas boiler that supplies the house hot water including a power shower. The water is heated via a coil, like this:

I have three DS18B20 temperature sensors on the tank: one at the top buried in the insulation next to the electric immersion heater (not shown on diagram), and one cable-tied to both flow and return pipes as close to the tank as possible.

From these sensors, I derive a weighted “tank heat charge” percentage figure into a template sensor, which is shown on my Home Assistant page and gives a good idea how much hot water is in the tank: useful for knowing if there’s enough for a shower or if the boiler needs to be run first.

However, when the boiler does run, the two sensors on the coil immediately report a very high temperature and totally skew the measurement whilst the water is being heated: the “charge” jumps to 100%.

What’s needed is a “damped” sensor that has the physical aspects of the tank in its configuration: the rate of heating should not be too quick, although the rate of discharge can be quicker — a shower removes heat faster than the boiler can replenish it.

I did experiment with the filter integration and its lowpass filter, trying to combine a “fast” and a “slow” filtered version of the raw value, but ran into issues over updates — if the calculated value jumps to 100% and stays there, it does not send new values to the filter, which gets stuck at a lower value.

Eventually I designed this setup:

  • raw sensor value
  • an input_number
  • a smoothed template sensor reflecting the value of the input_number
  • driven by this automation:
- alias: Tank value update
    - platform: state
      entity_id: sensor.time
  condition: []
    - service: input_number.set_value
        entity_id: input_number.hw_tank_smooth_value
        value: >
          {% set told = states('input_number.hw_tank_smooth_value')|float %}
          {% set tnew = states('sensor.hw_tank_heat_pct_raw')|float %}
          {% set max_up_per_min = 2 %}
          {% set max_down_per_min = 10 %}
          {% if tnew > told %}
            {{ [tnew, told + max_up_per_min]|min|round(0) }}
          {% else %}
            {{ [tnew, told - max_down_per_min]|max|round(0) }}
          {% endif %}

So every minute, triggered by an update from sensor.time, it compares the prior value stored in the input_number and adjusts it in line with the new raw value, but with the change limited to the max_up_per_min and max_down_per_min variables. In my setup, the maximum change is +2% or -10% per minute.

Here’s a plot with the drop from my 06:50 morning shower and then the heating cycle from the boiler starting at 07:45:


Seems to work well. I’ve posted it here a) in case it helps someone else and b) to see if I’ve missed a simpler way to do it, as the whole input_number thing seems a bit of a bodge.


Your setup sounds like what I need, appreciate the write up on the config part.

Any chance you could post some details about how you physically set this up, what did you buy, any code required, that sort of things ?
I’m getting tired of realizing I have no hot water until I’m already in the shower, I’d really like to monitor this too.

Thanks !

I use three DS18B20 sensors, all in the TO92 package (like a transistor) rather than the waterproof type in a chrome cylinder. These are wired together on the same 1-Wire bus (actually three wires required: I used stripped-out Cat5e cable with the data and ground as a twisted pair).

That bus then connects to my central heating controller which is a Wemos D1 Mini ESP8266 device, taking over the job from the Danfoss “dumb” proprietary unit I used to have. That unit also switches the hot water and central heating on and off, but if you just want the measurement you don’t need any of the relay hardware.

Code is with ESPHome: start here:

That provides HA with three temperature readings. I then combine these with a template sensor to provide the “raw” value referred to above:

    - name: "Hot water raw tank charge"
      unique_id: f4a53dbd-12da-40d9-9f45-a71c143d2e67
      unit_of_measurement: '%'
      icon: mdi:thermometer
      state: >-
        {{ ((([0, [50, (states('sensor.tank_top')|float(0.0))]|min - 35]|max) * 1.0) +
            (([0, [55, (states('sensor.coil_inlet')|float(0.0))]|min - 35]|max) * 3.75) +
            (([0, [40, (states('sensor.coil_return')|float(0.0))]|min - 35]|max) * 2))|round(2) }}

Those weightings are trial-and-error approximations for my tank, with 35°C being “cold” and the other numbers being “fully hot” for that segment. For example, if the immersion has been running such that the top sensor is over 50°C but the other two are below 35°C, it reports 15% ((50-35)*1.0); and if they’re all above their “hot” limit, it adds up to 100%.

The automation above then smooths them and stores the value in the input_number; and I have a second template sensor that reads this and provides a sensor value with icon:

    - name: "Hot water tank charge"
      unique_id: c2d75699-11bf-4aa2-a337-b26f282be395
      unit_of_measurement: '%'
      icon: >-
        {% if states('binary_sensor.hw_running') == 'on' or
              states('sensor.immersion_switch_power')|int(0) > 100 %}
        {% elif states('sensor.hot_water_tank_charge')|int(0) < 10 %}
        {% elif states('sensor.hot_water_tank_charge')|int(0) < 100 %}
            mdi:battery-{{ 10-(states('sensor.hot_water_tank_charge')[0]|int(0)) }}0
        {% else %}
        {% endif %}
      state: "{{ states('input_number.hw_tank_smooth_value')|round(0) }}"

There’s probably an easier more compact way in the new era of trigger-based template sensors, but this works for me.

My hot water scheduling is described in this post:

and then optimised further in this one:


Just ordered what I needed to give that a try myself, thanks for the writeup !
And to confirm what I read, using the TO-92 package as well, I do need the 4.7 komh resistor between the data and vnd pin of the sensor ? That’s one for each sensor, correct ?


Yes, that’s correct.

Alright, it’s now in place. It’s all wired to a breadboard for now, if I’m happy with the result I’ll have to come up with a cleaner way to package and power the esp-01s I’m using but it should be fine for a few days while I test it out.

I just re-used your numbers as is, but I’m sure I’ll have to tweak them. Looks like the top sensor is reading about 50 right now and the middle / bottom around 20, giving me a charge of 13% (which sounds about right). Since the immersion is off for the winter I was expecting the opposite but maybe it’s not plumbed the same way, I’ll have to see when it runs.

Thanks for the write up, looks good so far !

It’s probably right. If you’ve heated the tank up with the coil then drawn a lot of hot water off (replaced by cold water from the bottom), you’d expect the top to be hot and the bottom to be cold.

The coil isn’t on right now, I’m only using the boiler. Here are the temperatures it’s seeing for the past day :
Not sure what that hole yesterday is, maybe I unplugged the wifi AP or something.

And the damped sensor for the charge :

It’s pretty clear where the boiler is running and where someone is taking a shower.

I haven’t gone all the way to 0 yet so I can’t be sure it’d actually be cold, but it’s looking pretty good.
I might decrease the increment value as it always seems to overshoot the charge a bit while the boiler is running and then fall immediately down by a few percents when the boiler stops, but that just a bit of tweaking.

That’s what I mean: the coil is fed from the boiler. See the diagram at the top.

The sensor setup isn’t perfect, but it’s better than nothing. Don’t get obsessed with trying to make it perfect :slight_smile: .