The Home Assistant Rainwater Tank Dashboard for Consumption and Rain Forecast with the Senvolon Level Sensor

The Home Assistant Rainwater Tank Dashboard for Consumption and Rain Forecast with the Senvolon Level Sensor

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 /config files 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:

  1. Consumption counter: the fill level is not a counter but fluctuates in both directions (drops with consumption, rises with rain or refilling). A utility_meter applied 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.
  2. Rain inflow: the mirror image of that, a second helper counter that only sums up rises.
  3. Supply range: fill level divided by the weekly average consumption.
  4. 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 a ZeroDivisionError in the live preview as long as last_period is still 0, that is, during the first week after setup. The {% if %} check with {{ none }} in the else branch instead ensures a clean unknown state 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 (unit mm, minimum 0, maximum 100000, step size 0.1)
  • Zisterne Kalibrierung Liter gesamt (unit L, 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
Replace weather.forecast_home with your own weather entity ID (Developer Tools → States → filter for weather.).
Important limitation: weather.get_forecasts typically 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_heute therefore 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. If selectattr('datetime', 'match', heute) returns no matches, swap match (which only matches from the start of the string) for search.

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_gesamt and liter_gesamt are 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_pattern trigger (here: at the next full hour). Anyone who checks right after setting it up and sees sensor.zisterne_regenertrag_7d still at 0 L or unknown has not made a mistake; the system simply has not triggered yet. For quick testing, the trigger can be shortened temporarily to minutes: "/2" (afterwards, do not forget to set it back to hours: "/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: weekly starts 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 with total_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, provides last_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