Capsule shaped rainwater tank - volume and fill percentage calculation

I searched around and did not find anything like this so I built it myself and wanna share it, since it might be useful for others.

I have a large 10’000L rainwater tank in my garden that is used for irrigation. I will install an Ecowitt laser distance sensor to monitor its fill level and switch my irrigation to water saving and/or refill it using well water.

The tank is - like most underground tanks - so called “capsule” shaped:

This obviously means that the water volume does not correspond linearly to the water level.

In my case the tank has a diameter of 2 meters (so radius R = 1 m) and an overall length (L) of 4.2 meters.

I created this template sensor to calculate liters depending on a sensor giving the heigt of the water level (h) (sensor.tank_level). All measures are SI conforming (meters / liters).

DISCLAIMER: Since I am a total YAML-Idiot I had chat GPT draw up most of the code for this. While it was wrong initially, it was enough for me to understand the basic concept and fix it.

If this is the first template sensor in your configuration.yaml (easiest accessed via file editor) you’ll need to once add:

template:
  - sensor:

and then comes the sensor:

      - name: "Rain Tank Liters"
        unit_of_measurement: "L"
        state: >
          {% set h = states('sensor.tank_level') | float(0) %}
          {% set R = 1.0 %}
          {% set L = 4.2 - 2 * R %}
          {% set pi = 3.141592653589793 %}

          {% if h <= 0 %}
            0.0
          {% elif h >= 2 * R %}
            {{ ((pi * R**2 * L + (4/3) * pi * R**3) * 1000) | round(1) }}
          {% else %}
            {% set cyl_area = R**2 * acos((R - h) / R) - (R - h) * (2 * R * h - h**2)**0.5 %}
            {% set cyl_volume = L * cyl_area %}

            {% if h <= R %}
              {% set cap_volume = (pi * h**2 * (3 * R - h)) / 3 %}
            {% else %}
              {% set cap_volume = (4 / 3) * pi * R**3 - (pi * (2 * R - h)**2 * (h - R)) / 3 %}
            {% endif %}

            {{ ((cyl_volume + cap_volume) * 1000) | round(0) }}
          {% endif %}

A second template sensor then generates fill percentage:

      - name: "Rain Tank Percentage"
        unit_of_measurement: "%"
        state: >
          {% set volume = states('sensor.rain_tank_liters') | float %}
          {% set max_volume = 10000 %}
          {{ ((volume / max_volume) * 100) | round(0) }}

Testing was positive so far, I will update if I find any bugs / optimizations.

Inputs and questions always welcome.




UPDATE:

The above Sensor did not work. It produced weird jumps in volume at the 50% mark that did not correlate with the fill heigth at all.

I tried to optimize stuff using chatGPT (because I’m not a coder myself) and ultimately it seems to come down to the fact that Jinja templating does not support complex funtions such as acos - and instead of throwing an error it just fails silently! GPT suggested importing some Python math library into the templating engine, but that did not work.

I ended up having GPT calculate a lookup table for the tank volume as well as a sensor based on said table. It seems to be working much more reliably so far.

Also I remeasured the interior dimensions of the tank and realized it is (obviously) smaller than the outside dimensions. That is why the max fill heigt in the table is now 1854mm giving me the exact 10’000L the tank is rated for.


      - name: "Rain Tank Liters"
        unit_of_measurement: "L"
        state_class: measurement
        device_class: volume
        state: >
          {% set h = states('sensor.rain_tank_fill_height') | float(0) %}
          {% set min_volume = 600 %}

          {% set lookup = [
            (0.000, 0),
            (0.103, 142),
            (0.206, 458),
            (0.309, 851),
            (0.412, 1341),
            (0.515, 1942),
            (0.618, 2618),
            (0.721, 3368),
            (0.824, 4148),
            (0.927, 4970),
            (1.030, 5768),
            (1.133, 6555),
            (1.236, 7309),
            (1.339, 7986),
            (1.442, 8549),
            (1.545, 9039),
            (1.648, 9421),
            (1.751, 9764),
            (1.854, 10000)
          ] %}

          {% if h <= lookup[0][0] %}
            0
          {% elif h >= lookup[-1][0] %}
            {{ ((lookup[-1][1] - min_volume) if (lookup[-1][1] > min_volume) else 0) | int }}

          {% else %}
            {% for i in range(lookup|length - 1) %}
              {% set h1, v1 = lookup[i] %}
              {% set h2, v2 = lookup[i + 1] %}
              {% if h >= h1 and h < h2 %}
                {% set ratio = (h - h1) / (h2 - h1) %}
                {% set interpolated = v1 + ratio * (v2 - v1) %}
                {{ ((interpolated - min_volume) if (interpolated > min_volume) else 0) | int }}
                {% break %}
              {% endif %}
            {% endfor %}
          {% endif %}`

Hello Nicolas,

Use this…

You will have to experiment around to get the numbers to draw the curvy line, but it will work…

Empty, get a number. Add a liter, get a number, etc…
That will give you distance at 0%, 10%, etc. for those locations. Use the integration by feeding it the current distance and it will give you continuous reading on the % based on the curve you generated in the integration…

Interesting. Thanks!

The mathematical approach I built works perfectly well for what I want it to do, so I’ll leave it at that for now.

But I’ll keep the Compensation integration in mind, it sure is very useful for other applications!

You don’t need to do this. pi is a constant that is already defined. So you can delete that line.

Unless you want to define pi as 3.0 or something else.

Oh - ok! I actually tried that initially, but then it didn’t work so I put it in there. Probably the mistake was another one and once it worked I just didn’t touch it anymore so not to break it…

I sure don’t want pi to be anything other than pi!

I’ll update that, thank you!

It was a joke.

Ha - Yeah I did realize it wasn’t a serious suggestion, but didn’t know that specific piece yet. Very nice! :slight_smile: