Image

Smart Water Tank Monitoring System: Revolutionizing Household Water Management

From Daily Drudgery to Automated Bliss – A Computer Engineer's

In the bustling streets of Kathmandu, Nepal, where water supply can be as unpredictable as the Himalayan weather, managing household water tanks often turns into a daily hassle. Picture this: rushing to check tank levels before the morning shower, worrying about overflows during monsoons, or dealing with empty tanks during dry spells. For many families, including mine, this "water rush" was a constant source of stress. But what if technology could handle it all? As a computer engineer with a passion for robotics, I, Sagar Luitel, decided to build my own smart water tank monitoring system – a game-changer that automates everything from level detection to pump control. Let's dive into the story behind this project and how it transformed my home into a smarter, stress-free haven.

The Spark: Turning Household Headaches into Innovation

My journey into this project started during my bachelor's in computer engineering at Madan Bhandari Memorial Academy Nepal, where I served as president of the Robotic Club. Leading robotics workshops and competitions honed my skills in embedding intelligence into everyday machines – from line-following robots to automated arms. But it wasn't until I returned home that I saw the real-world application staring me in the face: our family's water tanks.

Like many Nepali households, we rely on overhead and underground tanks to store water from intermittent municipal supplies. Manually checking levels meant climbing ladders, peering into dark tanks, or worse – dealing with floods from forgotten pumps. During peak "rush hours," like early mornings or evenings, forgetting to switch off the pump could waste precious water or cause overflows. As a tech enthusiast, I thought, "Why not automate this?" Drawing from my robotics background, I envisioned an IoT system that monitors levels in real-time, controls the pump intelligently, and sends alerts via a mobile app. This wasn't just a hobby project; it was a practical fix for my own home, blending my engineering expertise with the need to simplify daily life.

The Challenge: Battling Unreliable Water Supplies in Nepal

Water management in Nepal isn't just about convenience – it's a necessity. With frequent power cuts, erratic water deliveries, and the risk of contamination from overfilled tanks, households often face inefficiencies. Traditional methods like float valves are prone to failure, and manual oversight leads to wasted time and resources. My goal was to create a reliable, cost-effective system that:

  • Detects water levels accurately without invasive modifications.
  • Prevents overflows and dry runs automatically.
  • Allows remote monitoring and control via smartphone.
  • Incorporates safety features like timeouts and emergency overrides.

SEO-optimized tip: If you're searching for "DIY water tank level monitoring with ESP32" or "IoT pump automation for home," this project hits the mark, using affordable hardware to solve common "home water management problems in developing countries."

Building the Solution: A Blend of Hardware and Smart Code

At the heart of this system is the ESP32 microcontroller – a powerhouse for IoT projects known for its WiFi capabilities and low power consumption. I paired it with ultrasonic sensors for non-contact level measurement in both the main (underground) and secondary (overhead) tanks. Conductive probes add an extra layer of accuracy, detecting low and high water states to trigger actions reliably.

The pump control uses a relay module, ensuring it only activates when needed – like when the overhead tank dips low and the underground one has sufficient water. To make it user-friendly, I integrated Blynk, a popular IoT platform, for a sleek app interface. Users can view tank percentages, toggle manual mode, and receive notifications for critical events like "tank empty" or "tank full."

Key features include:

  • Real-Time Monitoring: Ultrasonic readings calculate percentages with median filtering for stability, updating every few seconds.
  • Auto and Manual Modes: Switch between hands-free automation and override control, with safety checks to prevent mishaps.
  • Debounce and Timeout Safeguards: Probes are debounced to avoid false triggers, and the pump times out after 35 minutes to prevent endless running.
  • App Integration: Visual gauges for tank levels, pump status indicators, and push alerts for peace of mind.

From prototyping on a breadboard to a polished enclosure, the build took about two weeks of tweaking. My robotics club experience was invaluable here – teaching me to iterate on sensor accuracy and code efficiency, much like debugging a competition bot.

The Impact: Efficiency, Savings, and Scalability

Since deploying this system in my household, the "daily rush" is history. No more wasted water from overflows, no dry taps during busy times, and remote checks mean I can monitor from anywhere – perfect for working professionals. It's eco-friendly too, conserving water in a country where shortages are common. Cost-wise, the entire setup was under $50, using readily available components like ESP32 boards and HC-SR04 sensors.

For fellow makers searching for "ESP32 Blynk water level project" or "automated water pump controller Nepal," this is a blueprint for customization. Future upgrades? Integrating solar power for off-grid reliability or AI for predictive maintenance based on usage patterns.

Wrapping Up: Empowering Homes with Tech

What started as a personal fix for household woes evolved into a testament to how engineering can solve real-life problems. As president of my college robotic club, I learned that innovation thrives at the intersection of technology and necessity. This smart water tank system isn't just gadgets and code – it's about reclaiming time, reducing stress, and making homes smarter. If you're inspired to build your own "IoT home automation project," start small, iterate, and watch the magic unfold.

Have you faced similar water management challenges? Share your thoughts in the comments, or reach out for tips on replicating this setup. Let's make everyday life a bit more automated! 1768696863-diagram.png

Source Code of Project:

======== // IMPORTANT CREDENTIALS - REPLACE THESE WITH YOUR OWN VALUES // =========

// Blynk credentials - Get these from your Blynk project dashboard
#define BLYNK_TEMPLATE_ID "YOUR_TEMPLATE_ID_HERE" // e.g. "TMPLxxxxxx"
#define BLYNK_TEMPLATE_NAME "YOUR_TEMPLATE_NAME_HERE" // e.g. "TankStatus"
#define BLYNK_AUTH_TOKEN "YOUR_AUTH_TOKEN_HERE" // 32-character token

// WiFi credentials - Change to your network
 #define WIFI_SSID "YOUR_WIFI_SSID_HERE" // e.g. "HomeNetwork"
#define WIFI_PASS "YOUR_WIFI_PASSWORD_HERE" // e.g. "MySecretPass123"

#define MAIN_TANK_HEIGHT     120
#define MAIN_FULL_DISTANCE   20
#define SEC_TANK_HEIGHT      115
#define SEC_FULL_DISTANCE    20

#define PUMP_TIMEOUT         2100000UL  // 35 minutes
#define PROBE_DEBOUNCE_TIME  4000UL
#define LEVEL_CHANGE_THRESHOLD 5
#define LEVEL_MAX_UPDATE_INTERVAL 300000UL  // 5 min

#define NUM_READINGS         5

using namespace ace_button;

#define TRIGPIN_MAIN    27
#define ECHOPIN_MAIN    26
#define TRIGPIN_SEC     25
#define ECHOPIN_SEC     33

#define MAIN_LOW_PROBE  4
#define MAIN_HIGH_PROBE 5
#define SEC_LOW_PROBE   19
#define SEC_HIGH_PROBE  23

#define RELAY_PIN       13
#define OVERRIDE_BUTTON 12
#define MODE_BUTTON     14
#define ONBOARD_LED     2

char auth[] = BLYNK_AUTH_TOKEN;

BlynkTimer timer;
Preferences preferences;

// Global variables
int mainTankPercent = -1;
int secTankPercent = -1;
int lastSentMainPercent = -999;
int lastSentSecPercent = -999;

bool pumpOn = false;
bool lastSentPumpState = !pumpOn;
bool isManualMode = false;
bool lastSentMode = !isManualMode;

unsigned long pumpStartTime = 0;

// Probe debounce
bool mainLowStable = false;
bool mainHighStable = false;
bool secLowStable = false;
bool secHighStable = false;

unsigned long mainLowChangeTime = 0;
unsigned long mainHighChangeTime = 0;
unsigned long secLowChangeTime = 0;
unsigned long secHighChangeTime = 0;

unsigned long lastLevelForceSend = 0;

// Notification tracking
bool prevMainEmpty = false;
bool prevMainFull = false;
bool prevSecLow = false;
bool prevSecFull = false;

ButtonConfig configOverride, configMode;
AceButton buttonOverride(&configOverride);
AceButton buttonMode(&configMode);

void updateLED() {
  digitalWrite(ONBOARD_LED, Blynk.connected() ? HIGH : (millis() % 1000 < 500 ? HIGH : LOW));
}

void checkConnection() {
  updateLED();

  if (WiFi.status() != WL_CONNECTED) {
    if (isManualMode) {
      isManualMode = false;
      preferences.putBool("manualMode", isManualMode);
    }
    Blynk.virtualWrite(V3, 0);
  }

  sendShortStatus();
}

float readUltrasonic(int trig, int echo) {
  float readings[NUM_READINGS];
  int valid = 0;
  for (int i = 0; i < NUM_READINGS; i++) {
    digitalWrite(trig, LOW);
    delayMicroseconds(2);
    digitalWrite(trig, HIGH);
    delayMicroseconds(10);
    digitalWrite(trig, LOW);
    long duration = pulseIn(echo, HIGH, 25000);
    float dist = duration > 0 ? duration * 0.0343 / 2 : -1;
    if (dist > 0 && dist < 1000) readings[valid++] = dist;
    delay(30);
  }
  if (valid == 0) return -1;
  // Sort for median
  for (int i = 0; i < valid - 1; i++)
    for (int j = 0; j < valid - i - 1; j++)
      if (readings[j] > readings[j + 1]) std::swap(readings[j], readings[j + 1]);
  return readings[valid / 2];
}

void updateLevelsAndPump() {
  float mainDist = readUltrasonic(TRIGPIN_MAIN, ECHOPIN_MAIN);
  float secDist = readUltrasonic(TRIGPIN_SEC, ECHOPIN_SEC);

  int newMainPercent = (mainDist < MAIN_FULL_DISTANCE) ? 100 :
                       (mainDist >= MAIN_FULL_DISTANCE && mainDist <= MAIN_TANK_HEIGHT) ?
                       map((int)mainDist, MAIN_TANK_HEIGHT, MAIN_FULL_DISTANCE, 0, 100) : -1;

  int newSecPercent = (secDist < SEC_FULL_DISTANCE) ? 100 :
                      (secDist >= SEC_FULL_DISTANCE && secDist <= SEC_TANK_HEIGHT) ?
                      map((int)secDist, SEC_TANK_HEIGHT, SEC_FULL_DISTANCE, 0, 100) : -1;

  mainTankPercent = newMainPercent;
  secTankPercent = newSecPercent;

  unsigned long now = millis();

  if (abs(mainTankPercent - lastSentMainPercent) >= LEVEL_CHANGE_THRESHOLD ||
      (now - lastLevelForceSend > LEVEL_MAX_UPDATE_INTERVAL)) {
    Blynk.virtualWrite(V0, mainTankPercent);
    lastSentMainPercent = mainTankPercent;
  }

  if (abs(secTankPercent - lastSentSecPercent) >= LEVEL_CHANGE_THRESHOLD ||
      (now - lastLevelForceSend > LEVEL_MAX_UPDATE_INTERVAL)) {
    Blynk.virtualWrite(V1, secTankPercent);
    lastSentSecPercent = secTankPercent;
    lastLevelForceSend = now;
  }

  bool mainLowRaw = (digitalRead(MAIN_LOW_PROBE) == LOW);
  bool mainHighRaw = (digitalRead(MAIN_HIGH_PROBE) == LOW);
  bool secLowRaw  = (digitalRead(SEC_LOW_PROBE) == LOW);
  bool secHighRaw = (digitalRead(SEC_HIGH_PROBE) == LOW);

  // Debounce logic
  if (mainLowRaw != mainLowStable) {
    if (now - mainLowChangeTime > PROBE_DEBOUNCE_TIME) mainLowStable = mainLowRaw;
  } else mainLowChangeTime = now;

  if (mainHighRaw != mainHighStable) {
    if (now - mainHighChangeTime > PROBE_DEBOUNCE_TIME) mainHighStable = mainHighRaw;
  } else mainHighChangeTime = now;

  if (secLowRaw != secLowStable) {
    if (now - secLowChangeTime > PROBE_DEBOUNCE_TIME) secLowStable = secLowRaw;
  } else secLowChangeTime = now;

  if (secHighRaw != secHighStable) {
    if (now - secHighChangeTime > 15000UL) {  // Longer debounce for sec high
      secHighStable = secHighRaw;
    }
  } else secHighChangeTime = now;

  bool mainLowConnected = mainLowStable;
  bool mainHighConnected = mainHighStable;
  bool secLowDisconnected = !secLowStable;
  bool secHighConnected = secHighStable;

  // States for notifications
  bool mainEmpty = !mainLowConnected;
  bool mainFull = mainHighConnected;
  bool secLow = secLowDisconnected;
  bool secFull = secHighConnected;

  // Send notifications only on state change (using logEvent)
  if (Blynk.connected()) {
    if (mainEmpty && !prevMainEmpty) {
      Blynk.logEvent("main_tank_empty", "Main Tank is Empty!");
    }
    if (mainFull && !prevMainFull) {
      Blynk.logEvent("main_tank_full", "Main Tank is Full");
    }
    if (secLow && !prevSecLow) {
      Blynk.logEvent("sec_tank_low", "Secondary Tank is Low");
    }
    if (secFull && !prevSecFull) {
      Blynk.logEvent("sec_tank_full", "Secondary Tank is Full");
    }
  }

  prevMainEmpty = mainEmpty;
  prevMainFull = mainFull;
  prevSecLow = secLow;
  prevSecFull = secFull;

  // Safety: Force off if secondary full or main empty
  if (secHighConnected || !mainLowConnected) {
    if (pumpOn) {
      digitalWrite(RELAY_PIN, HIGH);
      pumpOn = false;
      if (pumpOn != lastSentPumpState) {
        Blynk.virtualWrite(V2, 0);
        lastSentPumpState = false;
      }
      Serial.println("SAFETY: Pump FORCED OFF");
    }
    return;
  }

  // Timeout check
  if (pumpOn && (millis() - pumpStartTime > PUMP_TIMEOUT)) {
    digitalWrite(RELAY_PIN, HIGH);
    pumpOn = false;
    if (pumpOn != lastSentPumpState) {
      Blynk.virtualWrite(V2, 0);
      lastSentPumpState = false;
    }
    Serial.println("TIMEOUT (35 min): Pump OFF");
    return;
  }

  // Auto mode control
  if (!isManualMode) {
    if (secLowDisconnected && mainLowConnected && !pumpOn) {
      digitalWrite(RELAY_PIN, LOW);
      pumpOn = true;
      pumpStartTime = millis();
      if (pumpOn != lastSentPumpState) {
        Blynk.virtualWrite(V2, 1);
        lastSentPumpState = true;
      }
      Serial.println("AUTO: Pump ON");
    }
  }

  if (isManualMode != lastSentMode) {
    Blynk.virtualWrite(V3, isManualMode);
    lastSentMode = isManualMode;

    Blynk.virtualWrite(V0, mainTankPercent);
    Blynk.virtualWrite(V1, secTankPercent);
    lastSentMainPercent = mainTankPercent;
    lastSentSecPercent = secTankPercent;
    lastLevelForceSend = now;
  }

  // Debugging print
  Serial.printf("Sec High Raw: %d | Stable: %d | Pump: %s\n", secHighRaw, secHighStable, pumpOn ? "ON" : "OFF");
}

void sendShortStatus() {
  Serial.printf("Main: %s%% | Sec: %s%% | Pump: %s | Mode: %s | WiFi: %s\n",
                mainTankPercent >= 0 ? String(mainTankPercent).c_str() : "N/A",
                secTankPercent >= 0 ? String(secTankPercent).c_str() : "N/A",
                pumpOn ? "ON" : "OFF",
                isManualMode ? "Manual" : "Auto",
                WiFi.status() == WL_CONNECTED ? "OK" : "Lost");
}

void hardwareCheck() {
  Serial.println("Hardware Test...");
  float mainDist = readUltrasonic(TRIGPIN_MAIN, ECHOPIN_MAIN);
  float secDist = readUltrasonic(TRIGPIN_SEC, ECHOPIN_SEC);
  Serial.printf("US Main: %s | US Sec: %s\n",
                mainDist > 0 ? String(mainDist, 1).c_str() : "FAIL",
                secDist > 0 ? String(secDist, 1).c_str() : "FAIL");
  Serial.printf("Probes - Main Low: %s | Main High: %s | Sec Low: %s | Sec High: %s\n",
                digitalRead(MAIN_LOW_PROBE) == LOW ? "Connected" : "Open",
                digitalRead(MAIN_HIGH_PROBE) == LOW ? "Connected" : "Open",
                digitalRead(SEC_LOW_PROBE) == LOW ? "Connected" : "Open",
                digitalRead(SEC_HIGH_PROBE) == LOW ? "Connected" : "Open");

}

BLYNK_WRITE(V2) {
  if (isManualMode && WiFi.status() == WL_CONNECTED) {
    bool secHighFull = (digitalRead(SEC_HIGH_PROBE) == LOW);
    bool mainHasWater = (digitalRead(MAIN_LOW_PROBE) == LOW);
    if (secHighFull || !mainHasWater) {
      Blynk.virtualWrite(V2, 0);
      Serial.println("Manual blocked - Safety");
      return;
    }
    pumpOn = param.asInt();
    digitalWrite(RELAY_PIN, pumpOn ? LOW : HIGH);
    if (pumpOn) pumpStartTime = millis();
  } else {
    Blynk.virtualWrite(V2, pumpOn);
  }
}

BLYNK_WRITE(V3) {
  if (WiFi.status() == WL_CONNECTED) {
    isManualMode = param.asInt();
    preferences.putBool("manualMode", isManualMode);
  } else {
    Blynk.virtualWrite(V3, 0);
  }
}

BLYNK_CONNECTED() {
  Blynk.syncVirtual(V2, V3);
  digitalWrite(ONBOARD_LED, HIGH);
}

void overrideHandler(AceButton*, uint8_t eventType, uint8_t) {
  if (eventType == AceButton::kEventReleased && isManualMode) {
    bool secHighFull = (digitalRead(SEC_HIGH_PROBE) == LOW);
    bool mainHasWater = (digitalRead(MAIN_LOW_PROBE) == LOW);
    if (secHighFull || !mainHasWater) {
      Serial.println("Button blocked - Safety");
      return;
    }
    pumpOn = !pumpOn;
    digitalWrite(RELAY_PIN, pumpOn ? LOW : HIGH);
    if (pumpOn) pumpStartTime = millis();
    Blynk.virtualWrite(V2, pumpOn);
  }
}

void modeHandler(AceButton*, uint8_t eventType, uint8_t) {
  if (eventType == AceButton::kEventReleased) {
    if (WiFi.status() == WL_CONNECTED) {
      isManualMode = !isManualMode;
      preferences.putBool("manualMode", isManualMode);
      Blynk.virtualWrite(V3, isManualMode);

      Blynk.virtualWrite(V0, mainTankPercent);
      Blynk.virtualWrite(V1, secTankPercent);
      lastSentMainPercent = mainTankPercent;
      lastSentSecPercent = secTankPercent;
      lastLevelForceSend = millis();
    }
  }
}

void setup() {
  pinMode(RELAY_PIN, OUTPUT);
  digitalWrite(RELAY_PIN, HIGH);

  Serial.begin(115200);

  pinMode(TRIGPIN_MAIN, OUTPUT);
  pinMode(ECHOPIN_MAIN, INPUT);
  pinMode(TRIGPIN_SEC, OUTPUT);
  pinMode(ECHOPIN_SEC, INPUT);
  pinMode(MAIN_LOW_PROBE, INPUT_PULLUP);
  pinMode(MAIN_HIGH_PROBE, INPUT_PULLUP);
  pinMode(SEC_LOW_PROBE, INPUT_PULLUP);
  pinMode(SEC_HIGH_PROBE, INPUT_PULLUP);
  pinMode(ONBOARD_LED, OUTPUT);
  pinMode(OVERRIDE_BUTTON, INPUT_PULLUP);
  pinMode(MODE_BUTTON, INPUT_PULLUP);

  configOverride.setEventHandler(overrideHandler);
  configMode.setEventHandler(modeHandler);
  buttonOverride.init(OVERRIDE_BUTTON);
  buttonMode.init(MODE_BUTTON);

  preferences.begin("tankCtrl", false);
  isManualMode = preferences.getBool("manualMode", false);

  hardwareCheck();

  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  esp_wifi_set_ps(WIFI_PS_NONE);

  unsigned long start = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - start < 20000) {
    delay(500);
    Serial.print(".");
  }

  if (WiFi.status() != WL_CONNECTED) {
    Serial.println("\nWiFi Failed - Running in AUTO mode");
  } else {
    Serial.println("\nWiFi Connected!");
    Serial.print("IP: "); Serial.println(WiFi.localIP());
  }

  Blynk.config(auth);

  timer.setInterval(8000L, updateLevelsAndPump);
  timer.setInterval(2000L, checkConnection);

  lastLevelForceSend = millis();
}

void loop() {
  Blynk.run();
  timer.run();
  buttonOverride.check();
  buttonMode.check();


} 1768696109-photo_6296250601301544419_y.jpg 1768695981-photo_6296250601301544417_y.jpg



Leave A Comment