Building a Shower Timer with an ESP8266
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.
- Adafruit HUZZAH ESP8266.
- USB to TTL Serial Cable.
- 3/4" Electronic Flow Meter (AliExpress).
- 5 Volts, 3/4" Motorized Ball Valve (AliExpress).
- Une 5V Power Supply (Amazon).
- Breadboard, electrical wire and project box.
Wiring Diagram
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>
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.