Detect a water tank leak from behaviour
A low-level alarm tells you when the tank is already low. This design looks for an unexplained rate of loss while there is still time to find a leaking pipe, running trough, failed valve or unexpected pump demand.
The fault this project detects
The target is unexplained water loss, not ordinary consumption. A falling tank is normal while irrigation, stock watering or household use is active. It becomes suspicious when the fall is faster than this property's normal draw, persists, and no known use is enabled.
The worked layout is a rural NSW rainwater tank with a volume sensor in litres, an energy-monitored pressure pump, Home Assistant and a Toggle helper that marks known water use. The logic also works with a bore header tank, but the calibration must be redone.
The evidence model
A useful alarm does not promote one noisy reading straight to an emergency. It combines measurements that fail in different ways:
- Volume rate. A Derivative helper converts tank volume into litres per hour, making a slow but persistent draw visible.
- Expected-use context. Irrigation, a scheduled transfer or deliberate household use suppresses the leak interpretation without hiding the raw rate.
- Pump evidence. Unexpected pressure-pump operation suggests demand downstream; loss with the pump off points towards the tank or gravity-fed side.
- Persistence. The condition must outlast sensor slosh, rain input, pump starts and level-sensor quantisation.
Home Assistant cannot prove where the water went. It can prove that the measured behaviour does not match the states that should explain it, which is the useful moment to inspect the property.
Freshness is separate evidence
An entity can keep a plausible old value without becoming unavailable. Inspect last_changed on a source that should genuinely change, and work out the longest gap you'd expect between meaningful state changes on a normal day by reading that entity's own History graph over a few representative days. If the measured process can legitimately stay flat, add a changing source heartbeat or timestamp; do not treat an unchanged temperature, level or power value as proof that the transport is alive.
Rate maths: never copy a gradient
The Trend integration expresses min_gradient in units per second. The arithmetic example from the project brief is 4 °C/hour ÷ 3600 = 0.00111 °C/s. That is an example conversion, not a threshold for this equipment. Calculate the rate from your own timestamped readings: pull two points from the tank volume sensor's History graph, take the change between them, divide by the number of seconds between those two timestamps, and that gives you the site-specific gradient for your tank. A Derivative helper can instead display a friendlier per-hour unit when configured that way.
Collect this data before choosing a threshold
Export a quiet-day tank curve and at least one known-use event. If the tank geometry is irregular, verify that the reported litres are already volume-corrected; a percentage derivative is not automatically a litres-per-hour derivative.
| Measurement | Why it matters | Required value |
|---|---|---|
sensor.rainwater_tank_volume | Normal settled volume and sensor resolution | Read the normal settled litre value and how often it reports, both straight off this sensor's History graph |
sensor.rainwater_tank_volume_rate | Normal idle loss and deliberate draw rates | Compare a quiet-day L/h range against a known-draw L/h range from the same History graph |
binary_sensor.pressure_pump_running | Pump start, stop and longest normal run | Note the start/stop timestamps and the longest normal run length you see in this entity's History |
input_boolean.water_use_expected | Known irrigation, transfer and heavy-use periods | Base this on your own irrigation schedule or manual rule for when a legitimate draw is happening |
weather event context | Rainfall or refill events that make volume rise | Capture a timestamped refill curve from a real rain event or top-up on your own tank's History |
The experiment that makes this article defensible
- Record a normal day with all known use annotated, then calculate idle tank loss in L/h.
- Run one deliberate, measured draw and capture tank volume, volume derivative and pump state at matching timestamps.
- Stop the draw and measure how long sensor slosh or filtering takes to settle.
- If it can be done without wasting water or creating damage, stage a very small controlled draw and confirm the Warning logic detects it without becoming Critical.
Keep your own record as you go: save the timestamped readings from your test cycles, note the Home Assistant version you're running, note the exact entity IDs you used, and take one or two History or dashboard screenshots. Do not stage an unsafe electrical, refrigeration, pressure, battery or water fault merely to make a graph.
Create the helpers
Use Settings → Devices & services → Helpers. Create the named Number or Toggle helpers before loading the templates. Where this guide uses a Derivative, Statistics, History Stats, Integral, Schedule or Utility Meter helper, the current Home Assistant interface can create it; the source links below describe the current options.
| Helper or derived entity | Purpose | Setting |
|---|---|---|
sensor.rainwater_tank_volume_rate | Derivative helper; source is tank volume; output in L/h | Widen the smoothing window until sensor slosh settles out on your own History graph, but no further than that |
input_number.tank_leak_warning_l_per_hour | Unexplained loss rate that deserves review | Set this above the quiet-day L/h loss rate you measured, so a real leak still trips it |
input_number.tank_leak_critical_l_per_hour | Loss rate that warrants immediate inspection | Set this well above the warning threshold, at a loss rate that would need urgent attention on your own property |
input_number.tank_leak_hold_minutes | Persistence before Warning | Set this long enough to outlast the sensor slosh and pump starts you saw in your own History |
input_number.tank_leak_critical_hold_minutes | Persistence before Critical | Set this to a shorter, more urgent persistence window than the Warning hold, based on the same History evidence |
input_boolean.water_use_expected | On during a known legitimate draw | Leave this off whenever the tank should be idle, and only switch it on for a genuine known draw |
input_boolean.tank_leak_monitoring_enabled | Master notification gate | Leave this off until you've validated every threshold above against your own data, then switch it on |
input_number.tank_leak_critical_repeat_minutes | Critical repeat spacing | Choose any repeat interval greater than zero that matches how urgently you want reminders |
Current Home Assistant YAML
Merge top-level keys with your existing configuration instead of duplicating them. Replace the example entity IDs consistently. If you keep automations in automations.yaml, paste only the automation list items there, without a top-level automation: wrapper.
Combine rate, context and pump state
Create the Derivative helper in the UI first and confirm that a falling tank produces a negative L/h value. The Number helpers store positive magnitudes, so the templates negate them for comparison.
template:
- binary_sensor:
- name: "Tank loss unusual"
unique_id: tank_loss_unusual
default_entity_id: binary_sensor.tank_loss_unusual
device_class: problem
availability: >
{{ has_value('sensor.rainwater_tank_volume_rate')
and has_value('input_number.tank_leak_warning_l_per_hour') }}
state: >
{{ (states('sensor.rainwater_tank_volume_rate') | float)
<= -(states('input_number.tank_leak_warning_l_per_hour') | float) }}
- name: "Tank leak warning"
unique_id: tank_leak_warning
default_entity_id: binary_sensor.tank_leak_warning
device_class: problem
delay_on:
minutes: >
{{ states('input_number.tank_leak_hold_minutes') | int(0) }}
state: >
{{ is_state('binary_sensor.tank_loss_unusual', 'on')
and is_state('input_boolean.water_use_expected', 'off') }}
attributes:
reason: >-
Tank volume is falling at
{{ states('sensor.rainwater_tank_volume_rate') }} L/h
while known water use is off. Pump running:
{{ states('binary_sensor.pressure_pump_running') }}.
- name: "Tank leak critical"
unique_id: tank_leak_critical
default_entity_id: binary_sensor.tank_leak_critical
device_class: problem
delay_on:
minutes: >
{{ states('input_number.tank_leak_critical_hold_minutes') | int(0) }}
availability: >
{{ has_value('sensor.rainwater_tank_volume_rate')
and has_value('input_number.tank_leak_critical_l_per_hour') }}
state: >
{% set rate = states('sensor.rainwater_tank_volume_rate') | float %}
{% set critical = states('input_number.tank_leak_critical_l_per_hour') | float %}
{{ is_state('binary_sensor.tank_leak_warning', 'on')
and (rate <= -critical
or is_state('binary_sensor.pressure_pump_running', 'on')) }}Warning and Critical notification actions
Replace the mobile notification action and media-player entity with your own. Put critical-monitoring-alert.mp3 in /media, test it manually, and leave the monitoring Toggle off until calibration is complete.
automation:
- id: tank_leak_warning_push
alias: "Tank leak - Warning push"
mode: single
triggers:
- trigger: state
entity_id: binary_sensor.tank_leak_warning
to: "on"
conditions:
- condition: state
entity_id: input_boolean.tank_leak_monitoring_enabled
state: "on"
actions:
- action: notify.mobile_app_your_phone
data:
title: "Tank leak warning"
message: >-
{{ state_attr('binary_sensor.tank_leak_warning', 'reason')
or 'The warning condition is active. Check Home Assistant for evidence.' }}
- id: tank_leak_critical_repeat
alias: "Tank leak - Critical repeat"
mode: restart
triggers:
- trigger: state
entity_id: binary_sensor.tank_leak_critical
to: "on"
conditions:
- condition: state
entity_id: input_boolean.tank_leak_monitoring_enabled
state: "on"
actions:
- action: notify.mobile_app_your_phone
data:
title: "Tank leak critical"
message: "The critical condition is active. Check the equipment and evidence now."
- action: media_player.play_media
target:
entity_id: media_player.alert_speaker
data:
media_content_id: "media-source://media_source/local/critical-monitoring-alert.mp3"
media_content_type: "audio/mpeg"
- repeat:
while:
- condition: state
entity_id: binary_sensor.tank_leak_critical
state: "on"
- condition: template
value_template: >-
{{ states('input_number.tank_leak_critical_repeat_minutes') | int(0) > 0 }}
sequence:
- delay:
minutes: >-
{{ states('input_number.tank_leak_critical_repeat_minutes') | int(0) }}
- condition: state
entity_id: binary_sensor.tank_leak_critical
state: "on"
- action: notify.mobile_app_your_phone
data:
title: "Tank leak still critical"
message: "The critical condition remains active."
- action: media_player.play_media
target:
entity_id: media_player.alert_speaker
data:
media_content_id: "media-source://media_source/local/critical-monitoring-alert.mp3"
media_content_type: "audio/mpeg"
Why the logic works
Tank level alone cannot distinguish use from loss. Rate removes the scale problem, the expected-use Toggle supplies context, pump state separates downstream demand from passive loss, and delay filters out short disturbances. Keep raw volume, rate and pump state together on the same History graph when tuning it.
The design is intentionally diagnostic rather than controlling. It reports an impossible or abnormal relationship; it does not bypass equipment protection or decide that a single suspected cause is proven.
Severity tiers without alert fatigue
| Tier | Meaning | Action |
|---|---|---|
| Advisory | The tank is falling faster than its measured idle behaviour. | Dashboard only |
| Warning | The abnormal fall persists while known water use is off. | One push notification |
| Critical | The unexplained fall is severe or is accompanied by unexpected pump operation. | Push, repeat and audible alert |
A weak clue remains visible without interrupting you. A Warning requires persistence or corroboration. Critical is reserved for direct danger or several independent signals. That separation is what stops a useful monitoring system becoming notification wallpaper.
Troubleshooting
- The derivative spikes when the sensor updates
- Increase the Derivative smoothing window from evidence, and check whether the source jumps in coarse litre steps.
- Rain or a refill hides the loss
- Treat refill periods separately and restart interpretation only after the level has settled.
- Normal household use causes warnings
- Drive the expected-use entity from real schedules or flow zones, not occupancy alone.
- The pump state chatters
- Derive running state from measured power with suitable hysteresis or delay rather than a threshold sitting on standby noise.
- The level never changes
- Check sensor freshness and geometry conversion before assuming zero consumption.
Safety and limits
Do not close valves, stop a bore pump or isolate water automatically from this inference. A false shut-off can damage equipment, interrupt stock water or conceal a fire-service supply issue. Inspect first; use a licensed plumber or pump technician where required.
If a webhook is added later, treat its webhook_id like a password, keep local_only: true unless a reviewed design requires otherwise, and never use an unauthenticated webhook for locks, garage doors, power isolation or anything destructive.
Official sources
These are the primary documentation pages used for the design. Check them against the Home Assistant or ESPHome version you are actually running.
- Home Assistant Derivative
- Home Assistant Statistics
- Home Assistant Template
- Home Assistant Input number
Related Smart Home Fix guides
- Fix unreliable entities before trusting alarms
- Separate smart-home devices without breaking discovery
- Reduce false alarms without hiding real faults
FAQ
Can Home Assistant detect a leak without spot leak sensors?
It can detect unexplained water loss when tank volume, loss rate, pump activity and known-use context are available. It cannot identify the exact leak location, and very small losses may remain below the level sensor's resolution.
Why use litres per hour instead of tank percentage?
Litres per hour is directly related to water loss when the source sensor already converts the tank's geometry correctly. A percentage change can represent very different volumes on different tanks or at different heights.
Does pump running prove there is a leak?
No. It proves demand or pressure recovery is occurring. The leak interpretation becomes stronger only when pump operation is unexpected and agrees with a sustained tank-volume fall.
Should Home Assistant shut the pump or a valve automatically?
Not from this inference alone. A false action can interrupt essential water or damage equipment. Use the system to alert and diagnose, then add control only after a site-specific safety review.
How long should the leak condition persist?
Use the shortest duration that survives normal sensor slosh, filtering, pump starts and legitimate draws on your own system. The correct value must come from timestamped history, not a copied example.