Postlad/Guides/ESP32 water level monitoring system with a cloud chart

ESP32 water level monitoring system with a cloud chart

An ESP32 water level monitoring system measures the air gap above the water, converts that distance to a percentage, and sends it to a chart you can check remotely. This build uses a waterproof ultrasonic probe and reports distance, water depth and fill percentage together.

Ultrasonic water tank measurementA waterproof ultrasonic probe measures the air gap from the tank lid to the water surface; calibration converts it to depth and fill percent.air gapwater depthlidtank bottom
Measure the empty and full distances on your own tank. The percentage comes from those two calibration points, not a generic tank height.

It monitors the tank only. It does not switch a pump. Keeping sensing separate from mains-voltage control makes the first version safer and much easier to trust.

Reviewed, not yet bench-tested on a real tank. Ultrasonic modules sold under the same name can have different minimum ranges and timing behaviour.

Parts

  • ESP32 development board
  • Waterproof ultrasonic distance sensor such as JSN-SR04T
  • 1 kΩ and 2 kΩ resistors for the Echo voltage divider
  • Stable 5 V supply
  • Weather-resistant enclosure for the electronics
  • Cable glands and mounting hardware
  • Free Postlad account and write key

Do not put a normal HC-SR04 inside a damp tank lid and call it waterproof. Its exposed transducers corrode. A waterproof probe keeps the sensing face in the humid space while the controller board stays dry.

Wiring

The common JSN-SR04T arrangement uses 5 V power and produces a 5 V Echo signal. ESP32 inputs are 3.3 V devices, so Echo needs a divider.

SensorESP32 / divider
VCC5V/VIN
GNDGND
TRIGGPIO5
ECHO1 kΩ resistor → GPIO18
GPIO182 kΩ resistor → GND

The 1 kΩ/2 kΩ divider reduces a 5 V Echo signal to about 3.3 V. Confirm the module's actual output with a meter rather than assuming every clone is identical.

Mount the probe pointing straight down. It must remain above the highest possible water level and outside the sensor's blind zone. Keep it away from inlet spray, ladders, pipes and walls that can create a nearer echo.

Measure the two calibration distances

You need two numbers measured from the sensor face:

  • DISTANCE_EMPTY_CM: distance to the water or tank bottom at the lowest meaningful level
  • DISTANCE_FULL_CM: distance to the water at the highest safe level

For example, if the face is 120 cm above the empty reference and 25 cm above the full reference:

cpp
const float DISTANCE_EMPTY_CM = 120.0;
const float DISTANCE_FULL_CM = 25.0;

The usable depth is 95 cm. A measured gap of 72.5 cm is halfway between those references, so the reported level is 50%.

Complete ESP32 sketch

Replace WiFi, key and calibration constants before uploading.

cpp
#include <WiFi.h>
#include <HTTPClient.h>

const char* WIFI_SSID     = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* WRITE_KEY     = "YOUR_KEY";

const char* POSTLAD_URL = "http://api.postlad.com/u";
const unsigned long SEND_INTERVAL_MS = 60000;

const int TRIG_PIN = 5;
const int ECHO_PIN = 18;

const float DISTANCE_EMPTY_CM = 120.0;
const float DISTANCE_FULL_CM = 25.0;

float clampValue(float value, float low, float high) {
  if (value < low) return low;
  if (value > high) return high;
  return value;
}

void connectWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.print("Connecting to WiFi");

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

  Serial.print("\nConnected. IP: ");
  Serial.println(WiFi.localIP());
}

float measureDistanceCm() {
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(3);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);

  unsigned long duration = pulseIn(ECHO_PIN, HIGH, 30000);
  if (duration == 0) return NAN;

  return duration * 0.0343 / 2.0;
}

float medianOfFive() {
  float values[5];

  for (int i = 0; i < 5; i++) {
    values[i] = measureDistanceCm();
    if (isnan(values[i])) return NAN;
    delay(80);
  }

  for (int i = 0; i < 4; i++) {
    for (int j = i + 1; j < 5; j++) {
      if (values[j] < values[i]) {
        float swap = values[i];
        values[i] = values[j];
        values[j] = swap;
      }
    }
  }

  return values[2];
}

void setup() {
  Serial.begin(115200);
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
  digitalWrite(TRIG_PIN, LOW);
  connectWiFi();
}

void loop() {
  float distance = medianOfFive();

  if (isnan(distance)) {
    Serial.println("No ultrasonic echo; reading not sent");
    delay(SEND_INTERVAL_MS);
    return;
  }

  float usableDepth = DISTANCE_EMPTY_CM - DISTANCE_FULL_CM;
  float waterDepth = DISTANCE_EMPTY_CM - distance;
  float percent = waterDepth * 100.0 / usableDepth;

  waterDepth = clampValue(waterDepth, 0.0, usableDepth);
  percent = clampValue(percent, 0.0, 100.0);

  if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
  }

  WiFiClient client;
  HTTPClient http;
  String url = String(POSTLAD_URL)
             + "?k=" + WRITE_KEY
             + "&f1=" + String(distance, 1)
             + "&f2=" + String(waterDepth, 1)
             + "&f3=" + String(percent, 1);

  if (http.begin(client, url)) {
    int status = http.GET();
    String body = http.getString();
    Serial.printf(
      "gap %.1f cm, depth %.1f cm, %.1f%% -> HTTP %d %s\n",
      distance,
      waterDepth,
      percent,
      status,
      body.c_str()
    );
    http.end();
  }

  delay(SEND_INTERVAL_MS);
}

The median of five measurements rejects an occasional near or far echo without smoothing away a real level change.

Configure the stream

FieldNameUnit
f1Air gapcm
f2Water depthcm
f3Tank level%

Keep the raw air-gap measurement. If the percentage ever looks wrong, that field tells you whether the sensor moved, the calibration changed or the conversion code is wrong.

Tank shape and litres

Percentage of depth is not always percentage of volume.

  • A vertical rectangular tank has a linear depth-to-volume relationship.
  • A vertical cylinder also has a linear relationship if its cross-section is constant.
  • A horizontal cylinder does not. Half the depth happens to be half the volume, but other depths require circular-segment geometry.
  • Irregular underground tanks need a calibration table.

Do not label f3 as volume unless the geometry supports it. A convincing wrong unit is worse than a correct percentage.

Detect useful conditions from the chart

The time series can answer more than “how full is it?”:

  • A steady downward slope estimates consumption.
  • A sudden fall can indicate a leak or outlet event.
  • A rising line confirms refill rate.
  • A flat line during expected use may mean the sensor is stuck.
  • Repeated spikes often point to splashing, condensation or a competing echo.

Send one reading per minute at first. Tanks normally change slowly, and a slower interval makes noise easier to recognise.

Installation notes

Keep the ESP32 and sensor controller board out of the wet space. Use cable glands with downward drip loops. Condensation travels along cables and defeats an enclosure that looked sealed on the bench.

Ultrasonic sensing can fail over foam, heavy condensation, angled liquid surfaces and narrow tanks. For a critical installation, compare it with a pressure sensor or float switch and define what the system should do when there is no valid echo.

Troubleshooting

The reading is always the minimum distance

The probe may be seeing the tank lid, a brace or the edge of its mounting hole. Widen the opening or move the sensor away from nearby structures.

The ESP32 reports impossible values or resets

Disconnect Echo and verify the voltage divider. A 5 V input can damage the ESP32. Also use a supply that can handle WiFi current peaks.

The graph jumps when the pump runs

Electrical noise, vibration, ripples and inlet spray can all contribute. Power the pump separately, share ground only where the design requires it, add physical distance from the inlet, and keep the median filter.

The percentage is backwards

DISTANCE_EMPTY_CM must be larger than DISTANCE_FULL_CM. The air gap shrinks as the tank fills.

Why not control the pump in this guide?

Pump control adds mains isolation, dry-run protection, overflow protection, relay failure modes and local fail-safe behaviour. A remote dashboard must never be the only thing preventing a flood.

FAQ

Can ESP32 measure water level in real time?

Yes, with ultrasonic, pressure, capacitive or float-based sensing. “Real time” for a tank normally means a fresh reading every minute, not millisecond sampling.

Which sensor is best for an outdoor water tank?

A waterproof ultrasonic probe is a practical non-contact starting point, but condensation and geometry matter. Pressure sensors are often better in awkward tanks and need their own waterproofing and calibration.

Can I calculate litres from an ultrasonic reading?

Yes when the tank dimensions and shape are known. Use a linear calculation for constant cross-section tanks and the correct volume formula or calibration table for other shapes.

What happens when WiFi goes down?

The monitor should keep measuring and visibly leave a chart gap, or buffer timestamped readings locally for later batch upload. It should not invent data or silently report that a failed send succeeded.

How long is the water-level history kept?

On Postlad Free, full-detail readings are kept for 30 days, then hourly minimum, maximum and average are kept to 12 months. Older data is deleted.

Create the stream

Create a free account and use one of the two Free streams for the tank. A public or secret link lets other people check the level without receiving the write key.

Create your free account →

Sources and verification

  • Sensor electrical levels and minimum range must come from the exact purchased module's datasheet, not a reseller title.
  • ESP32 pin tolerances and board power arrangement must be checked against the exact development board used in the build.