Smart Home Fix

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.

Tank leak evidence flow Multiple measurements are combined into advisory, warning and critical fault states. Tank volume ratePump runningKnown water useCondition duration Evidence model direction + context agreement + duration Advisory dashboard only Warning one push Critical repeat + audible
A falling level becomes actionable only after rate, context and pump behaviour are considered together.

The evidence model

A useful alarm does not promote one noisy reading straight to an emergency. It combines measurements that fail in different ways:

  1. Volume rate. A Derivative helper converts tank volume into litres per hour, making a slow but persistent draw visible.
  2. Expected-use context. Irrigation, a scheduled transfer or deliberate household use suppresses the leak interpretation without hiding the raw rate.
  3. Pump evidence. Unexpected pressure-pump operation suggests demand downstream; loss with the pump off points towards the tank or gravity-fed side.
  4. 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.

MeasurementWhy it mattersRequired value
sensor.rainwater_tank_volumeNormal settled volume and sensor resolutionRead the normal settled litre value and how often it reports, both straight off this sensor's History graph
sensor.rainwater_tank_volume_rateNormal idle loss and deliberate draw ratesCompare a quiet-day L/h range against a known-draw L/h range from the same History graph
binary_sensor.pressure_pump_runningPump start, stop and longest normal runNote the start/stop timestamps and the longest normal run length you see in this entity's History
input_boolean.water_use_expectedKnown irrigation, transfer and heavy-use periodsBase this on your own irrigation schedule or manual rule for when a legitimate draw is happening
weather event contextRainfall or refill events that make volume riseCapture 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

  1. Record a normal day with all known use annotated, then calculate idle tank loss in L/h.
  2. Run one deliberate, measured draw and capture tank volume, volume derivative and pump state at matching timestamps.
  3. Stop the draw and measure how long sensor slosh or filtering takes to settle.
  4. 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 entityPurposeSetting
sensor.rainwater_tank_volume_rateDerivative helper; source is tank volume; output in L/hWiden 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_hourUnexplained loss rate that deserves reviewSet 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_hourLoss rate that warrants immediate inspectionSet this well above the warning threshold, at a loss rate that would need urgent attention on your own property
input_number.tank_leak_hold_minutesPersistence before WarningSet this long enough to outlast the sensor slosh and pump starts you saw in your own History
input_number.tank_leak_critical_hold_minutesPersistence before CriticalSet this to a shorter, more urgent persistence window than the Warning hold, based on the same History evidence
input_boolean.water_use_expectedOn during a known legitimate drawLeave this off whenever the tank should be idle, and only switch it on for a genuine known draw
input_boolean.tank_leak_monitoring_enabledMaster notification gateLeave this off until you've validated every threshold above against your own data, then switch it on
input_number.tank_leak_critical_repeat_minutesCritical repeat spacingChoose 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

TierMeaningAction
AdvisoryThe tank is falling faster than its measured idle behaviour.Dashboard only
WarningThe abnormal fall persists while known water use is off.One push notification
CriticalThe 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.

Related Smart Home Fix guides

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.