Updated on September 24, 2026: aligned with Part 3 of the rainwater tank series. Home Assistant now applies templates via reload, without a restart.
This how-to builds on the dry-run protection article and takes it a step further: instead of just protecting against the tank running dry, we make water consumption visible and answer three questions automatically:
- How much water have I used this week?
- How many more days will the rainwater tank last at the current rate of consumption?
- How much rain will I get in the next 7 days, and how many liters is that for my rainwater tank?
This turns the sensor that measures the water level of the rainwater tank into part of the smart home: Home Assistant calculates consumption, supply range and rain yield from the readings.
The third point is especially interesting. Here the system learns your rainwater tank's individual yield factor completely on its own. To do this, it simply compares the measured rain inflow with the local weather forecast.
Prerequisites
- Senvolon level sensor, integrated into Home Assistant via MQTT, with the "Home Assistant" option enabled (MQTT Discovery). A mounted sensor is required as the basis. Which measuring technology suits your rainwater tank and what to watch out for during installation is shown in our overview Measuring and monitoring the water level of the rainwater tank.
- Home Assistant OS, access to the
/configfiles via an add-on such as File Editor or Studio Code Server - A configured weather integration with forecast support (here: Met.no)
- The following entities from the sensor's MQTT discovery:
| Entity | Meaning |
|---|---|
sensor.level_sensor_fluid |
Fill level in liters |
sensor.level_sensor_level |
Absolute fill level in cm |
sensor.level_sensor_distance |
Distance to the surface |
sensor.level_sensor_temperature |
Housing temperature |
sensor.level_sensor_alert |
MIN/MAX alarm |
sensor.level_sensor_rssi |
Wi-Fi signal strength |
sensor.level_sensor_ip |
IP address |
Architecture Overview
Four building blocks that build on each other:
-
Consumption counter: the fill level is not a counter but fluctuates in both directions (drops with consumption, rises with rain or refilling). A
utility_meterapplied directly to the fill level would count every rain shower as a negative delta. So instead we use a helper counter that only sums up drops. - Rain inflow: the mirror image of that, a second helper counter that only sums up rises.
- Supply range: fill level divided by the weekly average consumption.
- Self-calibrating rain yield: comparing the measured rain inflow (building block 2) with the rain amount in mm forecast for the day produces, over time, an empirical liter-per-mm factor for your own rainwater tank, without any assumptions about roof area or material.
Accessing the Config Files on HAOS
On Home Assistant OS, access typically runs through an add-on, for example:
- Settings → Add-ons → Add-on Store
- File Editor for simple text editing, or
- Studio Code Server for a full VS Code-like editor with syntax highlighting (recommended when working on several YAML files in parallel)
After installation, the respective editor icon appears in the sidebar on the left; from there you can reach the /config folder.
Building Block 1: Consumption Counter
A quick note from us: This guide mixes the UI and YAML. Would you prefer everything to be click-based, or everything in one file to copy? Send a quick email to support@senvolon.de, we will build the next how-tos accordingly.
1.1 Creating the Helper Counter
Settings → Devices & Services → Helpers → + Create Helper → Number
- Name:
Zisterne Wasserverbrauch (kumuliert)(keep the helper names in German exactly as shown; Home Assistant derives the entity IDs used below from them) - Minimum:
0, Maximum:1000000, Step size:0.1 - Unit of measurement:
L
Entity ID (automatically generated): input_number.zisterne_wasserverbrauch_kumuliert
1.2 Automation for Summing Up
Settings → Automations & scenes → Create Automation → Edit in YAML
alias: Rainwater Tank Consumption Tracking
description: >
Adds every fill level drop to the cumulative consumption counter.
Increases (rain, refilling) are deliberately ignored.
triggers:
- trigger: state
entity_id: sensor.level_sensor_fluid
conditions:
- condition: template
value_template: >
{{ trigger.from_state is not none and trigger.to_state is not none
and trigger.from_state.state not in ['unknown', 'unavailable']
and trigger.to_state.state not in ['unknown', 'unavailable']
and trigger.to_state.state | float(0) < trigger.from_state.state | float(0) }}
actions:
- action: input_number.set_value
target:
entity_id: input_number.zisterne_wasserverbrauch_kumuliert
data:
value: >
{{ states('input_number.zisterne_wasserverbrauch_kumuliert') | float(0)
+ (trigger.from_state.state | float(0) - trigger.to_state.state | float(0)) }}
mode: queued
1.3 Mirroring with a Template Sensor
The utility_meter needs a sensor with state_class: total_increasing as its source; an input_number alone is not enough. So the value is mirrored:
Helpers → + Create Helper → search for "Template" → Template a sensor
- Name:
Zisterne Wasserverbrauch - State:
{{ states('input_number.zisterne_wasserverbrauch_kumuliert') }} - Unit of measurement:
L - Device class:
Water - State class:
Total Increasing
Pitfall: The template helper type is simply called "Template" in the search, not "Sensor". Searching for "sensor" will not find it, because that only filters helper types with "sensor" in the name (for example derivative sensor, integration sensor).
1.4 Utility Meter (Only Possible via YAML)
Create a new file utility_meter.yaml in the /config root directory:
zisterne_verbrauch_taeglich:
source: sensor.zisterne_wasserverbrauch
cycle: daily
zisterne_verbrauch_woechentlich:
source: sensor.zisterne_wasserverbrauch
cycle: weekly
Include it in configuration.yaml:
utility_meter: !include utility_meter.yaml
Important: The top-level key (utility_meter:) is missing from the outsourced file; it is already set by the!include. The file starts directly with the sensor names at indentation depth 0.
Afterwards: Settings → System → Restart. Utility Meter registers itself at boot; a plain config reload is not enough for that, unlike templates.
After the restart, check in Developer Tools → States: sensor.zisterne_verbrauch_taeglich and sensor.zisterne_verbrauch_woechentlich should exist. The last_period attribute (the value of the most recently completed cycle) is still at 0 at this point; it only fills in after the first fully completed cycle.
Building Block 2: Rain Inflow
The exact mirror image of building block 1, just with the comparison direction reversed.
2.1 Helper Counter
input_number.zisterne_regenzufluss_kumuliert (same configuration as above).
2.2 Automation
alias: Rainwater Tank Rain Inflow Tracking
description: >
Adds every fill level increase to the cumulative rain inflow counter.
Assumes that increases are caused exclusively by rain
(no active refilling/pump top-up in the setup).
triggers:
- trigger: state
entity_id: sensor.level_sensor_fluid
conditions:
- condition: template
value_template: >
{{ trigger.from_state is not none and trigger.to_state is not none
and trigger.from_state.state not in ['unknown', 'unavailable']
and trigger.to_state.state not in ['unknown', 'unavailable']
and trigger.to_state.state | float(0) > trigger.from_state.state | float(0) }}
actions:
- action: input_number.set_value
target:
entity_id: input_number.zisterne_regenzufluss_kumuliert
data:
value: >
{{ states('input_number.zisterne_regenzufluss_kumuliert') | float(0)
+ (trigger.to_state.state | float(0) - trigger.from_state.state | float(0)) }}
mode: queued
2.3 Template Sensor & Utility Meter
Analogous to 1.3: template sensor Zisterne Regenzufluss (state: {{ states('input_number.zisterne_regenzufluss_kumuliert') }}, unit L, device class Water, state class Total Increasing).
Extend utility_meter.yaml:
zisterne_verbrauch_taeglich:
source: sensor.zisterne_wasserverbrauch
cycle: daily
zisterne_verbrauch_woechentlich:
source: sensor.zisterne_wasserverbrauch
cycle: weekly
zisterne_regenzufluss_taeglich:
source: sensor.zisterne_regenzufluss
cycle: daily
A restart is needed again.
Caution: This approach assumes that fill level increases are caused exclusively by rain. Anyone running an active refill in their setup (for example a tap water top-up during dry spells) would incorrectly count that as rain; for this scenario (a plain rainwater tank without a top-up supply), that is not an issue.
Building Block 3: Supply Range
Pure state template logic, done entirely through the UI.
Helpers → Template → Template a sensor
- Name:
Zisterne Reichweite - State:
{% set wochenverbrauch = state_attr('sensor.zisterne_verbrauch_woechentlich', 'last_period') | float(0) %}
{% if wochenverbrauch > 0 %}
{{ (states('sensor.level_sensor_fluid') | float(0) / (wochenverbrauch / 7)) | round(1) }}
{% else %}
{{ none }}
{% endif %}
- Unit of measurement:
d - Device class and state class: leave empty
Pitfall: A naive formula without a guard clause (fluid / (last_period / 7)) throws aZeroDivisionErrorin the live preview as long aslast_periodis still0, that is, during the first week after setup. The{% if %}check with{{ none }}in the else branch instead ensures a cleanunknownstate until the first weekly cycle has completed.
Building Block 4: Self-Calibrating Rain Yield
4.1 Calibration Helpers
Two input_number helpers:
-
Zisterne Kalibrierung mm gesamt(unitmm, minimum 0, maximum 100000, step size 0.1) -
Zisterne Kalibrierung Liter gesamt(unitL, minimum 0, maximum 1000000, step size 0.1)
The units of measurement are not strictly necessary for the calculation (the templates cast to float with | float(0) anyway), but they make the helpers immediately understandable in the overview and on dashboards.
4.2 Calibration Automation
Compares the day's measured rain inflow with the rain amount forecast for the day, daily at 23:50:
alias: Rainwater Tank Rain Calibration Daily
description: >
Compares the measured rain inflow with the rain amount forecast for
today and updates the empirical liter-per-mm factor.
triggers:
- trigger: time
at: "23:50:00"
actions:
- action: weather.get_forecasts
target:
entity_id: weather.forecast_home
data:
type: hourly
response_variable: forecast_data
- variables:
heute: "{{ now().strftime('%Y-%m-%d') }}"
mm_heute: >
{% set forecast = forecast_data['weather.forecast_home'].forecast %}
{{ forecast
| selectattr('datetime', 'match', heute)
| map(attribute='precipitation')
| map('float', 0)
| sum
| round(1) }}
liter_heute: "{{ states('sensor.zisterne_regenzufluss_taeglich') | float(0) }}"
- condition: template
value_template: "{{ mm_heute | float(0) > 0.5 }}"
- action: input_number.set_value
target:
entity_id: input_number.zisterne_kalibrierung_mm_gesamt
data:
value: "{{ states('input_number.zisterne_kalibrierung_mm_gesamt') | float(0) + mm_heute }}"
- action: input_number.set_value
target:
entity_id: input_number.zisterne_kalibrierung_liter_gesamt
data:
value: "{{ states('input_number.zisterne_kalibrierung_liter_gesamt') | float(0) + liter_heute }}"
mode: single
Replaceweather.forecast_homewith your own weather entity ID (Developer Tools → States → filter forweather.).
Important limitation:weather.get_forecaststypically only returns future hours from the time of the call, not the hours of the day that have already passed. So if it rains heavily in the morning, for example, but the rest of the day stays dry, the 23:50 query no longer sees any of that; that period already lies in the past by then.mm_heutetherefore reflects more what was still expected for the rest of the day from the call time onward than what actually fell over the entire day. On a rain day that lasts several hours, the difference is usually small; on short morning showers, the calibration can systematically count mm values that are too low against the liters actually measured, which artificially skews the yield factor upward. Averaged over many rain days, this mostly evens out, but it is not an exact measurement, only an approximation.
Pitfall: Depending on the HA version, Met.no returns the forecast date with or without a time in ISO format. Ifselectattr('datetime', 'match', heute)returns no matches, swapmatch(which only matches from the start of the string) forsearch.
4.3 Yield Factor Sensor
Pure state template logic, done through the UI:
Helpers → Template → Template a sensor
- Name:
Zisterne Ertrag pro mm - State:
{% set mm = states('input_number.zisterne_kalibrierung_mm_gesamt') | float(0) %}
{% set liter = states('input_number.zisterne_kalibrierung_liter_gesamt') | float(0) %}
{% if mm > 5 %}
{{ (liter / mm) | round(2) }}
{% else %}
{{ 30 }}
{% endif %}
- Unit of measurement:
L/mm(add it as a custom entry, since HA does not know this unit; that is not a problem, because no device class is set and HA therefore does not validate the unit)
Until more than 5 mm of calibrated rain is available, the sensor returns a rough placeholder value (30 L/mm); after that, the empirically learned value.
Important to understand:mm_gesamtandliter_gesamtare never reset automatically, so the yield factor is a lifetime average since the very first calibration day, not a rolling average of the last days or weeks. This makes the value very stable and resistant to outliers (a single unusual rain day barely changes it anymore after many calibrated days), but it has a blind spot: if the real runoff characteristics change over time (the roof gets mossy, the gutter clogs up, leaf fall in autumn), the lifetime average reacts only very sluggishly to that, because the earlier, possibly cleaner years keep being weighted in. For getting started, the simple approach is the most pragmatic solution; anyone who later notices that the value shifts noticeably over the years can switch to a rolling average (for example, only considering the last 90 calibration days).
4.4 Rain Yield Forecast (7 Days)
This sensor needs the trigger-based template syntax with weather.get_forecasts and response_variable; no UI helper can represent that, only a YAML template with its own trigger: block.
Create a new file templates.yaml:
- trigger:
- trigger: time_pattern
hours: "/1"
action:
- action: weather.get_forecasts
target:
entity_id: weather.forecast_home
data:
type: daily
response_variable: forecast_data
sensor:
- name: "Zisterne Regenertrag 7d"
unique_id: zisterne_regenertrag_7d
unit_of_measurement: L
icon: mdi:weather-pouring
state: >
{% set forecast = forecast_data['weather.forecast_home'].forecast %}
{% set regen_mm = forecast[:7] | map(attribute='precipitation') | map('float', 0) | sum %}
{% set ertrag_pro_mm = states('sensor.zisterne_ertrag_pro_mm') | float(30) %}
{{ (regen_mm * ertrag_pro_mm) | round(0) }}
attributes:
regen_mm_7d: >
{% set forecast = forecast_data['weather.forecast_home'].forecast %}
{{ (forecast[:7] | map(attribute='precipitation') | map('float', 0) | sum) | round(1) }}
Include it in configuration.yaml:
template: !include templates.yaml
When you add templates.yaml for the first time, restart once (Settings → System → Restart). Later changes to the file are applied via Developer tools → YAML → Template entities without a restart, including trigger-based templates.
Pitfall: The trigger-based sensor does not fire right after the restart, only at the next interval tick of the
time_patterntrigger (here: at the next full hour). Anyone who checks right after setting it up and seessensor.zisterne_regenertrag_7dstill at0 Lorunknownhas not made a mistake; the system simply has not triggered yet. For quick testing, the trigger can be shortened temporarily tominutes: "/2"(afterwards, do not forget to set it back tohours: "/1"and restart again, otherwise the system calls the weather API unnecessarily often).
Dashboard
To avoid losing track given the number of entities, a dedicated dashboard using the newer sections view type is recommended instead of the classic masonry view; with masonry, cards slide into arbitrary columns automatically depending on their height, while with sections the placement stays exactly as you define it.
Settings → Dashboards → + Add Dashboard → New dashboard from scratch, then via the three dots in the top right "Edit in YAML":
title: Rainwater Tank
views:
- title: Overview
path: zisterne
icon: mdi:water-pump
type: sections
sections:
- type: grid
cards:
- type: heading
heading: Live Readings
heading_style: title
- type: entities
title: Level Sensor
entities:
- entity: sensor.level_sensor_fluid
name: Fill level
- entity: sensor.level_sensor_level
name: Absolute fill level
- entity: sensor.level_sensor_distance
name: Distance to the surface
- entity: sensor.level_sensor_temperature
name: Housing temperature
- entity: sensor.level_sensor_alert
name: Alarm
- type: grid
cards:
- type: heading
heading: Consumption & Supply Range
heading_style: title
- type: entities
title: Consumption Counter
entities:
- entity: sensor.zisterne_verbrauch_taeglich
name: Consumption today
- entity: sensor.zisterne_verbrauch_woechentlich
name: Consumption this week
- type: attribute
entity: sensor.zisterne_verbrauch_woechentlich
attribute: last_period
name: Consumption last week
suffix: " L"
- entity: sensor.zisterne_reichweite
name: Water for
- type: grid
cards:
- type: heading
heading: Rain Inflow & Calibration
heading_style: title
- type: entities
title: Rain
entities:
- entity: sensor.zisterne_regenzufluss_taeglich
name: Rain inflow today
- entity: sensor.zisterne_ertrag_pro_mm
name: Yield factor (learned)
- entity: sensor.zisterne_regenertrag_7d
name: Expected yield (7 days)
- entity: input_number.zisterne_kalibrierung_mm_gesamt
name: Calibration base mm
- entity: input_number.zisterne_kalibrierung_liter_gesamt
name: Calibration base liters
- type: grid
cards:
- type: heading
heading: System & Raw Counters
heading_style: title
- type: entities
title: Diagnostics
entities:
- entity: sensor.level_sensor_rssi
name: Wi-Fi signal
- entity: sensor.level_sensor_ip
name: IP address
- entity: input_number.zisterne_wasserverbrauch_kumuliert
name: Consumption (raw counter)
- entity: input_number.zisterne_regenzufluss_kumuliert
name: Rain inflow (raw counter)

You can also integrate individual entities into the previously created pump protection dashboard.

Planning for the Settling-In Period
After the restart, the system is technically complete but does not yet deliver fully meaningful values:
-
Supply range: only valid after the first complete weekly cycle (
cycle: weeklystarts on Monday at 00:00 by default) - Yield factor: only valid once more than 5 mm of calibrated rain has accumulated, so after the first one or two rain days of any significance
- Consumption/rain inflow today/this week: correct immediately, since it is derived directly from live readings
Appendix: All Entities at a Glance
A quick reference for looking things up; the full explanation and the corresponding YAML code can be found in the relevant section above.
Base helpers (number inputs):
-
input_number.zisterne_wasserverbrauch_kumuliert: only ever counts up, adds every fill level drop -
input_number.zisterne_regenzufluss_kumuliert: only ever counts up, adds every fill level increase -
input_number.zisterne_kalibrierung_mm_gesamt: sum of all calibrated rain millimeters since the start -
input_number.zisterne_kalibrierung_liter_gesamt: sum of the liters actually measured on those rain days
Mirrored sensors:
-
sensor.zisterne_wasserverbrauch: consumption counter withtotal_increasing, which makes it usable for Utility Meter -
sensor.zisterne_regenzufluss: the same for the rain inflow
Utility Meter:
-
sensor.zisterne_verbrauch_taeglich: consumption since midnight, resets daily -
sensor.zisterne_verbrauch_woechentlich: consumption since Monday, resets weekly, provideslast_period -
sensor.zisterne_regenzufluss_taeglich: rain inflow since midnight, queried by the calibration
Calculated sensors:
-
sensor.zisterne_reichweite("Water for"): fill level divided by the weekly average, in days -
sensor.zisterne_ertrag_pro_mm: learned liter-per-mm factor, lifetime average -
sensor.zisterne_regenertrag_7d: 7-day rain forecast yield in liters, updated hourly
Automations:
- Rainwater Tank Consumption Tracking: detects fill level drops, feeds the consumption counter
- Rainwater Tank Rain Inflow Tracking: detects fill level increases, feeds the rain inflow counter
- Rainwater Tank Rain Calibration Daily: compares the forecast with the measured value daily at 23:50, feeds the yield factor