Ever left your water pump running and came back to a mini swimming pool on your terrace? I have. That's why I built this smart water pump controller using ESP32, Firebase, and a simple web dashboard. With just a browser, I can now switch the pump ON or OFF, detect leakage and even calculate the water usage bill. And yes, it’s as cool as it sounds.
Whether you’re diving into IoT or just tired of forgetting to turn off your pump, this is a fun and useful project to try out. It’s also a great way to dip your toes into cloud-connected hardware without getting overwhelmed.
🔍 Why This Project?
In every other house, someone’s yelling "Hey, turn off the motor!" and someone else is replying "Oh no, I forgot!" 🤦🏼♂️
This everyday chaos inspired me to build a smarter solution, a system that lets me control the motor remotely from my phone or laptop. It’s simple, practical, and surprisingly satisfying to see it work in real time.
So I thought, why not create a controller that:
- Works from any web browser
- Uses Firebase for real-time data sync
- Doesn’t require expensive components
- Saves me from daily “water fights” at home
- And even calculates the water usage bill
It’s a great starter project to learn about IoT + cloud + web UI.
Also, since the ESP32 comes with built-in Wi-Fi, it’s perfect for projects like this. Pairing it with Firebase Realtime Database gives you instant updates without the need for a custom backend. Win-win!
🛠️ What You'll Need
Before jumping into the build, make sure you gather all the necessary hardware and software tools.
Hardware
Here’s what I used:
- ESP32 Dev Board – the brain of your project, with built-in Wi-Fi.
- 5V Relay Module – acts as an electronic switch to control the pump.
- Two YF-S201 Water Flow Sensors – measure the water flow rate for tank input and house usage.
- DC Submersible Pump (3V–5V) – a basic mini pump for testing.
- Breadboard & Jumper Wires – for easy wiring and prototyping.
- 5V Power Supply or Battery – powers the pump separately from the ESP32.
- Transparent PVC Soft Water Pipe – to connect your sensors and pump.
Software
You don’t need a fancy dev setup. Just the essentials:
- Arduino IDE – for coding the ESP32
- Firebase Console – your cloud-based database
- Web Browser – to run the UI locally or host it online
- (Optional) VS Code – if you like an advanced editor to tweak your HTML/JS
If this is your first Firebase project, don’t worry. It’s actually very beginner-friendly, and I’ll guide you step by step.
🛠️ Step-by-Step Tutorial
Let’s break it down. You can follow along one section at a time.
✅ 1.Hardware connections
Let’s wire up all the components and bring the system to life.
- Take a look at the circuit diagram below to get an overview of the connections.
- Mount the ESP32 firmly on the breadboard.
-
Connect power:
- Use a red wire to connect the 3V3 pin of the ESP32 to the positive (power) rail on the breadboard.
- Use a green wire to connect the GND pin of the ESP32 to the negative (ground) rail.
- Connect Water Flow Sensor 1:
- Red wire → Power rail (+3.3V)
- Black wire → Ground rail (GND)
- Yellow wire → GPIO D18 on the ESP32 (signal pin)
- Connect Water Flow Sensor 2:
- Red wire → Power rail (+3.3V)
- Black wire → Ground rail (GND)
- Yellow wire → GPIO D19 on the ESP32
- Connect the Relay Module:
- Red wire (VCC) → Power rail (+3.3V or +5V as per your relay module)
- Green wire (GND) → Ground rail
- Black wire (IN) → GPIO D23 on the ESP32
- Connect the Pump and Power Supply:
- Connect the positive terminal of the battery to the COM (Common) pin on the relay.
- Connect the negative terminal of the battery to the negative terminal of the water pump.
- Finally, connect the positive terminal of the pump to the NC (Normally Closed) pin of the relay.
That’s it, your hardware connections are now complete!
Double-check your wiring before powering on the circuit to make sure all connections are secure and correct.
✅ 2. Setup Firebase
- Head over to Firebase Console and click “Get Started.”
- Create a new project — name it “AquaFlowproj.”
- Turn off Gemini and Google Analytics, then click Create Project.
- Once it’s ready, go to the Build section on the left sidebar and select Realtime Database.
- Click Create Database, choose your nearest region (I used Singapore, Asia), then select “Start in Test Mode” and click Enable.
- At the root path of your database, add the following keys: data flow1 flow2 pump
- Next, open the Rules tab, replace the existing rules with the custom rules given below, and click Publish.
{
"rules": {
".read": true,
".write": true,
"data": {
".read": true,
".write": true
},
"pump": {
".read": true,
".write": true
},
"flow1": {
".read": true,
".write": true
},
"flow2": {
".read": true,
".write": true
}
}
}
- Now click the gear icon ⚙️ → Project Settings.
- Under the General tab, scroll to the Your Apps section and select Web.
- Add an app nickname, click Register App, and then Continue to Console.
- You’ll now see all your Firebase configuration details like apiKey, authDomain, databaseURL, etc. copy these into a Notepad for later use.
- Next, go to Build → Authentication.
- Click Get Started, then choose Email/Password as the Sign-in Method and click Enable → Save.
- Under the Users tab, click Add User.
Example:
- Email: project@user.com
- Password: project123
- Save these login credentials along with your Firebase details, we’ll need them soon in the website code.
✅ 3. Setup Arduino IDE
- Open Arduino IDE and paste this ESP32 code.
/*
AquaFlow - By Yugesh
This code connects an ESP32 to Firebase and monitors
two water flow sensors (YF-S401) along with a relay
for pump control. Data (flow1 & flow2) is sent to Firebase
every second, and pump commands (ON/OFF/AUTO) are received
from the database in real time.
Before running this code:
1. Replace Wi-Fi and Firebase credentials with your own.
2. Ensure your Firebase Realtime Database structure (contains nodes: /pump, /flow1, /flow2)
3. Connect components according to the pin config below.
*/
#include <WiFi.h>
#include <Firebase_ESP_Client.h>
#include "addons/TokenHelper.h"
// Wi-Fi Configuration – CHANGE THESE VALUES
#define WIFI_SSID "Your_WiFi_Name" // Replace with your Wi-Fi name
#define WIFI_PASSWORD "Your_WiFi_Password" // Replace with your Wi-Fi password
// Firebase Configuration – CHANGE THESE VALUES
#define API_KEY "Your_Firebase_API_Key" // Get from Firebase project settings
#define DATABASE_URL "https://your-database-url.firebaseio.com/" // Your Firebase RTDB URL
#define USER_EMAIL "your_email@example.com" // Must be a registered Firebase user
#define USER_PASSWORD "your_password" // Corresponding password
// Firebase Objects
FirebaseData fbdo;
FirebaseAuth auth;
FirebaseConfig config;
// Pin Configuration (ESP32 GPIO pins)
// You can change these if your wiring differs.
const int relayPin = 23; // Relay control pin (Active LOW)
const int flowSensor1 = 18; // Flow sensor 1 input pin (YF-S401)
const int flowSensor2 = 19; // Flow sensor 2 input pin (YF-S401)
// Flow Measurement Variables
volatile int pulseCount1 = 0;
volatile int pulseCount2 = 0;
unsigned long lastSendTime = 0;
// Interrupt Service Routines for Flow Sensors
void IRAM_ATTR pulseCounter1()
{
pulseCount1++;
}
void IRAM_ATTR pulseCounter2()
{
pulseCount2++;
}
// Setup Function
void setup()
{
Serial.begin(115200);
// Relay setup
pinMode(relayPin, OUTPUT);
digitalWrite(relayPin, HIGH); // Relay OFF initially (active LOW)
// Flow sensor setup
pinMode(flowSensor1, INPUT_PULLUP);
pinMode(flowSensor2, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(flowSensor1), pulseCounter1, FALLING);
attachInterrupt(digitalPinToInterrupt(flowSensor2), pulseCounter2, FALLING);
// Wi-Fi connection
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(300);
}
Serial.println("\nWi-Fi Connected");
// Firebase setup
config.api_key = API_KEY;
config.database_url = DATABASE_URL;
auth.user.email = USER_EMAIL;
auth.user.password = USER_PASSWORD;
config.token_status_callback = tokenStatusCallback;
Firebase.begin(&config, &auth);
Firebase.reconnectWiFi(true);
Serial.println("Firebase Initialized");
}
// Loop Function
void loop()
{
if (Firebase.ready()) {
// Read pump status from Firebase
if (Firebase.RTDB.getString(&fbdo, "/pump")) { // Path: /pump (do not change unless needed)
String command = fbdo.to<String>();
Serial.print("Firebase command: ");
Serial.println(command);
if (command == "ON")
{
digitalWrite(relayPin, LOW); // Turn ON pump
}
else if (command == "OFF")
{
digitalWrite(relayPin, HIGH); // Turn OFF pump
}
else if (command == "AUTO") {
// Optional: Add automation logic based on sensor data
}
} else
{
Serial.print("Failed to read pump: ");
Serial.println(fbdo.errorReason());
}
// Send flow data every 1 second
if (millis() - lastSendTime > 1000) {
detachInterrupt(digitalPinToInterrupt(flowSensor1));
detachInterrupt(digitalPinToInterrupt(flowSensor2));
// Convert pulse counts to flow rate (L/min)
float flowRate1 = (pulseCount1 / 7.5);
float flowRate2 = (pulseCount2 / 7.5);
pulseCount1 = 0;
pulseCount2 = 0;
lastSendTime = millis();
attachInterrupt(digitalPinToInterrupt(flowSensor1), pulseCounter1, FALLING);
attachInterrupt(digitalPinToInterrupt(flowSensor2), pulseCounter2, FALLING);
Serial.printf("Flow1: %.2f L/min | Flow2: %.2f L/min\n", flowRate1, flowRate2);
// Send data to Firebase
bool success1 = Firebase.RTDB.setFloat(&fbdo, "/flow1", flowRate1); // Path: /flow1
if (success1)
{
Serial.println("flow1 sent to Firebase");
}
else
{
Serial.print("flow1 failed: ");
Serial.println(fbdo.errorReason());
}
bool success2 = Firebase.RTDB.setFloat(&fbdo, "/flow2", flowRate2); // Path: /flow2
if (success2)
{
Serial.println("flow2 sent to Firebase");
}
else
{
Serial.print("flow2 failed: ");
Serial.println(fbdo.errorReason());
}
}
}
delay(100); // Keep loop responsive for accurate flow measurement
}
/*
Notes:
- Wi-Fi and Firebase credentials must be updated before upload.
- Ensure you’ve installed the “Firebase ESP Client” library by Mobizt.
- Flow sensor calibration constant (7.5) is for YF-S401; adjust if using a different model.
- Database paths (/pump, /flow1, /flow2) should exist in your Firebase RTDB.
*/
- Update the placeholders (API key, Auth domain, etc.) using the Firebase details you saved earlier.
- Connect your ESP32 board to your computer via USB.
- Open Library Manager (Sketch → Include Library → Manage Libraries) and install these two libraries:
- Firebase Arduino Client Library for ESP8266 and ESP32 by Mobizt
- Firebase ESP32 Client by Mobizt
- Save your sketch (File → Save As) in a new folder named, for example, AquaFlow.
- In the same folder, create a new file called TokenHelper.h and paste the code given below.
#ifndef TOKEN_HELPER_H
#define TOKEN_HELPER_H
// Provide the token generation process info
void tokenStatusCallback(TokenInfo info){
Serial.printf("Token info: type = %s, status = %s\n",
getTokenType(info).c_str(),
getTokenStatus(info).c_str());
}
#endif
- Go to Tools → Board → ESP32 → ESP32 Dev Module.
- Then go to Tools → Port and select the correct COM port (e.g., COM3).
- Click the Upload (→) icon to flash the code to your ESP32.
- Once the upload completes, open the Serial Monitor to confirm that the ESP32 connects to Wi-Fi successfully.
- If you see your device connected, congratulations, your ESP is now talking to Firebase!
✅ 4. Setting Up the Web Dashboard
- Open Visual Studio Code (or any code editor).
- Create three files in a folder:
Create index.html file and paste the below code
<!DOCTYPE html>
<html lang="en">
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AquaFlow Dashboard</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>AquaFlow</h1>
<div class="dashboard">
<!-- MOTOR CONTROL -->
<div class="section">
<h2>Motor Control</h2>
<div class="switches">
<button id="onBtn" onclick="setPump('ON')">ON</button>
<button id="offBtn" onclick="setPump('OFF')">OFF</button>
</div>
</div>
<!-- SENSOR STATUS -->
<div class="section">
<h2>Sensor Status</h2>
<div class="sensor-status">
<div><p>S1</p><div id="s1Status" class="status-dot"></div></div>
<div><p>S2</p><div id="s2Status" class="status-dot"></div></div>
</div>
</div>
<!-- WATER USAGE -->
<div class="section">
<h2>Water Usage</h2>
<div class="usage">
<div class="card"><p><b>Live</b></p><div class="value">₹<span id="livePrice">0</span></div><p><span id="liveLiters">0</span>L</p></div>
<div class="card"><p><b>Weekly</b></p><div class="value">₹<span id="weekPrice">0</span></div><p><span id="weekLiters">0</span>L</p></div>
<div class="card"><p><b>Monthly</b></p><div class="value">₹<span id="monthPrice">0</span></div><p><span id="monthLiters">0</span>L</p></div>
</div>
<button id="resetBtn">Reset</button>
</div>
<!-- LIVE FLOW -->
<div class="section">
<h2>Sensor Flow</h2>
<div class="flow">
<div class="card"><p><b>S1</b></p><div class="value"><span id="flow1">0</span> ml/sec</div></div>
<div class="card"><p><b>S2</b></p><div class="value"><span id="flow2">0</span> ml/sec</div></div>
</div>
</div>
<!-- LEAKAGE DETECTION -->
<div class="section">
<h2>Leakage Detection</h2>
<div class="sensor-status">
<div>
<div id="leakStatus" class="status-dot"></div>
</div>
</div>
</div>
<!-- ABOUT SECTION -->
<div class="section">
<p>Project by <b>Yugesh</b></p>
<div class="social">
<a href="https://www.linkedin.com/in/yugeshweb" target="_blank" title="LinkedIn">
<i class="fa-brands fa-linkedin"></i>
</a>
</div>
</div>
</div>
<script type="module" src="script.js"></script>
</body>
</html>
Create style.css file and paste the below code
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Poppins', sans-serif;
}
html, body {
height: 100%;
}
body {
background: url('Aquabg.jpg') repeat center center/cover;
display: grid;
grid-template-rows: 80px 1fr;
gap: 10px;
padding: 10px;
color: #ffffff;
overflow: hidden;
position: relative;
}
@media(max-width: 768px){
body {
overflow: auto;
height: auto;
}
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(102,126,234,0.1), rgba(118,75,162,0.1));
z-index: 0;
}
h1 {
text-align: center;
font-size: 2.5rem;
font-weight: 700;
z-index: 1;
}
.dashboard {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: 10px;
height: 100%;
z-index: 1;
}
@media(max-width: 900px){
.dashboard {
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(3, 1fr);
}
}
@media(max-width: 600px){
.dashboard {
grid-template-columns: 1fr;
grid-template-rows: repeat(6, 1fr);
}
}
.section {
background: rgba(255,255,255,0.25);
border-radius: 20px;
backdrop-filter: blur(15px);
padding: 15px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 1px solid rgba(255,255,255,0.4);
}
.section h2 {
margin-bottom: 10px;
font-size: 1.2rem;
font-weight: 600;
text-align: center;
}
.switches {
display: flex;
gap: 10px;
}
button {
background: rgba(255,255,255,0.4);
border: 2px solid #000;
color: #000;
font-weight: 600;
padding: 8px 20px;
border-radius: 25px;
cursor: pointer;
transition: 0.3s;
}
button.active {
background: #000;
color: #fff;
}
.sensor-status {
display: flex;
gap: 15px;
justify-content: center;
}
.sensor-status > div {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
padding: 10px;
background: rgba(255,255,255,0.2);
border-radius: 15px;
min-width: 60px;
border: 1px solid rgba(255,255,255,0.3);
}
.status-dot {
width: 20px;
height: 20px;
border-radius: 50%;
background: #ff6b6b;
border: 2px solid rgba(0,0,0,0.2);
transition: 0.3s;
}
.status-dot.active {
background: #51cf66;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
.card {
background: rgba(255,255,255,0.3);
border-radius: 15px;
padding: 10px;
text-align: center;
width: 90px;
margin: 5px;
}
.value {
font-size: 1.2rem;
font-weight: 700;
}
.usage, .flow, .leakage {
display: flex;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
}
#resetBtn {
background: rgb(35, 6, 6);
color: #fff;
border: none;
padding: 6px 15px;
border-radius: 25px;
cursor: pointer;
margin-top: 5px;
}
.social-links {
display: flex;
flex-direction: column;
gap: 5px;
align-items: center;
margin-top: 10px;
}
.social-links a {
text-decoration: none;
color: #000;
font-weight: 600;
transition: 0.3s;
}
.social-links a:hover {
color: #51cf66;
transform: scale(1.05);
}
.social a {
color: #c2c2c2;
font-size: 30px;
text-decoration: none;
}
.social a:hover {
color: #000000;
}
Create script.js file and paste the below code
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.0.1/firebase-app.js";
import { getDatabase, ref, onValue, set, get } from "https://www.gstatic.com/firebasejs/11.0.1/firebase-database.js";
// Replace these values with your own Firebase credentials
const firebaseConfig = {
apiKey: "YOUR_API_KEY_HERE",
authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
databaseURL: "https://YOUR_PROJECT_ID-default-rtdb.YOUR_REGION.firebasedatabase.app",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_PROJECT_ID.appspot.com",
messagingSenderId: "XXXXXX",
appId: "1:XXXX:web:XXXX"
};
const app = initializeApp(firebaseConfig);
const db = getDatabase(app);
const flow1Ref = ref(db, "flow1");
const flow2Ref = ref(db, "flow2");
const pumpRef = ref(db, "pump");
const dataRef = ref(db, "data");
let totalLiters = 0, totalPrice = 0, lastF1 = 0, lastF2 = 0, lastFlow1 = 0, lastFlow2 = 0;
// Load permanent data once
async function loadData() {
const snap = await get(dataRef);
if (snap.exists()) {
const d = snap.val();
totalLiters = d.totalLiters || 0;
totalPrice = d.totalPrice || 0;
lastFlow1 = d.lastFlow1 || 0;
lastFlow2 = d.lastFlow2 || 0;
updateUsageDisplay();
}
}
loadData();
// Save permanent data
function saveData() {
set(dataRef, { totalLiters, totalPrice, lastFlow1, lastFlow2 });
}
const onBtn = document.getElementById("onBtn");
const offBtn = document.getElementById("offBtn");
window.setPump = (state) => {
set(pumpRef, state);
onBtn.classList.toggle('active', state === 'ON');
offBtn.classList.toggle('active', state === 'OFF');
};
function updateUsageDisplay() {
document.getElementById("liveLiters").innerText = totalLiters.toFixed(2);
document.getElementById("livePrice").innerText = totalPrice.toFixed(2);
}
// Listen for flow updates
onValue(flow1Ref, snap => {
const f1 = snap.val() || 0;
document.getElementById("flow1").innerText = (f1 * 1000 / 60).toFixed(1);
document.getElementById("s1Status").classList.toggle('active', f1 > 0);
lastF1 = f1;
checkLeak();
if (f1 > lastFlow1) {
lastFlow1 = f1;
saveData();
}
});
onValue(flow2Ref, snap => {
const f2 = snap.val() || 0;
document.getElementById("flow2").innerText = (f2 * 1000 / 60).toFixed(1);
document.getElementById("s2Status").classList.toggle('active', f2 > 0);
lastF2 = f2;
checkLeak();
if (f2 > lastFlow2) {
const diff = f2 - lastFlow2;
totalLiters += diff;
totalPrice = totalLiters * 0.3; // Adjust pricing logic as needed
lastFlow2 = f2;
saveData();
updateUsageDisplay();
}
});
document.getElementById("resetBtn").addEventListener('click', () => {
totalLiters = 0;
totalPrice = 0;
lastFlow1 = 0;
lastFlow2 = 0;
saveData();
updateUsageDisplay();
});
function checkLeak() {
const leakDot = document.getElementById("leakStatus");
const isNormal = Math.abs(lastF1 - lastF2) < 0.05;
leakDot.classList.toggle('active', isNormal);
leakDot.style.background = isNormal ? '#51cf66' : '#ff6b6b';
}
- In your script.js, update the Firebase configuration values:
- apiKey
- authDomain
- databaseURL
- projectId
- storageBucket
(Use the same details you saved earlier from Firebase.)
- Once everything’s set, launch the website (you can open index.html directly in a browser).
- You’ll now be able to control the pump from your web dashboard, turning it ON or OFF in real time, with data synced instantly through Firebase.
Boom! You’ve successfully built your very own Smart Water Pump Controller.
✅Working Model
- Control From Website
🧩 Final Notes
In this project, the water quantity isn’t measured with perfect accuracy since this is just a prototype model. I haven’t calibrated the flow sensors for precise readings yet.
You’ll also notice that the calculated price and liters don’t reset even if you refresh the page. That’s because the data is stored persistently and can only be reset using the Reset button.
Another thing you might observe is the Leakage Detection indicator turning red. This happens when there’s a mismatch in the flow rate between the two sensors. In a real system, that would indicate a possible leak, but in this small-scale model, it’s mainly due to pressure and height differences in water flow, not an actual leak.
- For future improvements, this system can be expanded to:
- Support multiple houses, each with individual water usage tracking and billing
- Automatically send monthly bills via email
- Or even allow users to log in and check their water usage directly from the dashboard
GitHub Link: https://github.com/yugeshweb/AquaFlow
If you have any questions or suggestions, feel free to drop them in the comments below!
Thanks for reading :)
Top comments (2)
Amazing yugesh !
Thank you :)