DIY Smart Water Leak Sensor with ESP32 and Home Assistant: Build Guide

This guide is assembled from official documentation, vendor datasheets, and community build reports — we did not build this unit ourselves. It shows how to wire an ESP32 development board to a Seeed Grove water sensor and run ESPHome so Home Assistant raises a leak alert, with no cloud subscription. Two variants are covered: a mains/USB always-on build (recommended) and a battery build with deep sleep. If you need flashing basics first, read the ESPHome setup guide.

Mains or Battery? Decide Before You Buy

A mains- or USB-powered node is the recommended design. Espressif’s ESP32 datasheet gives 10 µA for chip deep sleep, but a development board adds regulator and USB-serial overhead that no source used for this guide quantifies. Battery operation is therefore a costed option, not the default.

Approach Power Wet-event latency Runtime reality Complexity Choose it when
Variant A (this build) USB 5 V / mains Immediate; Wi-Fi stays connected No battery to manage Lowest An outlet is nearby and you want instant alerts and easy OTA
Variant B (this build) 150 mAh LiPo + deep sleep Seconds: wake, boot, join Wi-Fi Depends on unknown board overhead (see the runtime section); not a multi-year node Adds deep_sleep, divider, battery care No outlet nearby and you accept a check-in cadence
Commercial Zigbee/Thread puck Coin cell Immediate over mesh Vendor-claimed multi-year coin cell None: pair and mount Fit-and-forget use, or no Wi-Fi reach

For unattended multi-year battery use, a commercial puck is the better answer. See our water leak detection guide for comparisons. This guide recommends Variant A.

Parts

The parts list needs an esp32dev-class board, a Seeed Grove water sensor, and jumper leads. Two prices were verified for this guide, both from Adafruit: the HUZZAH32 at $19.95 and the 150 mAh LiPo at $5.95. Any esp32dev-class board that breaks out GPIO25 and GPIO34 can substitute; its price was not verified. Passives are generic.

Part Exact model / Product ID Qty Price (verified 2026-11-07) Source Notes
ESP32 dev board Adafruit HUZZAH32 – ESP32 Feather Board, PID 3405 1 $19.95 ($17.96 at 10–99; $15.96 at 100+) adafruit.com/product/3405 WROOM32-based, 4 MB flash, USB-to-serial, automatic bootloader reset, LiPo charger. Battery and USB cable not included
Water sensor Seeed Grove Water Sensor (V1.1, July 2014) 1 Not verified this run Seeed wiki Conductivity pad with 1 MΩ pull-up. Spec 4.75–5.25 V; this guide runs it at 3.3 V, a below-spec deviation
LiPo battery (Variant B only) Adafruit 3.7 V 150 mAh, PID 1317, JST 2-PH 1 $5.95 adafruit.com/product/1317 Matches the board’s battery connector
USB data cable Not included with board 1 Not listed Assorted, from any electronics supplier Needed for flashing and Variant A power
100 kΩ resistors (Variant B only) Two required 2 Not listed Assorted, from any electronics supplier Battery divider
Status LED (optional) LED with ~330 Ω series resistor 1 Not listed Assorted, from any electronics supplier Only if the board lacks an LED on the status pin
Enclosure and leads Grove-to-jumper leads, enclosure 1 set Not listed Assorted, from any electronics supplier Keep electronics above the water path; never seal the sensing pad

Wiring and Pinout

The sensor signal goes to GPIO25, which is RTC-capable so a sleeping chip can wake on water. The battery divider goes to GPIO34, an ADC1 pin. Both variants power the Grove sensor from 3V3, because its pull-up ties SIG to the supply rail; powering it at 5 V would put about 5 V on a 3.3 V GPIO.

ESP32 pin Connects to RTC-capable (deep-sleep wake)? Note
GPIO25 Grove SIG Yes; usable as wakeup_pin SIG idles HIGH via the 1 MΩ pull-up; water pulls it LOW. Use inverted: true for both the binary sensor and wakeup
GPIO34 Battery divider midpoint (Variant B) No; chosen for ADC, not wake ADC1 pin (GPIO32–39). About 2.1 V at a full 4.2 V pack
GPIO2 status_led Yes Many esp32dev-class boards have an LED here; check your board’s documentation
3V3 Grove VCC — Seeed lists 4.75–5.25 V; 3.3 V is a flagged deviation
GND Grove GND and divider low leg — Common ground is required
USB 5 V or LiPo JST Power — Variant A: USB. Variant B: battery on the board’s connector

Why GPIO25 for the Sensor and GPIO34 for the Battery

ESPHome’s deep_sleep docs list GPIO25 among the RTC pins usable for ESP32 wake, and its wakeup_pin needs a digital edge, so the sensor is read digitally. The ADC docs place ADC1 at GPIO32–39, so the battery divider uses GPIO34. As community practice, builders keep analog reads off ADC2 when Wi-Fi is active; this guide avoids that question entirely.

Firmware: The ESPHome YAML

The board is configured with board: esp32dev as described in the ESP32 platform docs. Variant A below compiles standalone. Replace the !secret entries with values from your secrets.yaml.

esphome:
  name: water-leak-sensor
  friendly_name: "Water Leak Sensor — Under Sink"

esp32:
  board: esp32dev
  framework:
    type: esp-idf  # Arduino is equally supported

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

api:

ota:
  - platform: esphome

logger:

binary_sensor:
  - platform: gpio
    pin:
      number: GPIO25
      inverted: true
      mode:
        input: true
        pullup: true
    name: "Water Leak"
    device_class: moisture
    filters:
      - delayed_on: 10ms
      - delayed_off: 10ms

status_led:
  pin: GPIO2

The power_save_mode: none setting follows the Wi-Fi docs, which warn that power-saving modes “generally also decrease the reliability of the WiFi connection, with frequent disconnections from the router in the highest power saving mode.” On mains power, reliability matters more than saving energy. The binary sensor uses GPIO binary sensor interrupts by default; the 10 ms filters debounce brief splashes. If your board’s LED is elsewhere, change status_led per its status_led docs.

Variant B: Battery and Deep Sleep

Start from Variant A, then make these edits:

  1. Add a deep-sleep block (per the deep_sleep docs):
deep_sleep:
  run_duration: 30s
  sleep_duration: 30min
  wakeup_pin:
    number: GPIO25
    inverted: true

The docs’ example uses 10s and 10min; 30s and 30min is a tunable design choice. The pin wakes the chip early on water, and the timer produces periodic check-ins.

  1. Under api:, add reboot_timeout: 0s, a common pattern for sleeping nodes.

  2. Add a battery sensor:

sensor:
  - platform: adc
    pin: GPIO34
    attenuation: auto
    name: "Battery Voltage"
    filters:
      - multiply: 2.0
    update_interval: 60s

multiply: 2.0 undoes the divider. ESPHome recommends calibrated readings, so treat values as approximate. Keep the Grove sensor on the always-on 3V3 rail; power-gating it would break the wake logic.

Optionally, add an uptime sensor to confirm check-ins.

The Deep-Sleep Fine Print

While asleep, the node “will not do any work and not respond to any network traffic, even Over The Air updates,” per the deep_sleep docs. Updates require catching the node awake or powering it over USB. In Home Assistant, entities may show Unavailable between wakes; the docs say this usually means the component was added after pairing, so remove and re-add the device.

Battery Runtime: The Honest Arithmetic

Use this formula:

life_hours ≈ (capacity_mAh × 1000) ÷ (10 + board_overhead_µA + 21)

The 10 µA is Espressif’s chip figure. The 21 µA is the divider across 200 kΩ at 4.2 V (4.2 V ÷ 200 kΩ). The board_overhead term is the unknown: regulator and USB-serial quiescent draw, which no source used here specifies. Active windows add draw not included below.

Assumed board overhead (the unknown) Total sleep draw (10 µA chip + overhead + ~21 µA divider) 150 mAh lasts (illustrative)
0 µA (datasheet floor; unreachable on a dev board) 31 µA ≈4,800 h ≈ 200 days
100 µA 131 µA ≈1,150 h ≈ 48 days
500 µA 531 µA ≈280 h ≈ 12 days
1,000 µA 1,031 µA ≈145 h ≈ 6 days

Illustrative arithmetic only; nothing in this table was measured. On any plausible board overhead, 150 mAh is a days-to-weeks check-in sensor, not a multi-year node. This arithmetic is why Variant A is recommended.

Home Assistant: Sensor, Alert, Automation

Once the node joins, Home Assistant exposes the binary sensor as binary_sensor.water_leak. Its moisture device class reports Wet (on) and Dry (off), with the binary sensor docs listing the Moisture detected and Moisture cleared triggers.

What Shows Up in HA

The entity reports Wet or Dry. Its moisture.detected and moisture.cleared triggers let you build automations without parsing state strings.

The Alert Integration

Add this to configuration.yaml, following the alert docs. Replace mobile_app_your_phone with your own notify service.

alert:
  water_leak_alert:
    name: "Water leak detected"
    entity_id: binary_sensor.water_leak
    state: "on"
    repeat: [5, 10, 30, 60]
    can_acknowledge: true
    title: "Water leak"
    message: "The under-sink water sensor reports wet."
    done_message: "The under-sink water sensor is dry again."
    notifiers:
      - mobile_app_your_phone

The required keys are name, entity_id and repeat. state: "on" fires while the sensor is wet, repeat sets the reminder schedule in minutes, can_acknowledge lets you silence it, and notifiers names the delivery services.

The Wet-Notification Automation

This automation trigger sends an immediate notification once when the sensor turns wet.

automation:
  - alias: "Water leak immediate ping"
    trigger:
      - platform: state
        entity_id: binary_sensor.water_leak
        to: "on"
    action:
      - service: notify.mobile_app_your_phone
        data:
          title: "Water leak"
          message: "Under-sink sensor reports wet."

The alert nags on its repeat schedule until acknowledged. The automation pings immediately, once. Use both: the automation gives the first notice, and the alert keeps the leak visible until someone acts.

Testing and Verification

These six checks are for you to run on your own build:

  1. The ESPHome logs show the binary sensor change state when you bridge the traces with a wet fingertip.
  2. The Home Assistant entity toggles between Dry and Wet.
  3. The automation fires once on wet.
  4. The alert entity turns on, repeats per its schedule, moves to off when acknowledged, and sends the done_message when dry.
  5. For Variant B, the logs show the run window, sleep, and early wake on water.
  6. The battery reading roughly matches pack voltage per the divider math, and you can compare it to a multimeter.

Making It Reliable: False Triggers and Corrosion

The Grove sensor classifies dry, damp, or immersed by conductivity, so condensation can read as damp. Seeed’s spec lists 10–90 %RH and 10–30 °C, so avoid hot water heaters and freezing spaces. Mitigations: keep the 10 ms filters (raise delayed_on in splashy spots, a design suggestion), keep the internal pull-up that stiffens the weak 1 MΩ high state, and mount the pad where pooled water reaches it first but dries quickly.

Exposed traces carrying a small DC current while wet are a community-reported corrosion route. Treat the pad as a consumable: inspect it, replace it when traces discolour, and dry it promptly after an event.

FAQ

Why does my sensor show Unavailable in Home Assistant?

In deep sleep, the node is offline between wakes, so its entities show Unavailable. The deep_sleep docs say this usually happens when the component was added after the device was paired, and the fix is to remove and re-add the device. Variant A, always-on, avoids this entirely because the node stays connected.

Can I OTA-update a sleeping node?

No. The deep_sleep docs state that a sleeping node will not respond to network traffic, including over-the-air updates. Catch the node during its awake window, or connect it over USB. For Variant B, plan updates around the run window.

Why GPIO25 and GPIO34?

GPIO25 is on ESPHome’s RTC wake-pin list, so a sleeping chip can wake when the sensor pulls the pin low. GPIO34 is in the ADC1 range (GPIO32–39), which is suitable for the battery divider. Each pin was chosen for one job, not for both.

Will condensation trigger the sensor?

It can. The Grove sensor reports damp as a distinct state, so condensation may read as a leak. Limit this with placement, the 10 ms debounce filters, and a mounting spot that stays within the 10–90 %RH and 10–30 °C spec. Raising delayed_on helps in splashy spots.

How long will the battery last?

This guide does not give a number. The runtime section provides a formula with the dev-board overhead as the unknown, and the table is illustrative only. Measure your own board’s sleep draw with a meter before relying on any estimate.

Why use an alert and an automation?

They do different jobs. The automation sends one immediate notification when the sensor turns wet. The alert repeats on its schedule until someone acknowledges it, and it sends a done message when the sensor dries. Using both gives you a fast first notice and a persistent reminder.

The Bottom Line

Build Variant A if you already run ESPHome and have an outlet nearby. It is the simplest, most reliable configuration, and it reports wet events immediately. Choose Variant B only if you accept check-ins and a costed battery, and buy a commercial puck if you need unattended battery operation for years. Related builds include the energy monitor project and the ESP32 Bluetooth proxy guide.

How This Guide Was Built

This guide draws on Espressif’s datasheet, ESPHome and Home Assistant documentation, Seeed’s Grove wiki, and Adafruit product pages. Prices were verified on 2026-11-07 for the HUZZAH32 and the 150 mAh LiPo only. Not verified: the development board’s deep-sleep current, battery runtimes, the Grove sensor’s price, and any hands-on build. This guide is assembled from official documentation, vendor datasheets, and community build reports — we did not build this unit ourselves.

← Back to guides