ESP32 Water Leak Detector

Smart HomeBeginnerIntermediateAdvanced

Protect your home from water damage with an ESP32 leak detector. Start with a simple moisture sensor and buzzer alert, scale to multi-zone monitoring with Telegram notifications, and finish with automatic main water shutoff via a solenoid valve.

Overview

In this beginner project you will connect a resistive water sensor strip to the ESP32 ADC. When the sensor strip is wet, resistance drops and the analog voltage rises above a threshold. A buzzer sounds and a red LED turns on. The Serial Monitor prints the raw ADC value every second so you can calibrate the wet and dry thresholds. The sensor is tested by dripping a few drops of water onto the copper tracks.

Components
  • 1× ESP32 DevKit V1
  • 1× Resistive water sensor strip — Parallel copper trace strip; inexpensive module
  • 1× Active buzzer 5 V
  • 1× Red LED and 220 ohm resistor — Leak indicator
  • 1× Green LED and 220 ohm resistor — Normal status indicator
  • 1× Breadboard and jumper wires
Wiring
Component PinESP32 PinNotes
Water sensor AOUTGPIO 34Analog signal; higher voltage = wetter
Water sensor DOUTGPIO 35Digital threshold from onboard comparator
Water sensor VCC3.3 VPower sensor only when reading to reduce electrolytic corrosion
Water sensor GNDGND
Buzzer +GPIO 25
Red LED anodeGPIO 26
Green LED anodeGPIO 27
Arduino Code
esp32-water-leak-detector_beginner.ino
// ESP32 Water Leak Detector - Beginner
// Resistive moisture sensor; buzzer + LED alert

const int SENSOR_AOUT = 34;
const int SENSOR_DOUT = 35;
const int BUZZER  = 25;
const int LED_RED = 26;
const int LED_GRN = 27;

const int WET_THRESHOLD = 2000; // tune from Serial output; 0-4095

void setup() {
  Serial.begin(115200);
  analogReadResolution(12);
  pinMode(SENSOR_DOUT, INPUT);
  pinMode(BUZZER,  OUTPUT);
  pinMode(LED_RED, OUTPUT);
  pinMode(LED_GRN, OUTPUT);
}

void loop() {
  int raw = analogRead(SENSOR_AOUT);
  bool wet = (raw > WET_THRESHOLD) || (digitalRead(SENSOR_DOUT) == HIGH);

  Serial.printf("Sensor: %d  Status: %sn", raw, wet ? "WET!" : "dry");

  if (wet) {
    digitalWrite(LED_RED, HIGH);
    digitalWrite(LED_GRN, LOW);
    digitalWrite(BUZZER,  HIGH);
  } else {
    digitalWrite(LED_RED, LOW);
    digitalWrite(LED_GRN, HIGH);
    digitalWrite(BUZZER,  LOW);
  }
  delay(1000);
}
How It Works
01

Resistive Moisture Detection: The sensor strip has two interleaved copper tracks. In dry air, resistance between tracks is very high (megaohms). Water bridging the tracks reduces resistance dramatically. The sensor module uses a voltage divider: lower track resistance means higher voltage at the AOUT pin.

02

Dual-Output Detection: Like the MQ-2 smoke sensor, the moisture module provides both an analog output (AOUT: proportional to wetness) and a digital output (DOUT: LOW/HIGH based on onboard potentiometer threshold). Both are checked for redundancy.

03

Corrosion Minimisation: Powering the sensor continuously causes electrolysis that corrodes the copper tracks within weeks. Best practice is to power the sensor only when reading: connect VCC to a GPIO output, set HIGH before analogRead(), read, then set LOW. For hourly checks, this reduces sensor energisation time by 99.9 percent.

04

Calibration from Serial Monitor: Run the sketch with the sensor dry and note the ADC value (typically 0-200). Then wet the sensor with a few drops and note the wet value (typically 2000-4095 depending on water conductivity). Set WET_THRESHOLD halfway between the two values.

Applications
  • Under-sink water leak detector for kitchen and bathroom
  • Washing machine overflow alert
  • Aquarium overflow detection sensor
  • Basement flooding early warning system
Troubleshooting

Sensor reads wet with no water present

Humidity can cause the sensor to read slightly elevated. Place the sensor indoors, away from direct steam or condensation. Increase WET_THRESHOLD slightly above the maximum dry reading observed over 24 hours in the installed location.

Copper tracks corroded within weeks

Implement powered-only-when-reading: add a GPIO-controlled power switch. Read once per minute, powering the sensor only for 100 ms each time. This reduces corrosion by several orders of magnitude.

ADC reads zero regardless of wetness

Check that the sensor VCC is connected and the AOUT wire is connected to GPIO 34 (input-only ADC pin). Verify with a multimeter that the AOUT voltage changes when the sensor is wet.

Upgrades
  • Add Wi-Fi and send a push notification when leak is detected
  • Add multiple sensors on different GPIO ADC pins for multi-zone detection
  • Add a solenoid valve relay to cut main water supply automatically
  • Add an OLED showing which zone is wet and for how long
FAQ

You need an ESP32 DevKit, Water sensor AOUT, Water sensor DOUT, a breadboard, jumper wires, and a USB cable for power and programming.

Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.

Start with Beginner if you are new to Home Automation. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.

Overview

The intermediate build monitors three sensor zones (under kitchen sink, bathroom, and washing machine) using three moisture sensors. An OLED displays the current status of each zone. When any zone detects a leak, a Telegram notification is sent immediately with the zone name. NVS logs every leak event with an NTP timestamp. A manual reset button acknowledges the alert and silences the buzzer while keeping the LED on until the sensor is dry.

Components
  • 1× ESP32 DevKit V1
  • 3× Resistive water sensor strip — One per zone
  • 1× SSD1306 OLED 128x64 I2C
  • 1× Active buzzer
  • 1× Tactile reset button — Acknowledge and silence buzzer
  • 1× Wi-Fi router — Telegram notifications
Wiring
Component PinESP32 PinNotes
Zone 1 AOUT (kitchen)GPIO 34
Zone 2 AOUT (bathroom)GPIO 35
Zone 3 AOUT (washing machine)GPIO 36
OLED SDA/SCLGPIO 21/22
BuzzerGPIO 25
Reset buttonGPIO 0 to GNDINPUT_PULLUP; active LOW
Arduino Code
esp32-water-leak-detector_intermediate.ino
// ESP32 Water Leak Detector - Intermediate (3-zone + Telegram + NVS log)
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <Preferences.h>
#include <time.h>

Adafruit_SSD1306 oled(128,64,&Wire,-1);
Preferences prefs;

const char* SSID="YourSSID", *PASS="YourPass";
const char* BOT_TOKEN="YOUR_BOT_TOKEN";
const char* CHAT_ID="YOUR_CHAT_ID";

const char* ZONE_NAMES[3]={"Kitchen","Bathroom","Washer"};
const int SENSORS[3]={34,35,36};
const int BUZZER=25, RST_BTN=0;
const int WET_THRESH=2000;
bool wasWet[3]={false};
bool buzzerAck=false;

void sendTelegram(const String &msg){
  WiFiClientSecure client; client.setInsecure();
  if(!client.connect("api.telegram.org",443)) return;
  String body="chat_id="+String(CHAT_ID)+"&text="+msg;
  client.printf("POST /bot%s/sendMessage HTTP/1.1rn"
    "Host: api.telegram.orgrnContent-Type: application/x-www-form-urlencodedrn"
    "Content-Length: %drnrn%s",BOT_TOKEN,(int)body.length(),body.c_str());
  delay(1000); client.stop();
}

void logLeak(int zone){
  time_t now=time(nullptr);
  prefs.begin("leaks",false);
  int n=prefs.getInt("n",0);
  String entry=String(now)+":"+ZONE_NAMES[zone];
  prefs.putString(String(n).c_str(),entry);
  prefs.putInt("n",n+1);
  prefs.end();
}

void setup(){
  Serial.begin(115200);
  Wire.begin(21,22);
  oled.begin(SSD1306_SWITCHCAPVCC,0x3C); oled.setTextColor(WHITE);
  analogReadResolution(12);
  pinMode(BUZZER,OUTPUT); pinMode(RST_BTN,INPUT_PULLUP);
  WiFi.begin(SSID,PASS);
  while(WiFi.status()!=WL_CONNECTED) delay(500);
  configTime(0,0,"pool.ntp.org");
}

void loop(){
  bool anyWet=false;
  for(int i=0;i<3;i++){
    bool wet=(analogRead(SENSORS[i])>WET_THRESH);
    if(wet&&!wasWet[i]){
      logLeak(i);
      sendTelegram("WATER LEAK: "+String(ZONE_NAMES[i]));
      buzzerAck=false;
    }
    wasWet[i]=wet;
    if(wet) anyWet=true;
  }
  if(digitalRead(RST_BTN)==LOW) buzzerAck=true;
  digitalWrite(BUZZER,anyWet&&!buzzerAck?HIGH:LOW);

  oled.clearDisplay(); oled.setTextSize(1); oled.setCursor(0,0);
  for(int i=0;i<3;i++)
    oled.printf("%s: %sn",ZONE_NAMES[i],wasWet[i]?"WET!":"ok");
  oled.println(anyWet?"ALERT!":"All clear");
  oled.display();
  delay(2000);
}
How It Works
01

Rising-Edge Leak Detection: The wasWet[] array tracks the previous state of each zone. A Telegram alert and NVS log entry are only created on the rising edge (was dry, now wet) rather than on every 2-second loop iteration. This prevents flooding Telegram with repeated messages while a puddle persists.

02

Manual Buzzer Acknowledgement: The reset button sets buzzerAck=true, silencing the buzzer immediately. The LED and OLED alert remain active until the sensor reads dry again. This is the standard fire alarm model: acknowledge to silence audible alert but maintain visual alert until the hazard is resolved.

03

NVS Leak Event Log: Each new leak event is appended to NVS as a Unix timestamp combined with zone name. The log persists across power cuts and can be read back and formatted as a history report via Serial or a web endpoint.

04

Telegram HTTPS Request: sendTelegram() uses WiFiClientSecure with setInsecure() to skip certificate verification for simplicity. The HTTP POST body uses application/x-www-form-urlencoded encoding with chat_id and text fields. Telegram delivers the message to the specified chat within 1-3 seconds.

Applications
  • Whole-home leak detection network with zone-specific alerts
  • Rental property remote monitoring for water damage prevention
  • Holiday home protection system with instant Telegram notification
  • Commercial kitchen under-sink monitoring for compliance logging
Troubleshooting

Telegram message arrives but with wrong zone name

Verify ZONE_NAMES[] index matches SENSORS[] index. The array position must correspond: SENSORS[0] is the kitchen sensor, ZONE_NAMES[0] must be "Kitchen". Swap array entries if zones are misidentified.

Multiple Telegram messages sent for one leak event

The rising-edge detection (if wet && !wasWet[i]) should prevent duplicate messages. If duplicates occur, add a per-zone cooldown: record the last alert timestamp and skip if less than 60 seconds have passed since the previous alert.

Button press does not silence buzzer

With INPUT_PULLUP, the button reads HIGH when open and LOW when pressed. Verify the button connects GPIO 0 to GND when pressed. Also verify that buzzerAck is set inside the if(digitalRead(RST_BTN)==LOW) block.

Upgrades
  • Add a solenoid valve on the main water supply to cut water automatically
  • Add a web page showing the full NVS leak event log with timestamps
  • Add SMS notification as a fallback if Telegram is unavailable
  • Add a capacitive sensor in parallel with the resistive sensor for distilled water detection
FAQ

You need an ESP32 DevKit, Water sensor AOUT, Water sensor DOUT, a breadboard, jumper wires, and a USB cable for power and programming.

Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.

Start with Beginner if you are new to Home Automation. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.

Overview

The advanced build adds automatic main water shutoff via a solenoid valve relay. When any sensor zone detects a leak, the ESP32 immediately closes the solenoid valve, sends a Telegram alert with zone details, and publishes the event to MQTT. The valve can be remotely re-opened via MQTT after the leak is repaired. A watchdog timer ensures the valve closes if the ESP32 loses Wi-Fi for more than 5 minutes, preventing undetected leaks during network outages.

Components
  • 1× ESP32 DevKit V1
  • 3× Resistive water sensor
  • 1× 12 V solenoid valve (normally open) — Mounts on main water supply pipe; closes on power
  • 1× Relay module — Controls solenoid valve 12 V coil
  • 1× 12 V 1 A power supply — Solenoid valve power
  • 1× MQTT broker — Remote valve control and status
Wiring
Component PinESP32 PinNotes
Zone sensors AOUTGPIO 34/35/36
Solenoid valve relay INGPIO 25LOW = relay on = valve closed (water off)
Relay VCC/GND5 V / GND
Solenoid valve coilRelay COM/NO from 12 VWhen relay closes, 12 V energises solenoid; closes valve
Arduino Code
esp32-water-leak-detector_advanced.ino
// ESP32 Water Leak Detector - Advanced (solenoid shutoff + MQTT + watchdog)
#include <WiFi.h>
#include <PubSubClient.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include <time.h>

WiFiClient wifiClient; PubSubClient mqtt(wifiClient);
const char* SSID="YourSSID", *PASS="YourPass";
const char* MQTT_HOST="192.168.1.100";
const char* BOT_TOKEN="YOUR_BOT_TOKEN";
const char* CHAT_ID="YOUR_CHAT_ID";

const int SENSORS[3]={34,35,36};
const char* ZONES[3]={"Kitchen","Bathroom","Washer"};
const int VALVE_RELAY=25;
const int WET_THRESH=2000;
bool valveOpen=true;
bool wasWet[3]={false};
unsigned long lastWifi=0;
const unsigned long WIFI_WATCHDOG=300000UL; // 5 minutes

void setValve(bool open){
  valveOpen=open;
  digitalWrite(VALVE_RELAY,open?HIGH:LOW); // HIGH=relay off=valve open
  mqtt.publish("water/valve/state",open?"OPEN":"CLOSED",true);
  Serial.printf("Valve: %sn",open?"OPEN":"CLOSED");
}

void sendTelegram(const String &msg){
  WiFiClientSecure c; c.setInsecure();
  if(!c.connect("api.telegram.org",443)) return;
  String body="chat_id="+String(CHAT_ID)+"&text="+msg;
  c.printf("POST /bot%s/sendMessage HTTP/1.1rnHost: api.telegram.orgrn"
    "Content-Type: application/x-www-form-urlencodedrnContent-Length: %drnrn%s",
    BOT_TOKEN,(int)body.length(),body.c_str());
  delay(1000); c.stop();
}

void mqttCallback(char* topic, byte* payload, unsigned int len){
  String msg((char*)payload,len);
  if(String(topic)=="water/valve/set"){
    setValve(msg=="OPEN");
  }
}

void setup(){
  Serial.begin(115200);
  analogReadResolution(12);
  pinMode(VALVE_RELAY,OUTPUT);
  setValve(true); // start with valve open
  WiFi.begin(SSID,PASS);
  while(WiFi.status()!=WL_CONNECTED) delay(500);
  lastWifi=millis();
  configTime(0,0,"pool.ntp.org");
  mqtt.setServer(MQTT_HOST,1883);
  mqtt.setCallback(mqttCallback);
}

void loop(){
  if(WiFi.status()==WL_CONNECTED) lastWifi=millis();
  // Watchdog: close valve if Wi-Fi lost for >5 min
  if(millis()-lastWifi>WIFI_WATCHDOG && valveOpen){
    setValve(false);
    Serial.println("WATCHDOG: valve closed due to Wi-Fi loss");
  }
  if(!mqtt.connected() && WiFi.status()==WL_CONNECTED){
    mqtt.connect("LeakDetector");
    mqtt.subscribe("water/valve/set");
  }
  mqtt.loop();
  for(int i=0;i<3;i++){
    bool wet=(analogRead(SENSORS[i])>WET_THRESH);
    if(wet&&!wasWet[i]){
      setValve(false); // close main water supply immediately
      sendTelegram("LEAK DETECTED: "+String(ZONES[i])+" - Water supply closed!");
      StaticJsonDocument<128> doc;
      doc["zone"]=ZONES[i]; doc["valve"]="CLOSED";
      char buf[128]; serializeJson(doc,buf);
      mqtt.publish("water/leak",buf);
    }
    wasWet[i]=wet;
  }
  delay(2000);
}
How It Works
01

Normally-Open Solenoid Valve: A normally-open solenoid valve allows water flow when de-energised. Energising the coil (12 V via relay) closes the valve and stops water flow. This fail-safe design means water flows if the ESP32 power fails, avoiding inadvertent water shutoff during power cuts. Change to normally-closed for a safety-critical installation that cuts water on power failure.

02

Wi-Fi Watchdog Timer: lastWifi records the time of the last successful Wi-Fi connection. If Wi-Fi is continuously lost for more than WIFI_WATCHDOG milliseconds (5 minutes), the valve is closed as a precaution. This protects against the scenario where a leak occurs during a network outage and the Telegram alert cannot be sent.

03

MQTT Valve Remote Control: Publishing "OPEN" to water/valve/set re-opens the valve after a leak is repaired. Publishing "CLOSED" manually shuts off water remotely (e.g. leaving home during a storm). The current valve state is published to water/valve/state with retain=true for dashboard synchronisation.

04

Immediate Shutoff Priority: setValve(false) is called before sendTelegram() because sending an HTTPS request to Telegram can take 2-5 seconds. Closing the valve immediately minimises water damage. The Telegram notification arrives within seconds of the shutoff, giving the homeowner time to investigate before significant damage occurs.

Applications
  • Whole-home automatic water shutoff on leak detection
  • Vacation rental property remote water management
  • Insurance-compliant water damage prevention system
  • Laboratory water supply safety interlock
Troubleshooting

Valve closes but water continues flowing

Verify the solenoid valve is installed on the correct pipe and in the correct flow direction (marked on the valve body). Also check that the relay actually closes: measure 12 V across the solenoid coil terminals when the relay is activated.

Watchdog closes valve unnecessarily during brief Wi-Fi drops

Increase WIFI_WATCHDOG from 5 to 15 minutes for environments with intermittent Wi-Fi. Also add logic to attempt Wi-Fi reconnection immediately after detecting a drop, resetting the lastWifi timer on each successful reconnection attempt.

Valve will not open remotely after closing

The MQTT subscribe to water/valve/set must succeed before commands can be received. Verify the subscription fires after reconnection in the mqtt.connect() block. Also verify the MQTT payload is exactly "OPEN" (case-sensitive).

Upgrades
  • Add a flow meter to measure and log total water consumption
  • Add Home Assistant integration for valve control from the HA dashboard
  • Add a water pressure sensor to detect pipe burst (sudden pressure drop)
  • Add a secondary SMS alert via a GSM module as a fallback when internet is down
FAQ

You need an ESP32 DevKit, Water sensor AOUT, Water sensor DOUT, a breadboard, jumper wires, and a USB cable for power and programming.

Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.

Start with Beginner if you are new to Home Automation. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.