Practical case: ESP32 Water Leak Detector

Practical case: ESP32 Water Leak Detector — hero

case-device-block-diagram,
.prometeo-educational-note,
.prometeo-device-postcode-section,
.prometeo-device-section-card {
margin: 2.4rem 0;
padding: 1.45rem 1.55rem;
border: 1px solid rgba(148, 163, 184, 0.30);
border-radius: 14px;
background:
linear-gradient(135deg, rgba(30, 41, 59, 0.50), rgba(15, 23, 42, 0.18)),
rgba(17, 24, 39, 0.48);
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.20);
}
.case-objective > h2:first-of-type,
.case-device-block-diagram > h2:first-of-type,
.prometeo-device-postcode-section > h2:first-of-type,
.prometeo-device-section-card > h2:first-of-type {
margin-top: 0;
}
.prometeo-device-section-card > :last-child,
.case-objective > :last-child,
.case-device-block-diagram > :last-child,
.prometeo-device-postcode-section > :last-child {
margin-bottom: 0;
}
.prometeo-device-section-card.prometeo-device-section-card-code {
border-left: 4px solid rgba(56, 189, 248, 0.86);
background:
linear-gradient(135deg, rgba(8, 47, 73, 0.42), rgba(15, 23, 42, 0.18)),
rgba(15, 23, 42, 0.58);
}
.prometeo-device-section-card.prometeo-device-section-card-compact {
padding: 1.2rem 1.35rem;
}
.prometeo-device-section-card pre,
.case-objective pre,
.case-device-block-diagram pre,
.prometeo-device-postcode-section pre {
max-width: 100%;
}
.prometeo-device-postcode-section .prometeo-device-flow-item {
background:
linear-gradient(135deg, rgba(15, 23, 42, 0.50), rgba(30, 41, 59, 0.28)),
rgba(15, 23, 42, 0.28);
}
@media print {
.case-objective,
.case-device-block-diagram,
.prometeo-educational-note,
.prometeo-device-postcode-section,
.prometeo-device-section-card {
background: #fff;
color: #111827;
box-shadow: none;
break-inside: avoid;
page-break-inside: avoid;
}
}

Objective and use case

What you’ll build: A smart, WiFi-enabled water leak detection prototype that triggers an immediate local audio-visual alarm while hosting a live web dashboard and JSON API to report its status over the local network.

Why it matters / Use cases

  • Basement flooding prevention: Detects groundwater seepage or sump pump failures early, allowing intervention before severe water damage occurs.
  • Appliance monitoring: Placed under water heaters, washing machines, or dishwashers to catch slow leaks that typically go unnoticed.
  • HVAC condensation overflow: Monitors drip pans in air conditioning units to alert building managers before water overflows into drywall.
  • Smart Home Integration: Provides a foundational IoT edge device that can be queried by systems like Home Assistant via its JSON endpoint with <500ms latency.

Expected outcome

  • The ESP32 successfully connects to a local 2.4GHz WiFi network and prints its IP address to the serial monitor.
  • In a dry state, the status LED remains off, the buzzer is silent, and the web dashboard reports a “DRY” status.
  • When water bridges the sensor contacts, the device triggers a local alarm (<100ms response time) and instantly updates the JSON/web dashboard to a “WET” state.

Audience: IoT Developers, Smart Home Enthusiasts; Level: Intermediate

Architecture/flow: Analog/Digital Water Sensor → ESP32 GPIO → Local Hardware Alarm (Buzzer/LED) + Asynchronous Web Server → JSON API / Live Dashboard

Conceptual block diagram

High-level view: what enters the system, what each block processes, and what comes out.

Functional architecture

Water probe

ESP32 GPIO/ADC

Threshold logic

LED/buzzer

Wi-Fi alert

Conceptual flow: moisture detection, local decision and user alert.

Validation path

Source code

PlatformIO build

Flash

Serial monitor

Conceptual summary of the tools used to check the published ESP32 project.

Educational validation note

Before publication, this case passed the Prometeo automated validation gate with status PASS. For this ESP32 DevKitC profile, the project was checked as a PlatformIO project: the validator extracted platformio.ini and src/main.cpp, created a temporary project and ran pio run against platform = espressif32, board = esp32dev and framework = arduino. It also checked article structure, copy/paste-safe ASCII command options, and unsupported stacks such as direct ESP-IDF or non-scoped ESP32 boards.

Published validation evidence

  • Automatic result: PASS.
  • Parsed structure: 4 sections, 4 tables and 2 code blocks detected before publication.
  • Checked code: 1 PlatformIO config + 1 ESP32 source/pio run.
  • Supported catalog: the article text was checked against Prometeo’s validation-capable device profiles, and unsupported stacks block publication.
  • Report findings: no blocking findings.

This validation confirms syntax and tool compatibility for the published code, but it does not replace physical testing on your exact ESP32 DevKitC board, wiring, power supply and local WiFi environment.

Educational safety note

Electrical and water hazard: this project interacts with water and must remain a low-voltage educational prototype. Do not place it near exposed outlets or 110/220 V wiring, power the ESP32 DevKitC from USB or an isolated battery, keep GPIO signals at 3.3 V, and always disconnect power before handling sensors, changing wires, or moving the device.

Prerequisites

To successfully complete this tutorial, you will need:
* Software: Visual Studio Code with the PlatformIO IDE extension installed.
* Knowledge: Basic understanding of C++ syntax, fundamental digital logic (HIGH/LOW states), and how to connect to a 2.4GHz WiFi network.
* System: A computer with an available USB port and the appropriate USB-to-UART drivers installed (CP210x or CH34x, depending on your specific ESP32 DevKitC variant).

Materials

You will need the following hardware components. Ensure you are using the exact device model specified.

ComponentDescription / SpecificationQuantity
ESP32 DevKitCStandard 38-pin or 30-pin ESP32 development board.1
Water leak sensorDigital or analog water sensor module (e.g., standard analog/digital comparator module with exposed trace probes). We will use the digital output (DO) pin.1
Active buzzer3.3V or 5V active buzzer module (sounds continuously when provided a DC voltage).1
Status LEDStandard 5mm LED (Red or Yellow preferred for alerts).1
Resistor220-ohm resistor for the status LED to limit current.1
BreadboardStandard half-size or full-size breadboard for prototyping.1
Jumper WiresAssorted male-to-male and female-to-male Dupont wires.1 set
Micro-USB CableData-capable USB cable for programming and power.1

Setup/Connection

Carefully wire the components on your breadboard according to the table below. Make sure the ESP32 is unplugged from your computer while making connections.

Pinout Mapping

ESP32 DevKitC PinComponentComponent PinNotes
3V3Water Leak SensorVCCPowers the sensor logic.
GNDWater Leak SensorGNDCommon ground.
GPIO 32Water Leak SensorDO (Digital Out)Pulled LOW or HIGH depending on the module when water is detected. (Code assumes HIGH on leak, adjust if your module is active-LOW).
3V3 / 5VActive BuzzerVCCPowers the buzzer. Check your buzzer’s voltage rating.
GNDActive BuzzerGNDCommon ground.
GPIO 26Active BuzzerI/O (Signal)Triggers the buzzer when set to HIGH.
GPIO 27Status LEDAnode (Long leg)Connect via the 220-ohm resistor.
GNDStatus LEDCathode (Short leg)Common ground.

Hardware Note: Active buzzers generate their own tone when a simple HIGH signal is applied. Do not confuse this with a passive buzzer, which requires a PWM (Pulse Width Modulation) signal to generate sound. We are using an active buzzer for simplicity.

Validated Code

Create a new project in PlatformIO, selecting the Espressif ESP32 Dev Module as your board and Arduino as the framework. Replace the contents of the auto-generated files with the code below.

platformio.ini

This file configures the PlatformIO build environment, specifying the board, framework, and serial monitor speed.

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200

src/main.cpp

This is the main application code. It implements a non-blocking state machine to flash the LED and pulse the buzzer without interrupting the WiFi web server. Important: Replace YOUR_WIFI_SSID and YOUR_WIFI_PASSWORD with your actual 2.4GHz network credentials.

Public preview of the validated file. The complete source is shown to members and in PDF/Print.

#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>

// ---------------------------------------------------------
// Network Credentials
// ---------------------------------------------------------
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";

// ---------------------------------------------------------
// Pin Definitions
// ---------------------------------------------------------
const int SENSOR_PIN = 32;  // Digital input from water sensor
const int BUZZER_PIN = 26;  // Output to active buzzer
const int LED_PIN = 27;     // Output to status LED

// ---------------------------------------------------------
// Global State Variables
// ---------------------------------------------------------
bool isLeakDetected = false;

// Non-blocking timing variables for the alarm
unsigned long previousMillis = 0;
const long alarmInterval = 500; // Blink/Beep interval in milliseconds
bool alarmState = false;

// ---------------------------------------------------------
// Web Server Initialization (Port 80)
// ---------------------------------------------------------
WebServer server(80);

// Function to generate the HTML dashboard
void handleRoot() {
    String html = "<!DOCTYPE html><html><head>";
    html += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">";
    // Auto-refresh the page every 5 seconds
    html += "<meta http-equiv=\"refresh\" content=\"5\">";
    html += "<title>Water Leak Monitor</title>";
    html += "<style>";
    html += "body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; background-color: #f4f4f9; }";
    html += "h1 { color: #333; }";
    html += ".status-box { display: inline-block; padding: 40px; border-radius: 10px; font-size: 24px; font-weight: bold; color: white; }";
    html += ".dry { background-color: #4CAF50; }"; // Green for safe
    html += ".leak { background-color: #F44336; animation: blinker 1s linear infinite; }"; // Red for danger
    html += "@keyframes blinker { 50% { opacity: 0.5; } }";
    html += "</style></head><body>";

    html += "<h1>ESP32 Water Leak Monitor</h1>";

    if (isLeakDetected) {
        html += "<div class=\"status-box leak\">WARNING: LEAK DETECTED!</div>";
        html += "<p>Water has been detected by the sensor.</p>";
    } else {
        html += "<div class=\"status-box dry\">STATUS: DRY</div>";
        html += "<p>No water detected. System normal.</p>";
    }

    html += "</body></html>";

    server.send(200, "text/html", html);
}

// Function to provide a JSON API endpoint for smart home integration
void handleStatusJSON() {
    String json = "{";
    json += "\"leak\": " + String(isLeakDetected ? "true" : "false") + ",";
    json += "\"system\": \"ESP32-DevKitC\"";
    json += "}";

    server.send(200, "application/json", json);
}
// ...

🔒 This content is premium. With the monthly subscription (7-day free trial) you can unlock the full didactic material and the print-ready PDF pack.🔓 Unlock it — 7-day free trial
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>

// ---------------------------------------------------------
// Network Credentials
// ---------------------------------------------------------
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";

// ---------------------------------------------------------
// Pin Definitions
// ---------------------------------------------------------
const int SENSOR_PIN = 32;  // Digital input from water sensor
const int BUZZER_PIN = 26;  // Output to active buzzer
const int LED_PIN = 27;     // Output to status LED

// ---------------------------------------------------------
// Global State Variables
// ---------------------------------------------------------
bool isLeakDetected = false;

// Non-blocking timing variables for the alarm
unsigned long previousMillis = 0;
const long alarmInterval = 500; // Blink/Beep interval in milliseconds
bool alarmState = false;

// ---------------------------------------------------------
// Web Server Initialization (Port 80)
// ---------------------------------------------------------
WebServer server(80);

// Function to generate the HTML dashboard
void handleRoot() {
    String html = "<!DOCTYPE html><html><head>";
    html += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">";
    // Auto-refresh the page every 5 seconds
    html += "<meta http-equiv=\"refresh\" content=\"5\">";
    html += "<title>Water Leak Monitor</title>";
    html += "<style>";
    html += "body { font-family: Arial, sans-serif; text-align: center; margin-top: 50px; background-color: #f4f4f9; }";
    html += "h1 { color: #333; }";
    html += ".status-box { display: inline-block; padding: 40px; border-radius: 10px; font-size: 24px; font-weight: bold; color: white; }";
    html += ".dry { background-color: #4CAF50; }"; // Green for safe
    html += ".leak { background-color: #F44336; animation: blinker 1s linear infinite; }"; // Red for danger
    html += "@keyframes blinker { 50% { opacity: 0.5; } }";
    html += "</style></head><body>";

    html += "<h1>ESP32 Water Leak Monitor</h1>";

    if (isLeakDetected) {
        html += "<div class=\"status-box leak\">WARNING: LEAK DETECTED!</div>";
        html += "<p>Water has been detected by the sensor.</p>";
    } else {
        html += "<div class=\"status-box dry\">STATUS: DRY</div>";
        html += "<p>No water detected. System normal.</p>";
    }

    html += "</body></html>";

    server.send(200, "text/html", html);
}

// Function to provide a JSON API endpoint for smart home integration
void handleStatusJSON() {
    String json = "{";
    json += "\"leak\": " + String(isLeakDetected ? "true" : "false") + ",";
    json += "\"system\": \"ESP32-DevKitC\"";
    json += "}";

    server.send(200, "application/json", json);
}

// ---------------------------------------------------------
// Setup Function
// ---------------------------------------------------------
void setup() {
    // Initialize Serial Monitor
    Serial.begin(115200);
    delay(1000);
    Serial.println("\nInitializing Water Leak Monitor...");

    // Configure GPIO Pins
    pinMode(SENSOR_PIN, INPUT_PULLDOWN); // Ensure pin doesn't float
    pinMode(BUZZER_PIN, OUTPUT);
    pinMode(LED_PIN, OUTPUT);

    // Ensure alarm is off at boot
    digitalWrite(BUZZER_PIN, LOW);
    digitalWrite(LED_PIN, LOW);

    // Connect to WiFi
    Serial.print("Connecting to WiFi network: ");
    Serial.println(ssid);
    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }

    Serial.println("\nWiFi connected successfully!");
    Serial.print("IP Address: ");
    Serial.println(WiFi.localIP());

    // Define Web Server Routes
    server.on("/", handleRoot);
    server.on("/status", handleStatusJSON);

    // Start the Web Server
    server.begin();
    Serial.println("HTTP server started.");
}

// ---------------------------------------------------------
// Main Loop
// ---------------------------------------------------------
void loop() {
    // 1. Handle incoming HTTP client requests
    server.handleClient();

    // 2. Read the water sensor
    // Note: Adjust logic if your sensor module is active-LOW (e.g., reads LOW when wet)
    int sensorValue = digitalRead(SENSOR_PIN);

    if (sensorValue == HIGH) {
        if (!isLeakDetected) {
            Serial.println("ALERT: Water Leak Detected!");
            isLeakDetected = true;
        }
    } else {
        if (isLeakDetected) {
            Serial.println("INFO: Sensor is dry. System normalized.");
            isLeakDetected = false;

            // Immediately shut off alarm outputs when dry
            digitalWrite(LED_PIN, LOW);
            digitalWrite(BUZZER_PIN, LOW);
        }
    }

    // 3. Handle non-blocking local alarm (LED and Buzzer)
    if (isLeakDetected) {
        unsigned long currentMillis = millis();

        if (currentMillis - previousMillis >= alarmInterval) {
            previousMillis = currentMillis;

            // Toggle the alarm state
            alarmState = !alarmState;

            // Update hardware pins
            digitalWrite(LED_PIN, alarmState ? HIGH : LOW);
            digitalWrite(BUZZER_PIN, alarmState ? HIGH : LOW);
        }
    }
}

Build/Flash/Run commands

Use the PlatformIO Core CLI (or the integrated terminal in VS Code) to execute the following commands.

CommandPurpose
pio runCompiles the project to ensure there are no syntax errors.
pio run --target uploadCompiles and flashes the firmware to the ESP32 over USB.
pio device monitorOpens the serial monitor to view log outputs and find the IP address.

Workflow:
1. Open the PlatformIO terminal.
2. Run pio run to verify your code compiles successfully.
3. Connect your ESP32 DevKitC via USB.
4. Run pio run --target upload. (If the upload fails to connect, you may need to hold the “BOOT” button on the ESP32 while the terminal says “Connecting…”).
5. Run pio device monitor to observe the boot sequence, verify WiFi connection, and obtain the assigned IP address.

Step-by-step Validation

Follow these checkpoints to verify the system functions exactly as intended.

  1. Compilation Check
  2. Action: Execute pio run.
  3. Expected Observation: Terminal outputs SUCCESS with no compilation errors.
  4. Pass Condition: The firmware binary is successfully generated.
  5. Network Connection Check
  6. Action: Execute pio device monitor and press the EN (Reset) button on the ESP32.
  7. Expected Observation: The serial monitor prints “Connecting to WiFi network…”, prints dots, and eventually displays “WiFi connected successfully!” followed by an IP address (e.g., 192.168.1.50).
  8. Pass Condition: The ESP32 is reachable on the local network.
  9. Dry State Verification
  10. Action: Leave the water sensor dry. Observe the hardware.
  11. Expected Observation: The LED is off, the buzzer is silent, and the serial monitor does not print any alerts.
  12. Pass Condition: The system remains stable in standby mode.
  13. Web Dashboard Verification
  14. Action: Open a web browser on a device connected to the same network and enter the ESP32’s IP address.
  15. Expected Observation: A web page loads displaying a green box with the text “STATUS: DRY”.
  16. Pass Condition: The web server successfully handles root (/) HTTP requests.
  17. Wet State (Leak) Verification
  18. Action: Dip the probes of the water sensor into a shallow dish of tap water (or bridge the contacts with a damp paper towel).
  19. Expected Observation: The serial monitor prints “ALERT: Water Leak Detected!”. The LED blinks and the buzzer pulses on and off every 500 milliseconds.
  20. Pass Condition: The hardware alarm triggers immediately without freezing the microcontroller.
  21. Remote Alert Verification
  22. Action: While the sensor is wet, look at the web browser (wait for the 5-second auto-refresh or refresh manually). Navigate to http://<IP_ADDRESS>/status.
  23. Expected Observation: The main page updates to a blinking red “WARNING: LEAK DETECTED!”. The /status endpoint returns {"leak": true, "system": "ESP32-DevKitC"}.
  24. Pass Condition: The network interface accurately reflects the physical hardware state in real-time.

Troubleshooting

SymptomLikely causeFix
Code fails to upload (Timeout)ESP32 is not entering bootloader mode automatically.Hold the “BOOT” button on the DevKitC when the console displays “Connecting…”. Release it once uploading begins.
Cannot connect to WiFi (Endless dots)Incorrect SSID/Password, or trying to connect to a 5GHz network.Verify credentials in main.cpp. Ensure your router is broadcasting a 2.4GHz band (ESP32 does not support 5GHz).
Sensor triggers when dry (False Positives)Floating input pin or extreme humidity.Ensure INPUT_PULLDOWN is set in code. Adjust the potentiometer on the sensor module (if equipped) to decrease sensitivity.
Buzzer makes a clicking sound, not a toneYou are using a passive buzzer instead of an active buzzer.Replace with an active buzzer, or rewrite the digitalWrite logic to use tone() or ledcWrite() for PWM generation.
Web page is slow to load or times outBlocking code (delay()) is halting the web server loop.Ensure you are using the millis() logic provided in the Validated Code section. Do not use delay() inside the main loop().

Improvements

Once you have mastered the basic prototype, consider these architectural enhancements:

  • Power Optimization:
    • Implement ESP32 Deep Sleep mode. The device can sleep to save battery, waking up only when the GPIO pin attached to the water sensor triggers an interrupt (using esp_sleep_enable_ext0_wakeup).
  • Advanced Remote Notifications:
    • Integrate an MQTT client (PubSubClient.h) to push alerts to a central Home Assistant broker rather than relying on a pull-based web server.
    • Use an HTTP POST request to trigger external webhooks (like IFTTT, Pushbullet, or Telegram Bots) for push notifications to your smartphone.
  • Hardware Reliability:
    • Apply conformal coating to the ESP32 board (avoiding the USB port and buttons) to protect the microcontroller itself from moisture in damp environments like basements.
    • Use a dedicated power supply or a large LiPo battery with a charging circuit instead of relying on USB power.

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What is the primary objective of the prototype described in the article?




Question 2: What kind of alarm does the prototype trigger when a leak is detected?




Question 3: How does the prototype report its status over the local network?




Question 4: Which of the following is a mentioned use case for basement flooding prevention?




Question 5: Where can the prototype be placed for appliance monitoring?




Question 6: Why is the prototype useful for HVAC systems?




Question 7: What smart home system is specifically mentioned as being able to query the device?




Question 8: What is the expected latency when querying the device's JSON endpoint?




Question 9: What type of WiFi network does the ESP32 successfully connect to in the expected outcome?




Question 10: What happens to the device in a dry state?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:
Scroll to Top