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 %}`