Spiria logo.

Building a Shower Timer with an ESP8266

November 29, 2016.

Dads, you too can enjoy that warm trickling feeling of a hot shower once again without rising at the crack of dawn. Necessity is the mother of invention, and in my house, with two teenage daughters, I had to end the curse of cold morning showers by building a shower timer that I installed on my hot-water tank.

Working Principle

An electrically operated ball valve is installed on the supply side of my hot water tank and paired with a water flowmeter. A small microcontroller device monitors the amount of time during which the water is passing through the tank. When the allotted time is reached, the valve is temporarily cycled off and on for a few seconds to warn the person taking a shower that her time is up.

Required Materials

For this project, I selected an Adafruit HUZZAH ESP8266 board, which is both cheap and packed with features that make it interesting to play with. First, it has WiFi capability, which allows it to be controlled remotely. This means that it can be initialized as an access point, which allows me to configure it to join a home network. Second, it can run a Web server to allow over-the-air updates: once the original wiring plan is installed, there is no further need to connect it through the USB port for updates. Third, it comes with a built-in voltage regulator, converting the 5V current from the power supply that drives the valve’s motor to a voltage that can be handled by the GPIO pins.

Wiring Diagram

Wiring Diagram.

Installation

Installation.

Program

The logic is quite simple. An interrupt routine monitors the impulses generated by the flowmeter device. These impulses are accumulated while, every minute, a timer calls a callback routine that stores the accumulated impulses in an array which can hold up to 15 minutes of data. When all the cells in the array contain a value other than 0, the controller cycles the valve off and on

Source Code

// MrShower
// Created by Patrick Walsh.
//
// Hot Water monitoring system installed at the intake of a hot water tank.
// When hot water has been running for 15 minutes an electrical ball valve will cycle off and on every minute until usage ends.( Shower ) an elc
//

#include <esp8266httpupdateserver.h>
#include <esp8266mdns.h>
#include <esp8266wifi.h>
#include <ticker.h>   
#include "WiFiManager.h"

#define SKETCH_VERSION              1.0

#define HAL_SENSOR_PIN               15   // Flowmeter Hal sensor is connected to this pin 
#define MONITOR_MINS                 15   // Showers can last a maximum of 15 mins before warning kicks in
#define SHOWER_SHUTOFF_WARNING_TIME  5.0  // Duration valve will be closed to warn time is up
#define TIMER_PERIOD                 60.0 // Period required to calculate RPM of Hal effect sensor
#define VALVE_CLOSE_CTRL_PIN         12   // Relay connected to the electrical valve Open Wire     
#define VALVE_OPEN_CTRL_PIN          14   // Relay connected to the electrical valve Close Wire 
#define VALVE_TRANSIT_DELAY          2.0  // Time taken for valve to move from closed state ot open state

//
// Global variables
//
const char*              g_HostName = "MrShower";  // Duh
ESP8266WebServer         g_HttpServer(80);         // Http server we will be providing
ESP8266HTTPUpdateServer  g_HttpUpdater(false);     // A OverTheAir update service.       Http://MrShower.local/update
Ticker                   g_MonitoringTimer;        // Interupt timer used for counting minutes
unsigned                 g_Rpms[MONITOR_MINS];     // History of rpms in the last "n" minutes

// global variables accessed from ISR that need to be protected
volatile bool            g_FlashWater = false;     // True if valve needs to be closed temporarily
volatile unsigned        g_HallSensorPulses = 0;   // FlowMeter pulses that have occurred in the current minute
volatile bool            g_WaterOn = false;        // State of Valve 

//
// Forward Declarations
//
void closeWaterValve();
void openWaterValve();
void onHallEffect();
void onTimerTick();

void webServerHandleRoot();
void webServerHandleNotFound();
void webServerHandleOpenCmd();
void webServerHandleCloseCmd();
void webServerHandleResetCmd();
void webServerHandleCycleCmd();

///////////////////////////////////////////////////////////////////
//
// Setup is called at initialization
//
/////////////////////////////////////////////////////////////////////
void setup()
{
  // Initialize Relay control
  pinMode(VALVE_CLOSE_CTRL_PIN, OUTPUT);
  pinMode(VALVE_OPEN_CTRL_PIN, OUTPUT);
    
  // Make sure the water valve is open.
  openWaterValve();
  g_HallSensorPulses = 0;  
 
  // Setup an Access point in order to allow network setup
  WiFiManager wifiManager;
  wifiManager.autoConnect( g_HostName );

  WiFi.mode( WIFI_STA );
  WiFi.softAPdisconnect( true );

  // Establish a connection with our configured access point
  while( WiFi.waitForConnectResult() != WL_CONNECTED )
  {
    WiFi.begin();
  }

  // install web serv handlers.
  g_HttpServer.on("/",      webServerHandleRoot ); 
  g_HttpServer.onNotFound(  webServerHandleNotFound );
  g_HttpServer.on("/open",  webServerHandleOpenCmd);
  g_HttpServer.on("/close", webServerHandleCloseCmd );
  g_HttpServer.on("/reset", webServerHandleResetCmd );
  g_HttpServer.on("/cycle", webServerHandleCycleCmd );

  // Add OTA update service provided by library "/update" command
  g_HttpUpdater.setup( &g_HttpServer );
  g_HttpServer.begin( );

  // Register web services we expose with Bonjour
  MDNS.begin( g_HostName );
  MDNS.addService( "http", "tcp", 80 );
  
  // Clear the array of rpms
  memset( g_Rpms, 0, sizeof( g_Rpms ) );  

  // Attach Hall Sensor to HAL_SENSOR_PIN.
  // ISR called when voltage rises to match one 
  // revolution of the flowmeter turbine  
  attachInterrupt( HAL_SENSOR_PIN, onHallEffect, RISING );

  // Start a timer which will treat the reading of the hall sensor and calculate RPM
  g_MonitoringTimer.attach( TIMER_PERIOD, onTimerTick );
}

///////////////////////////////////////////////////////////////////
//
// Code in the loop method will be run repeatedly
//
///////////////////////////////////////////////////////////////////
void loop() 
{
  // Allow webserver to process queue of requests sent to it.
  g_HttpServer.handleClient();

  if( g_FlashWater )
  {
    // Need to cycle the hot water valve
    closeWaterValve( );
    delay( SHOWER_SHUTOFF_WARNING_TIME * 1000 );
    openWaterValve( );
    g_FlashWater = false;
  }
}

///////////////////////////////////////////////////////////////////
//
// Close water valve by controlling 2 relays connected
// to ball valve motor
//
///////////////////////////////////////////////////////////////////void closeWaterValve()
{
  digitalWrite( VALVE_OPEN_CTRL_PIN, HIGH );
  digitalWrite( VALVE_CLOSE_CTRL_PIN, LOW );
  delay( VALVE_TRANSIT_DELAY * 1000 );
  digitalWrite(VALVE_CLOSE_CTRL_PIN, HIGH ); 
  g_WaterOn = false;
}

///////////////////////////////////////////////////////////////////
//
// Open water valve by controlling 2 relays connected
// to ball valve motor
//
///////////////////////////////////////////////////////////////////
void openWaterValve()
{ 
  digitalWrite( VALVE_CLOSE_CTRL_PIN, HIGH );
  digitalWrite( VALVE_OPEN_CTRL_PIN, LOW );
  delay( VALVE_TRANSIT_DELAY * 1000 );
  digitalWrite( VALVE_OPEN_CTRL_PIN, HIGH );
  g_WaterOn = true;
}

///////////////////////////////////////////////////////////////////
//
// Interrupt routine called when FlowMeter is generating Hal Effect pulses.
//
///////////////////////////////////////////////////////////////////
void onHallEffect()
{ 
  // Increase the number of pulses detected by the HallEffect sensor
  // of the water flowmeter
  g_HallSensorPulses++;
}

///////////////////////////////////////////////////////////////////
//
// Timer-driven callback routine called every minute.
//
///////////////////////////////////////////////////////////////////

void onTimerTick()
{  
  // At every clock tick, once per minute, interrupts temporarily disabled while
  // accumulated pulses generated by flowmeter sensor are read and reset.  This gives us a rpm value.
  
  cli( );                          // disable interupts
  long l_rpm = g_HallSensorPulses; // copy rpm value locally
  g_HallSensorPulses = 0;          // sensor reading done for this minute
  sei( );                          // reenable interrupts

  // shift the content of the array to the right 1
  memmove( &g_Rpms[1], &g_Rpms[0], sizeof( g_Rpms ) - sizeof( g_Rpms[0] ) );

  // Set the latest reading in array
  g_Rpms[0] = l_rpm;

  int l_numUsedTicks = 0;
  
  for( int idx = 0; idx < MONITOR_MINS; idx++ )
  {
    if( g_Rpms[idx] )
    {
      l_numUsedTicks++;
    }
  }

  // Do we need to flash hot water
  if( l_numUsedTicks >= MONITOR_MINS && !g_FlashWater )
  {
    g_FlashWater = true;

    // Set last element of array to 0 to delay next hot water valve cycle by one minute
    g_Rpms[ MONITOR_MINS - 1 ] = 0; 
  }
}

///////////////////////////////////////////////////////////////////
//
// Web Server section
//
///////////////////////////////////////////////////////////////////
void webServerHandleRoot() 
{
  String message;
  message += g_HostName;
  message += "Version ";
  message += SKETCH_VERSION;
  message += " \n";
  message += "FlowMeter revolutions this minute ";
  message += g_HallSensorPulses;
  message += " \n";
  message += g_WaterOn == true ? "Valve is Opened" : "Valve is Closed";
  message += " : ";
  for(int idx = 0; idx < MONITOR_MINS; idx++)
  {
    message +=  g_Rpms[idx];
    message += " "; 
  }

  message += " \n";
  message += "Commands : /open /close /update /cycle /reset\n";
  
  g_HttpServer.send(200, "text/plain", message );      
}

///////////////////////////////////////////////////////////////////
//
// Unknown page requested
//
///////////////////////////////////////////////////////////////////
void webServerHandleNotFound() 
{
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += g_HttpServer.uri();
  message += "\nMethod: ";
  message += (g_HttpServer.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += g_HttpServer.args();
  message += "\n";
  
  for (uint8_t i = 0; i < g_HttpServer.args(); i++) 
  {
    message += " " + g_HttpServer.argName(i) + ": " + g_HttpServer.arg(i) + "\n";
  }
  
  g_HttpServer.send(404, "text/plain", message);
}


///////////////////////////////////////////////////////////////////
//
// Open requested
//
///////////////////////////////////////////////////////////////////
void webServerHandleOpenCmd()
{
  g_HttpServer.send(200, "text/plain", "Opening Valve");
  openWaterValve();
} 

///////////////////////////////////////////////////////////////////
//
// Close requested
//
///////////////////////////////////////////////////////////////////
void webServerHandleCloseCmd()
{
  g_HttpServer.send(200, "text/plain", "Closing Valve");
  closeWaterValve();
}

///////////////////////////////////////////////////////////////////
//
// Reset requested
//
///////////////////////////////////////////////////////////////////
void webServerHandleResetCmd()
{
  memset( g_Rpms, 0, sizeof( g_Rpms ) );  
  openWaterValve();
  g_HttpServer.send(200, "text/plain", "System was reset" );
}

///////////////////////////////////////////////////////////////////
//
// Cycle the valve off and on
//
///////////////////////////////////////////////////////////////////
void webServerHandleCycleCmd()
{
  g_HttpServer.send(200, "text/plain", "Cycling water valve" );

  // Set the Flash water variable to true.
  // The loop routine, when true, will cycle the water valve,
  g_FlashWater = true;
}
</ticker.h></esp8266wifi.h></esp8266mdns.h></esp8266httpupdateserver.h>

 

Web Server.

Did you know that?
Spiria’s teams have a long experience in the development of complex custom applications and can help you on any large-scale project.

Ideas for improvements

  • Collecting and reporting hot water consumption per month or year.
  • Providing UI to help configure the valve cycling time and monitoring period.
  • Adding water sensor on floor to detect accidental flooding due to water heater tank failure.
  • Support for Apple Homekit or other home automation network.