How to make an IoT water level monitor

summary of the water IoT water level monitor setup.

In this post, I’ll explain how I made a water level sensor with a JSN-SR04T ultrasonic sensor, an ESP32 microcontroller and a Raspberry Pi. I’ll explain why I made it and how you can make one too.

If you want to jump to the GitHub repository, click here. You can also find a copy of the code at the end of the post.

Why I made a water level monitor

In Mexico City water is usually collected in a water tank at ground level and then pumped to another water tank at the top of the building with a water pump.

The top water tank can sometimes be hard to reach. If there’s a leak or a water shortage, by the time I notice the water tanks may be empty and need immediate attention.

Image with a typical water setup for a house in Mexico City.
Diagram showing a typical setup for a house in Mexico City.

To help with this problem I decided to place a water level sensor in the top water tank and set an alert if the water level falls below 50%.

With this setup I can monitor the tank remotely, check the amount of water I use and get an alert if there’s a problem before it’s an emergency.

How the water level sensor works

The JSN-SR04T is a waterproof ultrasonic sensor placed in the lid of the water tank. It takes measurements every 5 minutes and calculates the distance from the lid to the water level inside the tank. It then transmits that information to an ESP32 microcontroller.

Image of he JSN-SR4T waterproof ultrasonic sensor.
The JSN-SR4T Waterproof ultrasonic sensor.

Using Wi-Fi, the ESP32 then sends the measurement to a Raspberry-Pi computer in the same network using the MQTT protocol.

Image of an ESP32 microcontroller
An ESP32 microcontroller. You can also use an ESP8266.

The Raspberry Pi then calculates the volume of water in the tank and saves it to an Influx DB database.

Image of a Raspberry Pi computer.
A Raspberry Pi computer.

Also in the Raspberry Pi, Grafana, an open-source visualization software is used to read the database and display the information.

Finally, an alarm is set up in Grafana, so if the water level goes below a threshold value, an alarm is triggered, and a Telegram message is sent to alert me of the problem.

Image of a flow diagram explaining how the water level monitor works and the technology it uses.
Flow diagram of the water level monitor.

Components of the water level sensor

To build the water level sensor I used the following:

Hardware:

  • 1 ESP32 microcontroller or 1 ESP8266 microcontroller
  • 1 Raspberry Pi computer
  • 1 JSN-SR04T ultrasonic sensor
  • A power outlet for the microcontroller and sensor
  • Electrical PVC conduit (optional)
  • 3D printed sensor fitting (optional)
Image of a 3D Printed sensor fitting to protect the sensor cable from the elements where it attaches to the tank lid.
3D Printed sensor fitting to protect the sensor cable from the elements where it attaches to the tank lid.

Software:

Image of the setup showing a water tank, the electrical box and the conduit protecting the sensor in the tank lid.
Image of the water level monitor.

I chose the JSN-SR04T sensor because it’s waterproof and can provide an accurate range up to 4 meters. It requires 3V to work so it can be powered directly by the ESP32 microcontroller.

For the setup to work, the ESP32 must be in range of your Wi-Fi network. This way it can send the data from the sensor to the Raspberry-Pi. Also, both the ESP32 and the Raspberry Pi need to be in the same local area network.

You can use either an ESP32 or an ESP8266 microcontroller. Both will work and both are set up in exactly the same way.

How I built the water level sensor

Connections

To set up the sensor I didn’t use any soldering or special connections. I simply connected the wires following the electrical diagram below:

Image of a diagram showing the electrical connections between the JSN-SR04T Ultrasonic sensor and the ESP32 microcontroller.
Electrical diagram showing the connections between the JSN-SR04T Ultrasonic sensor and the ESP32 microcontroller.

Outdoor housing

Since this is an outdoor setup, I placed the cables inside an electrical conduit to protect them from the rain and the sun.

Image of the waterproof electrical box showing an ESP32, part of the ultrasonic sensor and power outlet.
Waterproof electrical box showing an ESP32, and part of the ultrasonic sensor and power outlet.
Image showing the sensor attached to the lid, with a 3D printed sensor fitting (in white) and the sensor cables protected by electrical conduit from the sun and the rain.
Image showing the sensor attached to the lid, with a 3D printed sensor fitting (in white) and the sensor cables protected by electrical conduit from the sun and the rain (green).

You will also need to make sure to place an electrical outlet nearby to power the ESP32 and the sensor.

To protect the ESP32, part of the ultrasonic sensor and the power outlet, I decided to house everything inside a waterproof electrical box attached to a wall.

Inside the electrical box, I fixed the ESP32 and part of the sensor using Din rails to keep it in place.

To make an exact adapter, you can buy Din rail mounts or get the 3D printed files and make your own from here.

To set up the sensor, I then drilled a hole at the top of the water tank. The sensor has silicone pressure fittings that should keep it fixed in place.

I decided to make a 3d printed water tank adapter to make sure the sensor cable is protected from the elements.

Image showing the detail of the water tank lid and the 3D printed fitting to protect the end part of the sensor.
Detail of the water tank lid and the 3D printed fitting to protect the end part of the sensor.

Measurements used

I then took measurements of the water tank to calculate its maximum water volume.

Water tank measurements.

The water tank can be separated into two different shapes: A cylinder and a cone with its top cut off (called a frustum cone).

The formulas to calculate the volume of both shapes are:

Image showing the formula for calculating the volume of a cylinder. We will use it to calculate the volume of the water tank.
Formula for calculating the volume of a cylinder.
Image showing the formula for calculating the volume of a frustum of a cone. We will use it to calculate the volume of the water tank.
Formula for calculating the volume of a frustum of a cone.

Using both formulas, I calculated the volume for the water tank:

Cylinder volume: 998 Liters.

Frustum volume: 119 Liters.

Total volume: 1,117 Liters.

Since I have two identical water tanks connected in the roof, I doubled the amount to get my maximum total water volume.

Reading the water level sensor data

After taking the measurements of the water tank and calculating its volume, I used the sensor to calculate the height from the top of the water tank to the water line and then subtract that height from the frustum and cylinder formulas.

To calculate the distance from the sensor to the water line I used this code and uploaded it to the ESP32.

The code reads the JSN-SR04T sensor data and sends it through MQTT to the Raspberry Pi.

Once uploaded, I opened the Arduino serial monitor to make sure it was working correctly.

After that I connected to my Raspberry Pi and confirmed that the Mosquitto MQTT Broker was receiving the values that the ESP32 was reporting.

Saving the values to a database

After confirming that MQTT was working correctly, I configured Node-RED to read the MQTT data, save it as as JSON object and then write it to a Influx DB database I had previously set up.

Here is an image of the Node-RED setup:

Image of the Node-RED setup in the Raspberry Pi. It reads the water level distance, calculates the volume and writes it into the database.
  • The ‘Water Level’ node reads the MQTT message.
  • The ‘Water Level Calculation’ node calculates the water volume taking into account the the distance to the waterline.
  • The ‘Database Storage’ reads the volume variable every 5 minutes.
  • The ‘Prepare InfluxDB Payload’ node adds the date and time, and saves the output as as a JSON object.
  • The ‘NivelTinaco_Agua’ node saves the information to Influx DB in a database with the same name.
  • Green nodes are used for debugging and testing.

To have a look at the code inside the ‘Water Level Calculation’ and ‘Prepare InfluxDB Payload’ nodes, click here.

The rest of the nodes are configured through the node menu in Node-RED and don’t need any code.

To check that the data was accessible, I then logged into the the Influx DB database using my terminal and took a look at the values:

Image of the Influx DB Database displaying the water level values and timestamp.
Influx DB Database displaying the water level values and timestamp.

Visualizing the data

After making sure that the information is getting written into the influx DB database correctly, I configured Grafana to visualize the data.

Grafana is a great tool for data visualization because its open source, easy to use, and produces great dynamic charts.

I made a dashboard with a Gauge reading for the current water level and also a time series to display the water values over a 24-hour period.

Image of a gauge chart to display the current amount of water in the tank.
Gauge to display the current amount of water in the tank.
Image showing a time series chart showing the water tank level for several days. It also shows the minimum, maximum and average tank level for the period.
Time series showing the water tank level for several days. It also shows the minimum, maximum and average tank level for the period.

In the graph you can see how the water level falls periodically (morning showers, washing clothes at midday) and is gradually restored over time.

Water levels don’t seem to fall below the 1,000L mark, but an alarm is set to send a telegram message if the water level falls below this threshold value.

How I use the sensor data

The sensor data is useful because it checks the water level every 5 minutes and sends me an alarm if the level is too low.

It lets me know if there’s a problem, which gives me time to fix it with enough water available for essential tasks.

It is also a useful sensor because it allows me to check my daily water consumption and how that trend changes over time.

Measuring is the first step towards efficiency and a more sustainable future.

More resources

I hope you found this post interesting. Building the water sensor was lots of fun and I found it a very useful gadget for my home.

Feel free to contact me if you decide to build one and you need any help.

Pablo.

You can browse other projects I’ve done here.

I also made IoT Energy Monitor. You can find the post about that here.

Here is a link to my GitHub profile.

Finally, here are some additional resources that I used to make this project possible:

This video has a good overview of the different ultrasonic sensors available in the market.

Andreas Spiess is a superb teacher and goes over alternatives to measure fluid levels.
This tutorial goes over how to setup a weather station using the same tech stack I used in this post.

Code

Back to top:

Arduino code uploaded to the ESP32:

// Code to measure water level in a water tank

// Mills Function 
const unsigned long event_interval = 300000; // 5 minute interval, 300 second
//const unsigned long event_interval = 2000; // 2 second interval. For testing
unsigned long previous_time = 0; 

// MQTT Library
#include "EspMQTTClient.h" 

#define WIFI_SSID        "<Wifi_Username_here>"                          // WiFi Username
#define WIFI_PASS        "<Wifi_Password_here>"                          // Wifi Password

#define BROKER_IP        "<MQTT_ip_here>"                                // IP adress of MQTT broker
#define BROKER_USERNAME  "<broker_username_here>"                        // Broker username
#define BROKER_PASSWORD  "<broker_password_here>"                        // Broker password
#define CLIENT_NAME      "<device_name_here>"                            // MQTT client name to identify the device
#define BROKER_PORT      <mqtt_port_here>                                // MQTT Port. No "" needed
#define lastwill_topic   "<lastwill_topic_here>"                         // MQTT topic to report lastwill and testament.
#define lastwill_text    "<lastwill_message_here>"                       // MQTT memssage to report lastwill and testament.

String  client_name       = CLIENT_NAME;                                 // MQTT Topic to report initial value
String  startup_topic     = "<startup_topic_here>";                      // MQTT Topic to report startup
String  water_level_topic = "<reporting_values_topic_here>";             // MQTT topic to report values

// Function to connect to MQTT 
EspMQTTClient client(
                  WIFI_SSID,
                  WIFI_PASS,
                  BROKER_IP,
                  BROKER_USERNAME,
                  BROKER_PASSWORD,
                  CLIENT_NAME,
                  BROKER_PORT
                  );

// Water Sensor pins
#define TRIG 14 //GPIO Number 14, D5
#define ECHO 12 //GPIO Number 12, D6

void setup() { 
  
  Serial.begin(115200); // Serial monitoring 
  
  // Enable  debugging messages sent to serial output
  client.enableDebuggingMessages(); 

  // Enable the web updater.
  client.enableHTTPWebUpdater();     
  
  // MQTT Last Will & Testament
  client.enableLastWillMessage( lastwill_topic , lastwill_text);
  
  // Water level sensor Pin Setup
  pinMode(TRIG, OUTPUT);       // Initializing Trigger Output
  pinMode(ECHO, INPUT_PULLUP); // Initializing Echo Input
  } 

// MQTT Innitial Connection
void onConnectionEstablished() {
  client.publish(startup_topic, String(client_name + " is now online."));
  }
  
  void loop() { 

    // MQTT Loop: Must be called once per loop.
    client.loop(); 

    // Mills Loop
    unsigned long current_time = millis();

    // Mills if Statement
    if(current_time - previous_time >= event_interval) {
      // Set the trigger pin to low for 2uS 
      digitalWrite(TRIG, LOW); 
      delayMicroseconds(2); 

      // Send a 20uS high to trigger ranging
      digitalWrite(TRIG, HIGH);  
      delayMicroseconds(20); 

      // Send pin low again
      digitalWrite(TRIG, LOW);  

      // Read pulse times
      int distance = pulseIn(ECHO, HIGH,26000);  

      //Convert the pulse duration to distance
      distance= distance/58; 

      //Print Result in serial monitor
      Serial.print("Distance ");
      Serial.print(distance);
      Serial.println("cm");

      // MQTT Client Publisher
      client.publish(water_level_topic, String(distance));

      // Mills Update timing for next time
      previous_time = current_time;
    }
    
}

Back to top:

Node-RED Code used:

// Code to calculate the available water in the water tank.
// Used in node: 'Water Level Calculation'

// Get sensor distance in meters
msg.payload = Number(msg.payload)/100;
sensor_distance = msg.payload;

// Constants
var pi = 3.141592;
var Liters = 0;
var VolumeFullCylinder = 998;

// Function to calculate water in Frustum
function FrustumVolume(x) {
    height = 0.35 - x
    FrustumWaterVolume = (pi/3)*height*(0.55**2 + 0.275**2 + 0.55*0.275)
    return FrustumWaterVolume*1000
  }

// Function to calculate water in Cylinder
function CylinderVolume(x) {
    height = 1.05 - x + 0.35
    CylinderWaterVolume = pi * 0.55**2 * height
    return CylinderWaterVolume*1000
  }

// Test to check total water volume
if (sensor_distance < 0.20) {
    Message = "Error: Value is too low. Check Sensor";
    Liters = 0;
    msg.payload = Liters

  } else if (sensor_distance >= 0.20 && sensor_distance <= 0.35) {
    Message = "Water is in Frustum.";
    Liters = FrustumVolume(sensor_distance)+ VolumeFullCylinder;
    Liters = Math.round(Liters*2) // Multiply x 2 because there are 2 water tanks
    msg.payload = Liters

  } else if (sensor_distance > 0.35 && sensor_distance <= 1.40) {
    Message = "Water is in the cylinder section.";
    Liters = CylinderVolume(sensor_distance);
    Liters = Math.round(Liters*2) // Multiply x 2 because there are 2 water tanks
    msg.payload = Liters

  } else {
    Message = "Error: Value is to high. Check Sensor";
    Liters = 9999;
    msg.payload = Liters
  }

// Prepare node to send information
msg.payload = Number(msg.payload); 
flow.set("water_in_tank",msg.payload);
flow.set("water_level_sensor","ESP32");
return {payload: msg.payload};
// Code to prepare water level data to save it into InfluxDB
// Used in node: 'Prepare InfluxDB Payload'

var today = new Date();

var date = today.getFullYear()+
            '-'+ (today.getMonth()+1)+
            '-'+ today.getDate() + 
            ' ' + today.getHours() + 
            ":" + today.getMinutes() + 
            ":" + today.getSeconds();

msg.payload = { 
    Timestamp:date, 
    Device:flow.get("water_level_sensor"), 
    Water_Level_In_Tank:flow.get("water_in_tank"),
}

return msg;

Back to top


Leave a Reply

Your email address will not be published. Required fields are marked *