Water tank level with automatic switch to city water


I have a rain water tank under my terrace. The only access is a trap door I have to lift (heavy) to check the level of the water inside. I have a water pump connected to a water circuit inside the house to feed the toilets, the washing machine and some taps. When the rain water level is too low, the pump is no more primed.

I can feed this water circuit with city water by switching some valves.

Last summer, I had to redo completely the terrace (concrete slab and tiles) so I used this opportunity to add a conduit from the tank to the house, with the idea to add a water level sensor of some sort.

At the end of the summer, I had an ultrasonic sensor installed in the tank (out of the water, measuring the distance between to top and the water surface), wired (phone cable with 2 pairs of copper wires) to an Arduino. I 3D printed a box for the Arduino to attach it next to the washing machine. On the Arduino, a touch screen is placed to display the water level and some button to control a relay activating solenoid valves to switch off the rain water and opening the city water when the level is too low and reverse when the water level is at 50% or more.

I did all the programming and improved the code when issues poped up. The system worked great for a moment but now, it is unusable.

My current problem is: I have A LOT of false reading on the ultrasonic sensor. I see two possible causes, but you may come with more, I am sure :wink:

  1. Because of the length of the wired connection from the sensor to the Arduino, the signal receive a lot of interference. I plan to fix this possible issue by changing the phone cable by a CAT8 network cable (plenty of shielding) from the sensor to the Arduino.
  2. The sensor is dead (or defect) because of the humidity in the tank. I am trying to find a reliable sensor (doesn't need to be ultrasonic) to put in the tank.

Can you, please, give me some advise and recommendation to make my project usable again ?

I can share some pictures, material list, code if requested.


PS: I am native French speaker, don't hold a grudge on me if I make stupid mistakes in English :wink:

To narrow down the propositions, here are some technical information.

The cable length between the tank and the Arduino is 15m. It goes under the concrete slab into an air vent then, attach to the wall, inside the house to the laundry room.
The water tank has total height of 1,5m and the maximum water level arrives at 23cm from the top.
I currently use an HC-SR04 sensor in the tank.
I already bought a JSN-SR04T-2 sensor, but the tests outside the tank aren’t promising at all.

The system worked great for a moment but now, it is unusable.

How does it fail? False readings or no readings from the water level sensor? By the way, attach the code using code tags, the symbol up to the left in this window.

I don't understand trying to "automate" a task that can easily be accomplished with a float valve.

If the tank is being filled by rainwater, the float will be high, closing the valve. If not, the float will sink, opening the valve, letting city water in.

I'd use a float switch, from a boat bilge pump, attached to the side of the tank, at the level I want the water pump to engage.

When the water level is above the desired level, the float switch will try to float up and generate a signal that all is well. When the float switch is dropped below or at activation level, the pump energizes, water enters tank, and so on and so forth.

A bilge pump float switch is designed to survive in a harsh environment. If sticking to ultrasonic, get a water proof one.

but now, it is unusable.

I fail to see why you need anything more than this
Arduino is surely better employed elsewhere - unless you have some code for making rain.

Reading Your post again, it's false readings. Can You tell more? Constantly too low, too high, dancing around…..?

First of, the main issue is false reading (values equal to 0 or outside the possible range of values). I ended up programming a counting mechanism switching to city water after 500 false reading in a row (readings are made every 5 seconds)

Ok, my home setup is maybe not that clear. The only way to fill the tank is with the rain water collected from one half of my house roof. From that tank, a pipes enters the home and is connected to the pump. When the rain water is used in the house, the pump tries to keep 2,5bar of pressure in the house pipes. When there is not enough rain water in the tank, the pump is no more primed and I no more have water in that circuit. To solve that problem, I can, manually, switch off the rain water valve and switch on the city water valve. This is done after the pump. This switch does not fill the tank, it just serve city water to the house.

My automation system is to avoid to de-prime the pump when the rain water level is too low by switching to city water at about 10-15% capacity and to switch back to rain water when the tank is at least 50% filled.

Before the work on the terrace, I had no way to add any type of sensors. I tried wireless ones but the armed concrete structure in the terrace and the house walls was blocking the signal after only 2m. Now that I have a conduit going out of the tank and into the house, I can use wired sensors.

Here is the full code. It is receiving the signal from the ultrasonic sensor, controlling the touch screen and sending signals to the relay. The relay has two states: in the first state, the rain water solenoid valve is open and the city water solenoid valve is closed. In the other relay state, the status of the solenoid valves are reverse. The valves only use electricity when changing state. And the system is setup to be on city water when the relay is not powered. I can switch off the solenoid valves with a manual switch to turn the solenoid valves manually.

REM1: Some text in French but it should not be an issue to understand the code.
REM2: I copy / paste some code and did not cleaned, yet, everything I am not using

PART1 (9000 characters limit)

#include <Elegoo_GFX.h>    // Core graphics library
#include <Elegoo_TFTLCD.h> // Hardware-specific library
#include <TouchScreen.h>

#define DEV_MODE false

// Accessories pin settings
#define RELAY       26
#define ECHO_PIN    28
#define TRIGGER_PIN 29

// Electrovalve switching time
#define SWITCHING_DELAY 45000
// Measured distance at full tank
#define FULL_TANK  20.0
// Measured distance at empty tank
#define EMPTY_TANK 150.0
// Needed percentage to automatically switch from rain water to city water
// Needed percentage to automatically switch from city water to rain water
// Safety count for bad_following_reading to switch to city water

// Status messages
#define NO_MESSAGE ""
#define INITIALISATION "Initialisation"
#define MEASURE_ERROR "Erreur de mesures"
#define CHANGE_TO_CITY "Changement en cours Pluie -> Ville"
#define CHANGE_TO_RAIN   "Changement en cours Ville -> Pluie"
#define NO_CAPACITY_READ "Capacité non détectée"

// Buttons name
char buttonlabels[3][6] = {"Ville", "Pluie", "Fixe"};

// The control pins for the LCD can be assigned to any digital or
// analog pins...but we'll use the analog pins as this allows us to
// double up the pins with the touch screen (see the TFT paint example).
#define LCD_CS A3 // Chip Select goes to Analog 3
#define LCD_CD A2 // Command/Data goes to Analog 2
#define LCD_WR A1 // LCD Write goes to Analog 1
#define LCD_RD A0 // LCD Read goes to Analog 0

#define LCD_RESET A4 // Can alternately just connect to Arduino's reset pin

// For the Arduino Mega, use digital pins 22 through 29
// (on the 2-row header at the end of the board).

// Assign human-readable names to some common 16-bit color values:
#define BLACK   0x0000
#define BLUE    0x001F
#define RED     0xF800
#define GREEN   0x07E0
#define CYAN    0x07FF
#define MAGENTA 0xF81F
#define YELLOW  0xFFE0
#define WHITE   0xFFFF

// Color definitions
#define ILI9341_BLACK       0x0000      /*   0,   0,   0 */
#define ILI9341_NAVY        0x000F      /*   0,   0, 128 */
#define ILI9341_DARKGREEN   0x03E0      /*   0, 128,   0 */
#define ILI9341_DARKCYAN    0x03EF      /*   0, 128, 128 */
#define ILI9341_MAROON      0x7800      /* 128,   0,   0 */
#define ILI9341_PURPLE      0x780F      /* 128,   0, 128 */
#define ILI9341_OLIVE       0x7BE0      /* 128, 128,   0 */
#define ILI9341_LIGHTGREY   0xC618      /* 192, 192, 192 */
#define ILI9341_DARKGREY    0x7BEF      /* 128, 128, 128 */
#define ILI9341_BLUE        0x001F      /*   0,   0, 255 */
#define ILI9341_GREEN       0x07E0      /*   0, 255,   0 */
#define ILI9341_CYAN        0x07FF      /*   0, 255, 255 */
#define ILI9341_RED         0xF800      /* 255,   0,   0 */
#define ILI9341_MAGENTA     0xF81F      /* 255,   0, 255 */
#define ILI9341_YELLOW      0xFFE0      /* 255, 255,   0 */
#define ILI9341_WHITE       0xFFFF      /* 255, 255, 255 */
#define ILI9341_ORANGE      0xFD20      /* 255, 165,   0 */
#define ILI9341_GREENYELLOW 0xAFE5      /* 173, 255,  47 */
#define ILI9341_PINK        0xF81F

/******************* UI details */
#define BUTTON_X 55
#define BUTTON_Y 150
#define BUTTON_W 80
#define BUTTON_H 30

// text box where numbers go
#define TEXT_X 10
#define TEXT_Y 10
#define TEXT_W 220
#define TEXT_H 50
#define TEXT_TSIZE 3
char *textfield = (char*)malloc(10 * sizeof(char));

#define YP A3  // must be an analog pin, use "An" notation!
#define XM A2  // must be an analog pin, use "An" notation!
#define YM 9   // can be a digital pin
#define XP 8   // can be a digital pin

//Touch For New ILI9341 TP
#define TS_MINX 120
#define TS_MAXX 900

#define TS_MINY 70
#define TS_MAXY 920
// We have a status line
#define STATUS_X 10
#define STATUS_Y 65

// We have a measure line
#define MEASURE_X 10
#define MEASURE_Y 300

// Touch screen boundaries
#define MINPRESSURE 10
#define MAXPRESSURE 1000

TouchScreen ts = TouchScreen(XP, YP, XM, YM, 300);
// If using the shield, all control and data lines are fixed, and
// a simpler declaration can optionally be used:
// Elegoo_TFTLCD tft;

Elegoo_GFX_Button buttons[3];
/* create 3 buttons, in classic candybar phone style */
uint16_t buttoncolors[3] = {ILI9341_WHITE, ILI9341_BLUE, ILI9341_BLACK};
uint16_t buttontextcolors[3] = {ILI9341_BLUE, ILI9341_WHITE, ILI9341_RED};

// Sonar setup
#define MAX_DISTANCE 200
uint16_t cycle = 0;
uint16_t measure;
char *measureString = (char*)malloc(100 * sizeof(char));

// Program variables
bool isFixed = false;
bool isRainWater = false;
int bad_following_reading = 0;

// DEV variables
int dev_measure = 0;
void setup(void) {
  uint16_t identifier = tft.readID();
  identifier = detectLCD(identifier);

  // create buttons
  // Ville row 0 col 0
  setButton(0, 0, 0, 0, ILI9341_BLUE, ILI9341_WHITE);
  // Pluie row 0 col 1
  setButton(1, 0, 1, 0, ILI9341_BLACK, ILI9341_WHITE);

  // Fixe row 1 col 1
  setButton(2, 1, 0, 65, ILI9341_BLACK, ILI9341_RED);

  pinMode(RELAY, OUTPUT);
  digitalWrite(RELAY, LOW);

  // create 'text field'
  tft.drawRect(TEXT_X, TEXT_Y, TEXT_W, TEXT_H, ILI9341_WHITE);

// Add button
void setButton(int index, int row, int col, int offset, uint16_t buttoncolor, uint16_t buttontextcolor) {
  int x = BUTTON_X+col *(BUTTON_W+BUTTON_SPACING_X) + offset;
  buttons[index].initButton(&tft,     // x, y, w, h, outline, fill, text
                  BUTTON_W, BUTTON_H, ILI9341_WHITE, buttoncolor, buttontextcolor,
                  buttonlabels[index], BUTTON_TEXTSIZE); 

void loop(void) {

  // Sonar

  // Touch screen
  digitalWrite(13, HIGH);
  TSPoint p = ts.getPoint();
  digitalWrite(13, LOW);

  pinMode(XM, OUTPUT);
  pinMode(YP, OUTPUT);

   if (p.z > MINPRESSURE && p.z < MAXPRESSURE) {
    // scale from 0->1023 to tft.width
    p.x = map(p.x, TS_MINX, TS_MAXX, tft.width(), 0);
    p.y = (tft.height()-map(p.y, TS_MINY, TS_MAXY, tft.height(), 0));
  // go thru all the buttons, checking if they were pressed
  for (uint8_t b=0; b<3; b++) {
    if (buttons[b].contains(p.x, p.y)) {
      //Serial.print("Pressing: "); Serial.println(b);
      buttons[b].press(true);  // tell the button it is pressed
    } else {
      buttons[b].press(false);  // tell the button it is NOT pressed

  // now we can ask the buttons if their state has changed
  for (uint8_t b=0; b<3; b++) {
    if (buttons[b].justReleased()) {
      // Serial.print("Released: "); Serial.println(b);
      buttons[b].drawButton();  // draw normal
    if (buttons[b].justPressed()) {
        // City button!
        if (b == 0) {
          Serial.println(F("City pressed"));
        // Rain button!
        if (b == 1) {
          Serial.println(F("Rain pressed"));
        // Fixe button!
        if (b == 2) {
      delay(100); // UI debouncing


// Display LCD Driver type
uint16_t detectLCD(uint16_t identifier) {
  Serial.println(F("TFT LCD test"));
  #ifdef USE_Elegoo_SHIELD_PINOUT
  Serial.println(F("Using Elegoo 2.8\" TFT Arduino Shield Pinout"));
  Serial.println(F("Using Elegoo 2.8\" TFT Breakout Board Pinout"));

  Serial.print("TFT size is "); Serial.print(tft.width()); Serial.print("x"); Serial.println(tft.height());

  if(identifier == 0x9325) {
    Serial.println(F("Found ILI9325 LCD driver"));
  } else if(identifier == 0x9328) {
    Serial.println(F("Found ILI9328 LCD driver"));
  } else if(identifier == 0x4535) {
    Serial.println(F("Found LGDP4535 LCD driver"));
  }else if(identifier == 0x7575) {
    Serial.println(F("Found HX8347G LCD driver"));
  } else if(identifier == 0x9341) {
    Serial.println(F("Found ILI9341 LCD driver"));
  } else if(identifier == 0x8357) {
    Serial.println(F("Found HX8357D LCD driver"));
  } else if(identifier==0x0101)
       Serial.println(F("Found 0x9341 LCD driver"));
  }else {
    Serial.print(F("Unknown LCD driver chip: "));
    Serial.println(identifier, HEX);
    Serial.println(F("If using the Elegoo 2.8\" TFT Arduino shield, the line:"));
    Serial.println(F("  #define USE_Elegoo_SHIELD_PINOUT"));
    Serial.println(F("should appear in the library header (Elegoo_TFT.h)."));
    Serial.println(F("If using the breakout board, it should NOT be #defined!"));
    Serial.println(F("Also if using the breakout, double-check that all wiring"));
    Serial.println(F("matches the tutorial."));
  return identifier;

void status(const char *msg) {
  tft.fillRect(STATUS_X, STATUS_Y, 240, 8, ILI9341_BLACK);
  tft.setCursor(STATUS_X, STATUS_Y);

void sonarMeasure() {
  if (cycle == 0) {

    if(DEV_MODE) {
      dev_measure = (dev_measure + 10) % (int) EMPTY_TANK;
      measure = dev_measure;
    } else {
      digitalWrite(TRIGGER_PIN, LOW);

      digitalWrite(TRIGGER_PIN, HIGH);

      digitalWrite(TRIGGER_PIN, LOW);
      int distance = pulseIn(ECHO_PIN, HIGH, 26000);

      measure = distance / 58;

      if (measure < FULL_TANK || measure > EMPTY_TANK) {
        if (bad_following_reading == (int) BAD_FOLLOWING_BAD_READING_SAFETY_COUNT) {
      } else {
        bad_following_reading = 0;

    sprintf(measureString, "Measure: %dcm, %d bad reading(s)", measure, bad_following_reading);
  cycle = (cycle+1) % 10000;  

void capacity(int measure) {
  if ((measure >= 1 && measure <= FULL_TANK) || 
      (measure > EMPTY_TANK)) {
  } else if (measure > FULL_TANK) {
    int waterHeight = EMPTY_TANK - measure;
    Serial.print(F("Water height: "));

    int capacity = (waterHeight / (EMPTY_TANK - FULL_TANK)) *100;
    Serial.print(F("Computed capacity: "));

    // Automatic switch
    if (!isFixed) {

void displayMeasure(const char *measure) {
  tft.fillRect(MEASURE_X, MEASURE_Y, 240, 8, ILI9341_BLACK);
  tft.setCursor(MEASURE_X, MEASURE_Y);

void displayCapacity(int capacity) {
    // Clean the current message
    tft.setCursor(TEXT_X + 10, TEXT_Y+10);
    tft.setTextColor(TEXT_TCOLOR, ILI9341_BLACK);
    tft.print("           ");

    // Display the new capacity
    sprintf(textfield, "Capa.: %d%%", capacity);
    tft.setCursor(TEXT_X + 10, TEXT_Y+10);
    tft.setTextColor(TEXT_TCOLOR, ILI9341_BLACK);

void executeSwitching(const char *msg) {

void automaticSwitchEvaluation(int capacity) {
  if (isRainWater && capacity <= AUTOMATIC_SWITCH_TO_CITY_WATER) {
    Serial.println(F("Automatic switch to city water"));
  } else if (!isRainWater && capacity >= AUTOMATIC_SWITCH_TO_RAIN_WATER) {
    Serial.println(F("Automatic switch to rain water"));

void cityButtonPressed() {
  if (isRainWater) {
    setButton(0, 0, 0, 0, ILI9341_BLUE, ILI9341_WHITE);
    setButton(1, 0, 1, 0, ILI9341_BLACK, ILI9341_WHITE);

    digitalWrite(RELAY, LOW);
    isRainWater = false;

void rainButtonPressed() {
  if (!isRainWater) {
    setButton(0, 0, 0, 0, ILI9341_BLACK, ILI9341_WHITE);
    setButton(1, 0, 1, 0, ILI9341_BLUE, ILI9341_WHITE);

    digitalWrite(RELAY, HIGH);
    isRainWater = true;

void fixeButtonPressed() {
  Serial.println(F("Fixe pressed"));
  if (isFixed) {
    setButton(2, 1, 0, 65, ILI9341_BLACK, ILI9341_RED);
  } else {
    setButton(2, 1, 0, 65, ILI9341_RED, ILI9341_BLACK);
  isFixed = !isFixed;

I fail to see why you need anything more than this
Arduino is surely better employed elsewhere - unless you have some code for making rain.

I don't want to trigger the pump itself. It does it when the pressure of the output pipe is lower than 2,5bar.

What I want is:

  • be able to "see" how much rain water is in the tank
  • automate the switching from rain water to city water (and reverse) based on the rain water tank level

If a similar type of sensor can be connected to an arduino, please show it to me.

Something is wrong with the level sensor, or likely, the wiring. Reading either low or very high values make me think that the cable is broken somewhere and You have an intermittent contact.
Check the continuity of the cables to and from the sensor.
All You need is an on/off telling switch signalling whether the level is above or below 50%. A float and a micro switch would do fine I think, and 2 wires to the controller.

The cable length between the tank and the Arduino is 15m.

That is quite a long distance to receive reliable data from an Ultrasonic sensor as the Arduino is relying on precise pulse timings to determine distance. The Arduino needs to be as close to the ultrasonic sensor as possible.

As such you may be better off with 2 micro's. One at the tank reading the ultrasonic sensor pulse responses and one to then show you the results and/or actuate something at the other end of the 15m cable. Then you may need to use something like RS485 as a bridge between the two micros. This will require a RS485 to UART conversion etc. using some additional hardware.

The system was working but started to malfunction after some time.
I would check the cable and the connections at the controller.

The system was working but started to malfunction after some time.
I would check the cable and the connections at the controller.

Thanks for the advice. I will remove the current sensor from the tank to check it is still functioning correctly and I will replace the cable anyway. I bought that cat8 cable for this only usage.

More info after my checks.

I know you are not in the United States, but can you answer a couple of questions about your system. I tried collecting roof rain runoff into barrels a few years ago. I had to give up because of all trash and dirt that got washed into the barrels. How do you keep your water clean?

Second question. Does your commercial water supply allow you to connect to your own water storage without a back-flow preventer? That stops your rain water from flowing into the city water pipes if the city water pipe looses pressure and your system opens it's valve to allow city water into your system.

Thanks, Paul


The rain water isn't clean here either, but I don't pump it at the very bottom. I have a floating device that keeps the intake between the surface and the bottom without ever touching them. There is also a filter at the pump "in case of".

My house installation has a back-flow preventer at the entrance of the city water. The flow can never leave my house back to the city pipe. I also have back-flow preventers on each side of the "rain water circuit" in the house. I never mix the city water and the rain water elsewhere than where I accept rain water to be used.

This type of installation is no more approved here, but It was setup by a plumber 10 years ago (when it was still legal) and I only added the automation of the switch part.

Thanks! Good to know I wasn't the only one to have the problem and you have a good solution.


I had to give up because of all trash and dirt that got washed into the barrels. How do you keep your water clean?

Common local practice in Australia, and also.

Second question. Does your commercial water supply allow you to connect to your own water storage without a back-flow preventer?

Yours probably doesn't, so one solution.

Yes, please check the sensor if You have that opportunity. Make sure that the cable is not damaged, that some pet, rabbit, …… has not bitten the cable. One rabbit of my doughter cut the mains, 230 volt, got a big bang in the face and the nose was burned black.... He survived….
Any place were the cable might get moved, stepped on.... could be a riscy place. Check the entrance to the controller....

Byt the way, 10 - 15%, not 50%...…..