House well pump health monitoring via power and pressure

This guide provides information on monitoring your home well based water system. The objective is to understand when water is being utilized and to monitor the health of the system components. The implementation looks at the system’s water pressure and the utilization of power. This gives the capability to accomplish the following:

  • Know when water is being used so you can detect unexpected utilization
  • Discover slow leaks in the pluming system
  • Know if the well pump power utilization pattern changes which could indicate a pump problem
  • Know if the bladder tank is working correctly

The first part of the guide looks at monitoring of the well power usage while the second part looks at monitoring the system water pressure.

Power monitoring
A while ago I installed an Emporia VUE3 to provide power monitoring at the circuit panel. All major appliances are on their own dedicated circuits. A power sensor is created for each circuit, providing an easy method to monitor the washer, dryer and dishwasher running state. The well pump also has a dedicated circuit and thus its electric utilization be monitored. Here’s a picture of the Emporia ESPhome device installed:

Assuming you don’t what to monitor at the circuit breaker, there are multiple discussion of utilizing CT clamps with and esp and ESPhome to monitor power. You could put the CT clamp on the power line right at the well pump pressure switch.

If you have a well pump you should have a bladder tank similar to the one shown in this post. As the pump push more water than the pipes in the house can distribute, the tank provides a water buffer which reduces stress on the well pump. The bladder tank ensure the water system is pressurized and ready to deliver water. The bladder tank limits how often and how long the pump needs to run. However, the bladder tank will fail at some point resulting in increased stress on the well pump. If you’re not looking for the failed bladder tank you might not know it’s failed until the pump also fails. Monitoring the power allows you to watch the health of the water system. Here is a little less than one hour graph of my pump’s power usage.

You can see the power utilization is very spiky in nature. The pump runs for about 25 seconds at a time. Once the pump stops, If the water is fulling running, we get just under 2 minutes before the pump kicks on again. The amount of off time between spikes can be used to determine if the bladder has a problem. Once the bladder fails the pump will turn off for only a second or two before turning back on. By monitoring the power usage pattern, you can tell when the bladder fails.

Monitoring the power can also provide an indication that the pump is failing. If the power draw goes above the normal 1000+ watts or doesn’t make it above 800 watts the pump is most likely failing. Likewise, the pump running for a continuous duration beyond the normal 25 second burst also indicate something you need to investigate. The power draw, cycle duration and enter cycle gaps will be different for your specific implementation.

To track the pumps power usage, I create a number of sensor and add a few automations. The sensors are

  • Well Pump Running
  • Well Pump Last Cycle Power Draw
  • Well Pump Peak Power This Cycle
  • Well Pump Last Off Duration
  • Well Pump Last Run Duration

The automations create actionable notifications based on the above sensors. I created the file well_pump_monitoring.yaml under the homeassistant/packages directory on my controller. I also had to created the packages directory. The contents of the file follow:

# =============================================================================
# WELL PUMP MONITORING
# =============================================================================
# Power sensor: sensor.emporiavue3_well_pump_12_power (Emporia Vue 3, CT on well pump leg)
# Notifications go to notify.persistent_notification (HA UI only) - swap in
# a mobile_app notify service too if you want a push alert on your phone.
# Tune all thresholds marked with a comment to your actual pump's behavior.
# =============================================================================

# -----------------------------------------------------------------------------
# TEMPLATE SENSORS
# -----------------------------------------------------------------------------

template:
  # Debounced running state, used by the runtime + max-power automations below.
  - binary_sensor:
      - name: "Well Pump Running"
        unique_id: well_pump_running
        device_class: running
        state: "{{ states('sensor.emporiavue3_well_pump_12_power') | float(0) > 100 }}"
        delay_on:
          seconds: 1   # must stay above threshold 3s to count as "started" (debounce)
        delay_off:
          seconds: 2   # must stay below threshold 5s to count as "stopped"

# -----------------------------------------------------------------------------
# YOUR EXISTING TRIGGER-BASED SENSORS (kept as-is, feeding the automations below)
# -----------------------------------------------------------------------------

  # NOTE: these two duration sensors trigger off binary_sensor.well_pump_running,
  # NOT the raw power sensor. The raw sensor's value jitters slightly even while
  # idle/running (CT noise), which constantly resets its own last_changed - so
  # duration math against it reads "time since the last noise blip," not the
  # real on/off duration. The debounced binary_sensor only changes at genuine
  # transitions, so its last_changed is trustworthy.
  - trigger:
      - platform: state
        entity_id: binary_sensor.well_pump_running
        to: "on"
    sensor:
      - name: "Well Pump Last Off Duration"
        unique_id: well_pump_last_off_duration
        icon: mdi:timer-sand
        unit_of_measurement: "s"
        availability: "{{ trigger.from_state is not none }}"
        state: >
          {{ (now() - trigger.from_state.last_changed).total_seconds() | round(0) }}

  # Tracks the running MAX power seen during the current cycle, updating on
  # every raw sensor reading while the pump is on. Resets to the current
  # reading each time a new cycle starts. This exists because the raw
  # power sensor is instantaneous, not accumulated - grabbing a single
  # sample at shutoff (the old approach) has a real chance of landing on
  # a low tail-off reading and missing the actual peak entirely.
  - trigger:
      - platform: state
        entity_id: binary_sensor.well_pump_running
        to: "on"
        id: "cycle_start"
      - platform: state
        entity_id: sensor.emporiavue3_well_pump_12_power
        id: "power_update"
    sensor:
      - name: "Well Pump Peak Power This Cycle"
        unique_id: well_pump_peak_power_this_cycle
        icon: mdi:flash-alert
        unit_of_measurement: "W"
        state: >
          {% if trigger.id == "cycle_start" %}
            {{ states('sensor.emporiavue3_well_pump_12_power') | float(0) }}
          {% elif is_state('binary_sensor.well_pump_running', 'on') %}
            {{ [states('sensor.well_pump_peak_power_this_cycle') | float(0),
                states('sensor.emporiavue3_well_pump_12_power') | float(0)] | max }}
          {% else %}
            {{ states('sensor.well_pump_peak_power_this_cycle') | float(0) }}
          {% endif %}

  - trigger:
      - platform: state
        entity_id: binary_sensor.well_pump_running
        to: "off"
    sensor:
      - name: "Well Pump Last Run Duration"
        unique_id: well_pump_last_run_duration
        icon: mdi:timer-outline
        unit_of_measurement: "s"
        availability: "{{ trigger.from_state is not none }}"
        state: >
          {{ (now() - trigger.from_state.last_changed).total_seconds() | round(0) }}
      # Snapshots the running peak tracked above, right as the cycle ends -
      # by this point the delay_off debounce has already elapsed, so the
      # peak sensor has stopped updating and holds the true max for the cycle.
      - name: "Well Pump Last Cycle Power Draw"
        unique_id: well_pump_last_cycle_power_draw
        icon: mdi:flash
        unit_of_measurement: "W"
        availability: "{{ trigger.from_state is not none }}"
        state: >
          {{ states('sensor.well_pump_peak_power_this_cycle') | float(0) | round(0) }}

# -----------------------------------------------------------------------------
# AUTOMATIONS
# -----------------------------------------------------------------------------

automation:

  # ---------------------------------------------------------------------
  # 1. REAL-TIME (HARD ALARM): pump still running past a safe duration,
  # right now. This is the live catch for a stuck valve / dry well - it
  # fires while the pump is still on, unlike the end-of-cycle check below
  # which can only ever report on a run that has already finished.
  # Deliberately set higher than the "soft" long-cycle flag below it, so
  # a cycle that runs a bit long but stops on its own only gets the soft
  # notice, while one that's still going gets escalated here.
  # ---------------------------------------------------------------------
  - alias: "Well Pump - Still Running (Live Alert)"
    id: well_pump_still_running_live
    trigger:
      - platform: state
        entity_id: binary_sensor.well_pump_running
        to: "on"
        for:
          minutes: 2   # tune: should be comfortably above your longest normal cycle
    action:
      - service: notify.persistent_notification
        data:
          title: "🚨 Well Pump Alert: Still Running"
          message: >
            The well pump has been running continuously for over 2 minutes
            and has not yet stopped. Check for a stuck check valve, an open
            line, or a dry well right now.

  # ---------------------------------------------------------------------
  # 2. END-OF-CYCLE: short-cycling, total runtime, and power deviation.
  # Evaluated once per completed cycle, off your three trigger-based sensors.
  # Uses independent if/then blocks (not choose) so multiple simultaneous
  # issues on the same cycle all get reported, instead of only the first
  # match suppressing the rest.
  # ---------------------------------------------------------------------
  - alias: "Safety: Well Pump Diagnostics & Deviation Guard"
    id: well_pump_diagnostics_deviation_guard
    description: >-
      Evaluates well pump health at the end of every cycle for short-cycling,
      over-run, or power anomalies.
    triggers:
      - entity_id: sensor.well_pump_last_run_duration
        trigger: state
    conditions: []
    actions:
      - if:
          - condition: numeric_state
            entity_id: sensor.well_pump_last_off_duration
            below: 60   # tune: minimum seconds you'd expect between normal cycles
          - condition: template
            value_template: "{{ states('sensor.well_pump_last_off_duration') | float > 0 }}"
        then:
          - action: notify.persistent_notification
            data:
              title: "🚨 Well Pump Alert: Short Cycling"
              message: >-
                The well pump turned back on after only {{
                states('sensor.well_pump_last_off_duration') }} seconds. Your
                pressure bladder tank may be waterlogged.

      - if:
          # SOFT flag: the cycle finished on its own, but took longer than
          # typical. Lower threshold than the live "still running" alert -
          # this catches sluggish cycles that never trip the hard alarm.
          - condition: numeric_state
            entity_id: sensor.well_pump_last_run_duration
            above: 60   # 1 min - tune once you know your normal cycle length
        then:
          - action: notify.persistent_notification
            data:
              title: "⚠️ Well Pump Notice: Long Cycle"
              message: >-
                The well pump ran for an unusual duration of {{
                (states('sensor.well_pump_last_run_duration') | float / 60) |
                round(1) }} minutes. Worth a look if this keeps happening.

      - if:
          - condition: or
            conditions:
              - condition: numeric_state
                entity_id: sensor.well_pump_last_cycle_power_draw
                above: 1200
              - condition: numeric_state
                entity_id: sensor.well_pump_last_cycle_power_draw
                below: 500
        then:
          - action: notify.persistent_notification
            data:
              title: "🚨 Well Pump Alert: Power Deviation"
              message: >-
                The pump power peaked at {{
                states('sensor.well_pump_last_cycle_power_draw') }} Watts during
                its last cycle. This is outside safe baseline parameters.
    mode: single

The above file is include in my system by adding the following to /root/homeassistant/configuration.yaml file:

homeassistant:
  packages: !include_dir_named packages

My configuration file already had the “homeassistant:” tag, so I just added the packages line under it.

System water pressure monitoring
Monitoring the water pressure will let you know when water is running and when the water system has a slow leak. The link I provided above with the picture of the bladder tank includes this second link. This link provides instructions for building a ESPhome based water pressure monitor. Here are pictures of the ESP32 controller I wired together. The black wire coming is from the well pressure sensor. I utilized 3 10k Ohm resistors to reduce the water pressure sensor voltage from 0-5v to 0-3.3V

The link for building the ESP32 device has basic yaml for creating a sensor to monitor the water pressure. I added a couple of additional sensors. One sensor tells me if water is running and the second sensor reports if a slow leak is detected. The water running sensor reporting lags by about 15 seconds. In addition, if the water is turned off but the pump is currently running, it will not report the water is off until after the pump turns off. The slow leak detector takes about 15 minutes to look for a small decrease water pressure. If a decrease is detected, then you have a water leak. There is a second test in the yaml that looks for the pump kicking on three times without the water ever being turned on. I haven’t had this one go off as of yet. I added it to look for a really slow leak that I only expect would kick off if I wasn’t home for a day or two. Here is my water pressure monitoring yaml for the esp32dev board as it stands today.

esphome:
  name: well-pump-pressure
  friendly_name: Well pump pressure

# Replace this with your actual platform if you aren't using ESP32.
esp32:
  board: esp32dev
#  framework:
#    type: arduino

# Enable logging
logger:
  level: DEBUG

# Enable Home Assistant API
api:
  encryption:
    key: !secret api_key

ota:
  platform: esphome
  password: !secret ota_password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Optional manual IP
  manual_ip:
    static_ip: 192.168.10.123
    gateway: 192.168.10.2
    subnet: 255.255.255.0
    dns1: 192.168.10.2

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Well-Pump"
    password: !secret wifi_password

  
web_server:
  port: 80

# =============================================================================
# WATER-RUNNING / SLOW-LEAK INFERENCE
# =============================================================================
# All derived purely from the pressure trend - this ESP32 has no visibility
# into the well pump's power draw, so "running" is inferred from rate of
# change: a fast decline (tap open, pump not yet engaged), a fast rise (pump
# actively pressurizing), or the slow settling tail that follows a pump
# cycle, are all treated as "running." Only once the rate has been genuinely
# flat for a while does it clear back to "not running."
#
# Empirical basis (from your 14:18-14:32 log):
#   - Tap open / pump not yet engaged: pressure falls ~0.08-0.09 PSI/s
#   - Pump actively pressurizing:       pressure rises up to  ~0.84 PSI/s
#   - Post-pump settling tail:          pressure falls   ~0.01-0.02 PSI/s,
#                                        tapering to ~0 as it flattens out
#
# Rate of change is smoothed over a rolling 15-second window (4 samples at
# your ~5s publish interval) rather than a longer window, to keep detection
# latency low - slow leak detection tolerates lag fine (per your call), but
# on/off status shouldn't.
# =============================================================================

globals:
  # Rolling 15-second buffer (4 samples @ ~5s publish interval) used to
  # compute a smoothed rate of change - this averages out ADC/calibration
  # quantization noise that a single 5s delta would be too jumpy to use.
  # Shorter window = faster detection of on/off transitions, at the cost of
  # slightly less smoothing (compensated by the off-threshold below).
  - id: pressure_history
    type: float[4]
    restore_value: no
    initial_value: '{0,0,0,0}'
  - id: pressure_history_filled
    type: int
    restore_value: no
    initial_value: '0'

  # Slow-leak tracking (drift test)
  - id: leak_baseline_pressure
    type: float
    restore_value: no
    initial_value: '-1'   # -1 = not yet established
  - id: leak_had_activity
    type: bool
    restore_value: no
    initial_value: 'false'
  - id: leak_strike_count
    type: int
    restore_value: no
    initial_value: '0'

  # Slow-leak tracking (recurring-refill test)
  # A pump refill (rapid pressure RISE) that happens with no genuine tap-draw
  # (rapid pressure FALL) seen since the previous refill is unexplained - the
  # pump only kicks in because pressure dropped low enough on its own, which
  # a leak does just as well as a big legitimate draw. Several such
  # unexplained refills in a row, even spaced many minutes apart, is a second
  # independent leak signal - distinct from the drift test above, since it
  # catches a leak via the pump's own cut-in/cut-out cycling pattern rather
  # than by measuring the bleed rate directly.
  - id: leak_tap_draw_seen
    type: bool
    restore_value: no
    initial_value: 'false'
  - id: leak_refill_streak
    type: int
    restore_value: no
    initial_value: '0'

interval:
  # Every 5 minutes: check whether pressure has quietly bled down since the
  # last checkpoint. Windows containing pump/tap activity are skipped rather
  # than counted against the leak (a refill triggered by a leak is evidence
  # FOR the leak, not against it) - only a clean window with no meaningful
  # drop clears progress. Requires 3 qualifying windows (not necessarily
  # back-to-back in wall-clock time, since activity windows are skipped)
  # before alarming, to avoid a one-off noisy window.
  - interval: 5min
    then:
      - lambda: |-
          float current = id(water_pressure_sensor).state;

          if (id(leak_baseline_pressure) < 0) {
            // first run - just establish a baseline, nothing to compare yet
            id(leak_baseline_pressure) = current;
            id(leak_had_activity) = false;
            return;
          }

          if (!id(leak_had_activity)) {
            float drop = id(leak_baseline_pressure) - current;
            // tune: PSI of unexplained drop over 5 min that counts as suspicious.
            if (drop > 0.3f) {
              id(leak_strike_count) += 1;
            } else {
              // A clean window with no meaningful drop is real evidence
              // AGAINST a leak - pressure held steady on its own. This is
              // the only case that should clear accumulated strikes.
              id(leak_strike_count) = 0;
            }
          }
          // else: this window contained a pump refill (or real tap use) -
          // there's no clean drop to measure, but a refill isn't evidence
          // AGAINST a leak either - in fact a leak-driven refill is what
          // typically causes this. Skip the window without penalizing
          // prior progress, so a leak that periodically triggers its own
          // pump cycle doesn't keep resetting itself right before crossing
          // the strike threshold (this is exactly what happened in your log).

          id(slow_water_leak).publish_state(id(leak_strike_count) >= 3);

          id(leak_baseline_pressure) = current;
          id(leak_had_activity) = false;

sensor:
  - platform: adc
    id: water_pressure_sensor
    pin: GPIO36
    name: "Water Pressure Sensor"
    attenuation: 12db
    update_interval: 0.5s
    unit_of_measurement: PSI
    filters:
      - median:
          window_size: 10
          send_every: 10
          send_first_at: 1
      - lambda: 'ESP_LOGD("adc", "Raw ADC Value: %f", x); return x;'
      - calibrate_linear:
        - 0.34 -> 0
        - 0.61 -> 10
        - 0.90 -> 20
        - 1.14 -> 30
        - 1.43 -> 40
        - 1.69 -> 50
        - 1.95 -> 60
        - 2.22 -> 70
        - 2.42 -> 80
        - 2.67 -> 90
        - 2.90 -> 100
    on_value:
      then:
        - lambda: |-
            // shift the 15s rolling buffer and insert the latest reading
            for (int i = 0; i < 3; i++) id(pressure_history)[i] = id(pressure_history)[i + 1];
            id(pressure_history)[3] = x;
            if (id(pressure_history_filled) < 4) id(pressure_history_filled) += 1;

            if (id(pressure_history_filled) >= 4) {
              float rate = (id(pressure_history)[3] - id(pressure_history)[0]) / 15.0f;
              id(pressure_rate_of_change).publish_state(rate);

              // Hysteresis: different threshold to START "running" vs. to
              // STOP it, so the settling tail after a pump cycle correctly
              // keeps "running" true until the rate has genuinely gone flat,
              // rather than clearing the moment the rate first dips low.
              bool currently_running = id(water_running).state;
              bool now_running;
              if (currently_running) {
                now_running = (fabs(rate) > 0.012f);   // tune: "truly flat" cutoff
              } else {
                now_running = (fabs(rate) > 0.03f);    // tune: "onset" cutoff
              }
              id(water_running).publish_state(now_running);

              if (now_running) {
                id(leak_had_activity) = true;
              }

              // Recurring-refill leak test: only evaluated right at the
              // moment "running" starts (not currently_running -> now_running),
              // using the sign of rate at that instant to tell a pump refill
              // (rising) apart from a genuine tap draw (falling).
              if (!currently_running && now_running) {
                if (rate > 0.0f) {
                  // Pump refill. Unexplained unless a real tap draw was
                  // seen since the previous refill.
                  if (!id(leak_tap_draw_seen)) {
                    id(leak_refill_streak) += 1;
                  } else {
                    id(leak_refill_streak) = 0;   // this one was explained
                  }
                  id(leak_tap_draw_seen) = false;   // reset for the next interval
                  id(recurring_pump_cycling).publish_state(id(leak_refill_streak) >= 3);
                } else {
                  // Genuine tap draw - explains any refill that follows it.
                  id(leak_tap_draw_seen) = true;
                }
              }
            }

  - platform: template
    name: "Water Pressure Rate of Change"
    id: pressure_rate_of_change
    unit_of_measurement: "PSI/s"
    accuracy_decimals: 3
    icon: mdi:chart-line-variant
    update_interval: never   # purely driven by publish_state() above
    lambda: return {};

binary_sensor:
  - platform: template
    name: "Water Running (Inferred)"
    id: water_running
    icon: mdi:water
    # No lambda here on purpose: without one, ESPHome never auto-polls this
    # sensor. It only changes when publish_state() is called on it directly,
    # which happens inside the pressure sensor's on_value lambda above.

  - platform: template
    name: "Slow Water Leak Suspected"
    id: slow_water_leak
    device_class: problem
    icon: mdi:water-alert
    # Same as above - publish-only, driven by the 5-minute interval lambda.

  - platform: template
    name: "Recurring Pump Cycling (Possible Leak)"
    id: recurring_pump_cycling
    device_class: problem
    icon: mdi:water-sync
    # Publish-only, driven by the refill-vs-tap-draw logic in the pressure
    # sensor's on_value lambda above. Independent evidence from the drift
    # test: this one fires off the pump's own refill pattern rather than
    # measuring the bleed rate directly, so it still catches a leak even
    # if the drift test's windows keep getting interrupted.

Here’s a picture of the sensors that get created

I still need to add a couple of automations to run on home assistant. The first it to provide a notification if the water starts running when no one is home. The second automation will provide a notification if either of the slow leak detector goes off. The second slow leak detector is the Recurring Pump Cycling sensor.

2 Likes

Most would say either your pump is too large or your tank is too small (or partially failed already).

The design goal is a minimum run time of 60 seconds (with 2 minutes being better) I have two large pressure tanks on my system to get to 90+ seconds. My pump cycles about 5 or 6 times a 24 hour period when there are no leaks or unusual usage.

Starting the pump is the hardest part of its life. The motor and pump are rated to run continuously (unless you have a really inexpensive one).

My pump is 20+ years old so I want to make sure it stays healthy as long as possible.

1 Like

I wouldn’t be surprised if my tank is partially compromised. We have very acidic water and the tank have a tendency to fail between 5-10 years. I’ve been expecting the tank to fail, as the current tank has lasted longer than any previous tank. The status of the tank was a big driver for pulling this stuff together. With the monitoring I can now view the systems performance and look for changes in behavior, which is an indication of issues. The current tank running between 40-60 pounds of pressure gives about 5 gallons of water before the pump turns on. I figure with the knowledge I’ve gained I can now look at the tank and determine what amount of water it should really be providing.

One of the surprising things I’ve noticed with my system. If I turn the water on just until the pump kicks in, it runs the pressure close to 60 pounds. However, it will then settle back down to around 56 pounds. I don’t know if pressure settling is a normal thing for well base systems with a pressure tank. You know anything about this?

Also, if you have any thoughts for additional monitoring that could be done, please give me your suggestions.

1 Like

If it is a 20 gallon tank, that sounds about right, based on most charts I have seen. I have two 86 gallon tanks so get much more for each run. My pump is way oversized for normal usage. I wasn’t clear on how much flow I needed as a maximum and I didn’t ever want to deal with too low pressure.

When the pump turns on, the pressure will increase. If you mean as soon as the pump turns on the pressure hits 60, that is not normal. If you mean once the pump gets to 60 and then turns off it drops to 56, that is normal. It takes my pump 90s or so to get the tanks pressurized. If yours takes 25s, you might miss the rise.
This is what mine looks like:

1 Like

My graph is similar, with the pressure sage after reaching the max pressure, just as your graph shows. Thanks for posting the graph.