Project tutorial
Water Leakage Detector and Valve Control

Water Leakage Detector and Valve Control © GPL3+

This system monitors places where water could leak and turn the main water valve if any water leakage is detected.

  • 19,158 views
  • 7 comments
  • 39 respects

Components and supplies

About this project

Overview

During a chat with a friend, I realized water leakages are a big problem. My friend had to replace all the furniture in his basement, just because a pipe had broken while he was at work.

This project is similar to the one I published for the Microsoft IoT contest but this one is based on Arduino, not on Raspberry. My point of view is the following: instead of centralizing many responsibilities on a big platform (like RasPi, DragonBoard or a PC), I prefer to delegate simple responsibilities to simple devices (like Arduino and others). They will do what they are supposed to do and, as an option, they will connect over the network to other (simple or complex) devices to offer advanced services. In case of network failure, they still continue to do what they are supposed to do.

Basically, it monitors water leakages and it triggers actions like closing the main water entry, depending on the fault detected.

It also publishes messages to an MQTT Broker. The idea is that the device has to manager water locally but is also involved, along with other devices, in a bigger system that manages Home Automation.

Here is what it looks like:

Main water entry is located on the bottom. The first device is installed by city water supply services to control water pressure. I installed the part on the top of the picture. The motorized valve (in blue) is installed in parallel with a manual valve. In this drawing, the manual valve is opened and the motorized valve is therefore bypassed. It is useful in case of power outage. In normal mode, the manual valve must be turned off.

There is a DC motor (12v) within the valve and it turns clockwise or counter-clockwise depending on polarity. There is a feed-back loop that indicates whether the valve is effectively On or Off. To turn it On, just apply a positive voltage to the upper-left connection.

Here is the controller:

From left to right: the AC plug, a reset button, some LEDs to display statuses, connectors (to sensors, to motor), an Ethernet and a USB interface.

  • LED1: Steady Red = water detected locally, Blinking Red = water detected remotely, Off = no water leakage
  • LED 2: Steady Yellow = unable to control motorized valve, Blinking Yellow = unable to reach MQTT broker, Off = all good
  • LED3: Steady Blue = all good, Blinking Blue= motorized valve is closed Off = system is down or not powered

Here is a sensor, located where I think water could leak:

Here is the what's under the hood:

WARNING!

The AC/DC power supply I'm using has two outputs: the first one is 12V DC and is used to feed the motorized valve (controlled by two relays that controls motor rotation) and the second one is EXACTLY 5V DC, to feed the Arduino. That's why I'm feeding directly on the 5V circuit, not the Vin that requires at least 6V DC. Once the AC/DC is connected, you should NEVER ( I said NEVER) plug neither the Arduino DC jack nor Arduino USB cable. If you still want to debug over USB, setup a home-made cable WITHOUT the power lines, keep just the data lines. By the way, the link between AC housing and AC/DC power supply is 110V. Do not touch, ever!

Code

Water leakage detection and motorized valve controlArduino
#include <PubSubClient.h>
#include <Network.h>
#include <Timer.h>
#include <SPI.h>
#include <Ethernet.h>
#include <Dns.h>
#include <avr/wdt.h>

/* Here is how the HW works

Therea are three subsystems:
- the main box :
    - an Arduino Uno with and Ethernet shield
    - a red LED    : steady when water is detected localy
                     blinking when water is detected remotely ,                     
                     off otherwise
    - a yellow LED : steady when valve is out of order, 
	                   blinking when MQTT broker is unreachable (for any reason),
					           off otherwise
    - a blue LED  :  steady when valve is open and system is monitoring leakages, 	
                     blinking when valve has been turned off                    
					           off is system is down
    - a push button : once pressed, a self-test is triggered 
    - a dual relay to control a remote motorized valve
    - another dual relay to sense the open/close limit switches installed on the remote valve
- a set of water detectors (all in parrallel) (all 3 connectors on the fron panel are connected in parralel)

The motorized valve has the following connections:
- Yellow and Blue : DC to power the motor
- Black : limit switches input (will be set to GND in our circuit)
- Red = will turn to GND when the valve hits its fully closed position (note: due to the internal design of the limit switch, there is no guarantee that continuity will remain once the valve if powerd off)
- Green = will turn to GND when the valve hits its fully open position (note: due to the internal design of the limit switch, there is no guarantee that continuity will remain once the valve if powerd off)

*/
// Network
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xCF, 0xFC, 0xEE };      // Arduino's MAC address
IPAddress ip(192, 168, 12, 215);                          // Arduino's IP address
IPAddress server(192, 168, 12, 130);                      // MQTT broker's address
EthernetClient ethClient;

// MQTT 
PubSubClient client(ethClient); 
#define mqttClientPrefix "GLX"                            // Prefix to use any MQTT publication/subscription 
#define mqttClientLocation "BASEMENT"                     // Second part of the client identifier
#define mqttClientUID "001"                               // Last part of the client identifier
#define mqttClientStatusTopic "Status"                    // Topic to be used to publish device status 
#define mqttClientFaultTopic "Fault"                      // Topic to be used to publish/subscribe to Faults
const int mqttInterval = 20;                              // determines how often the system will report to MQTT broker (ie. every mqttInterval * mainLoopDelay ms )
int mqttIntervalCnt = 0;                                  // local variable used to count down
int isConnectedToBroker = -1;                             // 1 when connected, -1 = unknown, 0 = unable to connected

// Pin-out
const int SystemLedPin = A0;                              // Blue led 
const int FaultLedPin = A1;                               // Yellow led 
const int AlarmLedPin = A2;                               // Red led 
const int WaterDetectorPin = 2;                           // goes to LOW when water is detected, otherwise pulled-up to VCC
const int ToggleButtonPin = 3;                            // goes to LOW when someone press on the button and then goes to HIGH when button is released, otherwise pull-down to GND
const int SdCardPin = 4;                                  // SD card on ethernet shield, not used
const int ValveClosedPin = 5;                             // goes to LOW when the motor hits the closed switch limit, otherwise pull-up to HIGH
const int ValveOpenedPin = 6;                             // goes to LOW when the motor hits the open switch limit, otherwise pull-up to HIGH
const int ValveControl1 = 8;                              // to control the first relay that controls the motorized valve power supply
const int ValveControl2 = 7;                              // to control the second relay that controls the motorized valve power supply
                                                          // Note do not use D10, D11, D12 and D13 since those pins are reserved for Ethernet shield
// WaterLeakage (local)                                                           
int isWaterDetected = 0;                                  // status as per the last good reading

// WaterLeakage (remote)
int isWaterDetectedRemotely = 0;                          // status as per messages received from other monitoring devices

// Motorized valve
int isValveClosed = -1;                                   // status of motorized valve (-1 = unknown, 0 = opened, 1 = closed))
const int valveTimeOut = 15;                              // in sec, max time allowed to open or close valve
int isConnectedToValve = -1;                              // 0 when system cannot control motorized valve, 1 = connected, -1 = unknown

// Manual RESET button
volatile boolean isResetRequested = 0;                    // this one will change when button triggers an interrupt

// Logic
const int mainLoopDelay = 500;                            // a fixed delay within main loop, in ms
void(* resetFunc) (void) = 0;

// Initialization 
void setup()
{
  wdt_disable(); //always good to disable it, if it was left 'on' or you need init time
  Serial.begin(9600);
  Serial.println(F("Begin of setup"));   
  // HW setup
  pinMode (SystemLedPin, OUTPUT);
  pinMode (FaultLedPin, OUTPUT);
  pinMode (AlarmLedPin, OUTPUT);
  pinMode (WaterDetectorPin, INPUT);
  pinMode (ToggleButtonPin, INPUT);
  pinMode (ValveOpenedPin, INPUT);  //  12V DC relay is idle by default. Pin is connected to NO side of the relay 1 but there is a pull-up. Pin is therefore HIGH by default. 
  pinMode (ValveClosedPin, INPUT);  //  12V DC relay is idle by default. Pin is connected to NO side of the relay 2 but there is a pull-up. Pin is therefore HIGH by default. 
  pinMode (ValveControl1, OUTPUT);
  digitalWrite(ValveControl1, HIGH);  // 5V DC relay 1 is idle by default, i.e motor is connected to GND
  pinMode (ValveControl2, OUTPUT);   
  digitalWrite(ValveControl2, HIGH);  // 5V DC relay 2 idle by default, i.e motor is connected to GND
  pinMode(SdCardPin, OUTPUT);
  digitalWrite(SdCardPin, HIGH); // to disable SD card since we do not use it
  // Self test
  testLeds(); 
  // Network and MQTT setup
  client.setServer(server, 1883);
  client.setCallback(MQTTBrokerCallback);
  Ethernet.begin(mac, ip);
  Serial.print(F("Current IP is : "));
  Serial.print(Ethernet.localIP());  
  Serial.print(F(" - MQTT broker IP is : "));
  Serial.println(server);  
  // Initialy, we don't know valve's status and limit switches are not so reliable. 
  // Let's open motorized valve and wait for completion. Worst case, if it is already opened, it will just hit the limit switch briefly  
  if (openValve() == 0) 
  {    
    Serial.println(F("Valve is open and system is now monitoring"));      
    // There are other monitoring devices in the house, let's listen to the faults they could report to the MQTT broker
    subscribeToRemoteWaterSensors();    
  }   
  else
  { 
     Serial.println(F("Unable to open valve, system is out of order. Please use plumbing bypass"));   
  };   
  enableInterruptOnResetButton();   
  delay(1500);  // allow hardware to sort itself out  
  Serial.println(F("End of setup"));       
}

// Main loop
void loop()
{  
  // LEDs
  configureLedsWithInitialStates(); 
  // React to reset request
  if (isResetRequested == 1)
  {
     Serial.println(F("Someone pushed on the button to reset this device")); 
     publishStatus();  
     wdt_enable(WDTO_1S); //enable watchdog, will fire in 1 second   
     delay(5000); 
     Serial.println(F("this message should never appear")); 
  }
  // Let's check now whether any water leakage has been detected
  readLocalWaterSensor();   
  if (isWaterDetected == 1 || isWaterDetectedRemotely == 1) 
  {
     if (isValveClosed == 0){ closeValve();}; 
  }   
  // Publish to MQTT broker
  if (mqttIntervalCnt == 0)
  { 
      if (isWaterDetected == 1){ publishFault();}
      publishStatus();        
      mqttIntervalCnt = mqttInterval;
  }    
  else
  { 
      if (isConnectedToValve == 0) 
      {
        Serial.println(F("System is out of order - unable to control motorized valve. No monitoring in place"));
      }
      else
      { 
        Serial.print(F("."));
      }  
      mqttIntervalCnt = mqttIntervalCnt - 1;      
  }       
  // Take some rest
  delay(mainLoopDelay / 2 ); 
  client.loop();  
  // LEDs
  configureLedsWithFinalStates(); 
  delay(mainLoopDelay / 2);    
}

//
// Local water sensor management
//
void readLocalWaterSensor()
{         
     isWaterDetected = !getDebouncedValue(WaterDetectorPin, 100, 10);      
     Serial.print(isWaterDetected); 
}

//
// Reset button management
//
void enableInterruptOnResetButton()
{
    isResetRequested = 0;
    attachInterrupt(1, onResetRequested, CHANGE);
}
void onResetRequested()
{
    detachInterrupt(1);
    isResetRequested = 1;   
}

// Manage valve opening sequence
int openValve()
{  
  Serial.print(F("Opening valve..."));
  // first, confirm the valve was closed by forcing the motor to hit briefly the "closed" limit switch again (since those limit switches are not so reliable...)
  setupRelays(1);
  if (waitForEndOfCycle(ValveClosedPin) == 0)  
  { 
     // now, let's try to open the valve
     setupRelays(2);  
     if (waitForEndOfCycle(ValveOpenedPin) == 0)
     {
        isConnectedToValve = 1;
        isValveClosed = 0;         
        setupRelays(0);  // power relays OFF 
        Serial.println(F(""));
        return 0;
     }
  }  
  setupRelays(0);  // power relays OFF 
  isConnectedToValve = 0;
  return -1;
}

// Manage the valve closing sequence
int closeValve()
{  
  Serial.print(F("Closing valve..."));
  // first, confirm the valve was open by forcing the motor to hit briefly the "open" limit switch again (since those limit switches are not so reliable...)
  setupRelays(2);
  if ( waitForEndOfCycle(ValveOpenedPin) == 0)  
  { 
     // now, let's try to close the valve
     setupRelays(1);  
     if (waitForEndOfCycle(ValveClosedPin) == 0)
     {
        isConnectedToValve = 1; 
        isValveClosed = 1;
        setupRelays(0);  // power relays OFF 
        Serial.println(F("Valve has been turned off. Please inspect carefully all rooms and cleanup detectors"));    
        return 0;
     }
  }  
  setupRelays(0);  // power relays OFF 
  isConnectedToValve = 0;
  return -1;
}

// Setup the relays in order to feed the motor with the right polarity
void setupRelays(int scenario)
{
  switch (scenario)
  {
     case 0:  // all OFF, no power sent the motorized valve
        digitalWrite(ValveControl1, HIGH);   
        digitalWrite(ValveControl2, HIGH);          
        break;   
     case 1:  // closing cycle
        digitalWrite(ValveControl1, HIGH);
        digitalWrite(ValveControl2, LOW);      
        break;   
     case 2:  // opening cycle
        digitalWrite(ValveControl1, LOW);
        digitalWrite(ValveControl2, HIGH);
        break;   
     default:         
        Serial.print(F("Unexpected relay scenario: "));
        Serial.println(scenario);
        digitalWrite(ValveControl1, HIGH);   
        digitalWrite(ValveControl2, HIGH);  
        break;        
  }
}

// Wait until the limit switch is hit by motorized valve's motor 
int waitForEndOfCycle(int limitSwitchPin)
{
   int cnt = valveTimeOut;
   while (cnt > 0)
   {
      if (getDebouncedValue(limitSwitchPin, 10, 10) == LOW) 
      {           
         return 0;
      } 
      cnt = cnt - 1;
      Serial.print(F("."));
      delay(1000);
   };  
   Serial.println(F(" - timeout reached while closing valve. Check whether valve is well powered up and cables are connected."));
   return -1;
}

// This routine helps to avoid false alarms
int getDebouncedValue(int inputPin, int intervalInMs, int requiredConfirmations)
{
    int confirmations = 1;
    int currentValue = digitalRead(inputPin);
    while (confirmations <= requiredConfirmations)
    {      
      delay(intervalInMs);
      if (currentValue == digitalRead(inputPin))
      {
         confirmations = confirmations + 1;
      }
      else
      {       
        confirmations = 1;
        currentValue = digitalRead(inputPin);
      }
    }  
    return currentValue;
}

//
// LEDs management
//
void configureLedsWithInitialStates()
{
  clearLeds();
  // Re-evaluate
  if (isWaterDetectedRemotely == 1 || isWaterDetected == 1) { digitalWrite(AlarmLedPin, HIGH);};
  if (isConnectedToValve == 0 || isConnectedToBroker == 0) { digitalWrite(FaultLedPin, HIGH);}; 
  digitalWrite(SystemLedPin, HIGH);
}
void configureLedsWithFinalStates()
{  
  if (isWaterDetectedRemotely == 1) { digitalWrite(AlarmLedPin, LOW);};      
  if (isConnectedToBroker == 0) { digitalWrite(FaultLedPin, LOW);};
  if (isValveClosed == 1) { digitalWrite(SystemLedPin, LOW);};   
}
void clearLeds()
{
  digitalWrite(AlarmLedPin, LOW);
  digitalWrite(FaultLedPin, LOW);
  digitalWrite(SystemLedPin, LOW);
}
void testLeds()
{
  clearLeds();
  digitalWrite(AlarmLedPin, HIGH);
  delay(500);
  digitalWrite(FaultLedPin, HIGH);
  delay(500);
  digitalWrite(SystemLedPin, HIGH);
  delay(500);
  clearLeds();
}

//
// MQTT related functions
//

// Handle incoming MQTT messages
void MQTTBrokerCallback(char* subscribedTopic, byte* payload, unsigned int length)
{
  Serial.print(F("New message received from MQTT broker. Topic="));
  Serial.print(subscribedTopic);
  String payloadAsString = (char*)payload;
  String realPayload = payloadAsString.substring(0,length);  // otherwise we get garbage since the buffer is shared between In and Out
  Serial.print(F(", content="));
  Serial.print(realPayload);
  if (realPayload.indexOf("WaterDetected") > 0 && realPayload.indexOf(mqttClientLocation) == -1 )  // the second part of the test is required to avoid self-triggered faults  
  {   
     isWaterDetectedRemotely = 1;
  }
  // for (int i=0;i<length;i++) {
  //   Serial.print((char)payload[i]);
  // }
  Serial.println();  
}

// Build the client identifier
String buildClientIdentifier()
{
  String data = mqttClientPrefix;
  data+="_";
  data+= mqttClientLocation;
  data+="_";
  data+= mqttClientUID; 
  return data; 
}

// Build the topic name to be used to publish status
String buildDeviceStatusTopic()
{
  String data = mqttClientPrefix;
  data+="/";
  data+=mqttClientLocation;
  data+="/";
  data+=mqttClientUID; 
  data+="/";
  data+=mqttClientStatusTopic;  
  return data; 
}

// Build the topic name to be used to publish/subscribe to Faults 
String buildFaultTopic()
{
  String data = mqttClientPrefix;
  data+="/";
  data+=mqttClientLocation;
  data+="/";
  data+=mqttClientUID; 
  data+="/";
  data+=mqttClientFaultTopic;  
  return data; 
}

// Build the topic name to be used to publish/subscribe to Faults 
String buildAnyFaultTopic()
{
  String data = mqttClientPrefix;
  data+="/";
  data+="+";
  data+="/";
  data+="+"; 
  data+="/";
  data+=mqttClientFaultTopic;  
  return data; 
}

// Build a JSON message to send to MQTT Broker
// NOTE : MQTT_MAX_PACKET_SIZE = 128 bytes.. therefore not more than 100 for the payload 
//        unless you change it in /Arduino/libraries/pubSubClient/src/PubSubClient.h
String buildDeviceStatusJson()
{ 
  String data = "{";
  data+="\n"; 
  data+="\"ResetByOperator\": ";
  data+=(int)isResetRequested;
  data+= ",";
  data+="\n"; 
  data+="\"WaterDetected\": ";
  data+=(int)isWaterDetected;
  data+= ",";
  data+="\n";
  data+="\"ValveClosed\": ";
  data+=(int)isValveClosed;
  data+= ","; 
  data+="\n";
  data+="\"ValveDetected\": ";
  data+=(int)isConnectedToValve;  
  data+="\n";
  data+="}"; 
  return data; 
}

// Build a JSON message to send to MQTT Broker
// NOTE : MQTT_MAX_PACKET_SIZE = 128 bytes.. therefore not more than 100 for the payload 
//        unless you change it in /Arduino/libraries/pubSubClient/src/PubSubClient.h
String buildFaultJson()
{ 
  String data = "{";
  data+="\n"; 
  data+="\"WaterDetected\": ";
  data+=(int)isWaterDetected;
  data+= ","; 
  data+="\n";
  data+="\"Location\": ";
  data+=mqttClientLocation;
  data+="\n";
  data+="}"; 
  return data; 
}

// Report to MQTT broker
void publishMsg(char (&topic)[200], char (&payload)[200] )  
{
  if (connectToBroker() == true)
  {       
     if (client.publish(topic, payload) == true)
     {
        isConnectedToBroker = 1;    
        Serial.print(F("Message sent to MQTT broker using the following topic "));  
        Serial.println(topic);
     }
     else
     {     
        Serial.print(F("Message NOT sent to MQTT broker using the following topic "));  
        Serial.println(topic);
        isConnectedToBroker = 0;
     }    
     client.loop();      
  }
}
// Report faults to MQTT broker
void publishFault()
{
	// Topic
	char topicBuffer[200];
	buildFaultTopic().toCharArray(topicBuffer, 200);
	// Payload
	char payloadBuffer[200];
	buildFaultJson().toCharArray(payloadBuffer, 200); ;
	// Publish message
	publishMsg(topicBuffer, payloadBuffer);
}

// Report faults to MQTT broker
void publishStatus()
{
	// Topic
	char topicBuffer[200];
	buildDeviceStatusTopic().toCharArray(topicBuffer, 200);
	// Payload
	char payloadBuffer[200];
	buildDeviceStatusJson().toCharArray(payloadBuffer, 200); ;
	// Publish message
	publishMsg(topicBuffer, payloadBuffer);
}

// Subscribe to Faults reported by other monitoring devices 
void subscribeToRemoteWaterSensors()
{
  if (connectToBroker() == true)
  {   	
	   char tempBuffer[200];
	   buildAnyFaultTopic().toCharArray(tempBuffer, 200);
	   client.subscribe(tempBuffer); // otherwise subscriptions will growth forever..
     if (client.subscribe(tempBuffer) == true)
     {
        isConnectedToBroker = 1;    
        Serial.print(F("Registred to MQTT broker as a subscriber for the following topic: ")); 
        Serial.println(tempBuffer);
     }
     else
     {     
        Serial.println(F("Not registred to MQTT broker as a subscriber")); 
        isConnectedToBroker = 0;
     }    
     client.loop();          
  }
  else
  {
    isConnectedToBroker = 0;
	  Serial.println(F("Cannot subscribe to any topic since connection to MQTT broker is not established"));
  }
}

// Manage connection with MQTT broker
int connectToBroker() 
{
  Serial.println(F(""));        
  Serial.print(F("Connecting to network and to MQTT Broker... "));    
  char tempBuffer[200];
  buildClientIdentifier().toCharArray(tempBuffer,200);
  if (client.connect(tempBuffer) == true)
  {        
    Serial.print(F("connected as ")); 
    Serial.println(tempBuffer);
  } 
  else
  {
     switch (client.state())
     {
        case -4:
          Serial.println(F("MQTT_CONNECTION_TIMEOUT - the server didn't respond within the keepalive time"));
          break;
        case -3:
          Serial.println(F("MQTT_CONNECTION_LOST - the network connection was broken"));
          break;
        case -2:
          Serial.println(F("MQTT_CONNECT_FAILED - the network connection failed"));
          break;
        case -1:
          Serial.println(F("MQTT_DISCONNECTED - the client is disconnected cleanly"));
          break;
        case 0:
          break;
        case 1:
          Serial.println(F("MQTT_CONNECT_BAD_PROTOCOL - the server doesn't support the requested version of MQTT"));
          break;
        case 2:
          Serial.println(F("MQTT_CONNECT_BAD_CLIENT_ID - the server rejected the client identifier"));
          break;
        case 3:
          Serial.println(F("MQTT_CONNECT_UNAVAILABLE - the server was unable to accept the connection"));
          break;
        case 4:
          Serial.println(F("MQTT_CONNECT_BAD_CREDENTIALS - the username/password were rejected"));
          break;
        case 5:
          Serial.println(F("MQTT_CONNECT_UNAUTHORIZED - the client was not authorized to connect"));
          break;
        default:
          Serial.print("failed, rc=");
          Serial.println(client.state()); 
          break;        
     }
  }
  return client.connected();    
}
MQTT libraryC/C++
Just in case you need it...
No preview (download only).

Schematics

Water leakage detection and motorized valve control
7ok47tbqvv0xgztftevj

Comments

Similar projects you might like

Homotica - a simple, cost-effective home control system

Project showcase by Davide Vertuani

  • 50,445 views
  • 17 comments
  • 154 respects

Arduino Control AC Water Heater temperature

Project tutorial by Mohannad Rawashdeh

  • 13,446 views
  • 0 comments
  • 9 respects

Servo Control with TV Remote Control

Project showcase by eldo85

  • 11,128 views
  • 5 comments
  • 32 respects

Arduino Dynamic Web Control

Project tutorial by phpoc_man

  • 44,017 views
  • 19 comments
  • 152 respects

Control your light system with your voice

Project tutorial by Maha Raafat

  • 39,714 views
  • 22 comments
  • 159 respects

Home Management System - Control your home from a website

Project tutorial by Ahmed El-Hinidy

  • 37,224 views
  • 22 comments
  • 123 respects
Add projectSign up / Login