The smart toilet light project brings IoT to your bathroom.  IoT, or Internet of Toilets, is the popular industry buzzword to describe a connected social network of intelligent toilets.  With the smart toilet light you can create a light that illuminates your toilet bowl based on data from the internet.  For example your toilet can shine red if it's going to rain, or glow yellow if it's sunny and clear outside.  Not only is the smart toilet light a useful aid to find the toilet in the dark, but it's a real source of information to help you plan for your day!

This project is built with a Feather Huzzah ESP8266 WiFi microcontroller and a 8mm NeoPixel LED.  You can 3D print a case and a LED clip to hold the hardware near the toilet bowl.  By connecting to Adafruit IO the ESP8266 can listen for color and animation commands that are sent from other services like If This Then That (IFTTT), Zapier, and more.  You'll have your toilet pulsing and flashing different colors from the internet in almost no time!

Before you get started you'll want to familiarize yourself with the following guides:

In addition you'll want to be familiar with how to solder--for this project you'll need to solder headers to a board, and wires to a LED.  This is a relatively easy soldering project even for a beginner, but be sure to read the guide to excellent soldering.

Parts

To build this project you'll need the following parts:

  • Feather Huzzah ESP8266.  You can use other ESP8266 boards like the simpler Huzzah ESP8266, but you'll need more parts like a serial to USB cable to talk to them.  Pick up a set of female Feather headers to make connecting wires to the Feather easier.
  • NeoPixel Diffused 8mm Through-Hole LED.  You can use any NeoPixels but I recommend the large 8mm through-hole LEDs for their large size and brightness.  The 3D printed LED holder for this project is designed for the 8mm NeoPixel.
  • 5V power supply.  The easiest way to power the board is with a 5V power supply plugged into the USB port.  You could use a battery like a lipoly pack, but I don't recommend putting a lithium battery near where it can get wet or fall into water.  Be careful, electronics and water don't mix!  Always plug devices into a GFCI outlet and do not place them where they can fall into water.
  • Soldering tools & wire.  You'll need to solder headers to the Feather and solder wires to the NeoPixel.

Assembly

Start by following the Feather Huzzah ESP8266 guide to assemble and test the ESP8266 board.  Don't continue until you're sure the board is working and you can upload sketches with the Arduino IDE!

Next solder wires to the NeoPixel.  Use a long enough length of wire to place the LED on the rim of the toilet bowl and keep the Huzzah board safely away from the toilet.  You'll need to solder wires to the data in, 5V and ground pins (the data out wire is unused):

It helps to tin each wire and pin with solder first, then hold them together with helping hands and apply the soldering iron to flow the solder.  Don't forget to slip on some heat shrink tubing too!

Connect the NeoPixel to the Feather Huzzah ESP8266 as follows:

  • NeoPixel data in to Huzzah ESP8266 pin #2.
  • NeoPixel 5V to Huzzah ESP8266 3.3V power.  You can instead use the VUSB pin if you're powering the board from its USB port.  Don't power the NeoPixel off the 3.3V power if you're using more than 1 or 2 pixels!  Consult the NeoPixel Uber Guide if you have any questions about how to power NeoPixels.
  • NeoPixel ground to Huzzah ESP8266 ground.

If you're having trouble getting bare wires to stay inside female headers, tin the wires with solder until they're thick enough to firmly stay in place.

That's all there is to assembling and wiring the hardware for this project!

3D Printed Case

If you have access to a 3D printer you can print a case for the Feather Huzzah ESP8266, and a bracket to hold the LED to the side of the toilet bowl.  These small parts should print on almost any 3D printer and are fine to print with PLA.

The LED bracket is designed to hold a 8mm LED and fit a toilet bowl with a 40mm wide rim.  It will probably work for slightly larger and smaller bowls, but you might need to adjust the part or design your own if your toilet is different.  You can find the source for this part on Tinkercad and adjust it as necessary.

Download the STL files for these parts from their home on Thingiverse below:

The parts should print without any need for support.  For PLA I used 20% infill and 0.02mm layer height with good results, but adjust as needed for your filament, printer, etc.

The Feather Huzzah ESP8266 should snap into place without need for screws, etc.  The case can optionally accomodate a slide switch however it is unused in this project.

For the LED holder thread the wires through the side and hole in the top, then pull them through to put the LED into place in the hole.  See the photos on the left for more details.

Once assembled your smart toilet light should look something like the following:

Place the clip on the rim of the toilet and make sure it is secure.  You can use tape or other means of adhesive if the clip is not held firmly in place (be careful not to use anything too permanent though!).  The Feather Huzzah ESP8266 should be placed somewhere secure and near a power source.

Be careful with electronics and water! Don't place anything near water which could cause harm if it were to get wet or fall into water. Always plug devices into a GFCI outlet, and ensure devices are secure and not able to fall into water!

Arduino Sketch

The software for this project is an Arduino sketch that will connect to Adafruit IO and listen for color and animation data from a feed.  You can even setup integration with IFTTT, Zapier, etc. to change the feed and toilet light color when something happens.  For example change the toilet color based on your local weather!

Before you setup the software for this project make sure you've followed the Feather Huzzah ESP8266 guide and can successfully program the board.  You will also want to install the following libraries with the Arduino library manager:

Then download the Arduino sketch for the project from its home on GitHub by clicking the button below:

// SPDX-FileCopyrightText: 2019 Tony DiCola for Adafruit Industries
//
// SPDX-License-Identifier: MIT

// Smart Toilet Light with ESP8266
//
// Use a feed in Adafruit IO to control the color and animation of a neopixel
// Attaching the neopixel to the bowl of a toilet so it shines down into it will
// create an interesting nightlight that can display information (like from IFTTT
// triggers).
//
// Author: Tony DiCola
// License: MIT (https://opensource.org/licenses/MIT)
#include <ESP8266WiFi.h>
#include "Adafruit_MQTT.h"
#include "Adafruit_MQTT_Client.h"
#include "Adafruit_NeoPixel.h"


// Configuration that you _must_ fill in:
#define WLAN_SSID       "... WiFi SSID ..."    // Your WiFi AP name.
#define WLAN_PASS       "... Password ..."     // Your WiFi AP password.
#define AIO_USERNAME    "... AIO username ..." // Adafruit IO username (see http://accounts.adafruit.com).
#define AIO_KEY         "... AIO key ..."      // Adafruit IO key

// Configuration you can optionally change (but probably want to keep the same):
#define PIXEL_PIN       2                      // Pin connected to the NeoPixel data input.
#define PIXEL_COUNT     1                      // Number of NeoPixels.
#define PIXEL_TYPE      NEO_RGB + NEO_KHZ800   // Type of the NeoPixels (see strandtest example).
#define LIGHT_FEED      "toilet-light"         // Name of the feed in Adafruit IO to listen for colors.
#define AIO_SERVER      "io.adafruit.com"      // Adafruit IO server address.
#define AIO_SERVERPORT  1883                   // AIO server port.
#define PING_SEC        60                     // How many seconds to wait between MQTT pings.
                                               // Used to help keep the connection alive during
                                               // long periods of inactivity.


// Global state (you don't need to change this):
// Put strings in flash memory (required for MQTT library).
const char MQTT_SERVER[] PROGMEM    = AIO_SERVER;
const char MQTT_USERNAME[] PROGMEM  = AIO_USERNAME;
const char MQTT_PASSWORD[] PROGMEM  = AIO_KEY;
const char MQTT_PATH[] PROGMEM      = AIO_USERNAME "/feeds/" LIGHT_FEED;
// Create ESP8266 wifi client, MQTT client, and feed subscription.
WiFiClient client;
Adafruit_MQTT_Client mqtt(&client, MQTT_SERVER, AIO_SERVERPORT, MQTT_USERNAME, MQTT_PASSWORD);
Adafruit_MQTT_Subscribe lightFeed = Adafruit_MQTT_Subscribe(&mqtt, MQTT_PATH);
// Create NeoPixel.
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(PIXEL_COUNT, PIXEL_PIN, PIXEL_TYPE);
// Other global state:
uint32_t nextPing = 0;       // Next time a MQTT ping should be sent.
int red = 0;                 // RGB color for the current animation.
int green = 0;
int blue = 0;
int animation = 0;           // Current animation (0 = none, 1 = pulse, 2 = rainbow cycle).
int pulsePeriodMS = 0;       // Period of time (in MS) for the pulse animation.


// Explicit declaration of MQTT_connect function defined further below.
// Necessary because of bug/issue with recent Arduino builder & ESP8266.
void MQTT_connect();

// Function to set all the NeoPixels to the specified color.
void lightPixels(uint32_t color) {
  for (int i=0; i<PIXEL_COUNT; ++i) {
    pixels.setPixelColor(i, color);
  }
  pixels.show();
}

// Function to parse a hex byte value from a string.
// The passed in string MUST be at least 2 characters long!
// If the value can't be parsed then -1 is returned, otherwise the
// byte value is returned.
int parseHexByte(char* data) {
  char high = tolower(data[0]);
  char low = tolower(data[1]);
  uint8_t result = 0;
  // Parse the high nibble.
  if ((high >= '0') && (high <= '9')) {
    result += 16*(high-'0');
  }
  else if ((high >= 'a') && (high <= 'f')) {
    result += 16*(10+(high-'a'));
  }
  else {
    // Couldn't parse the high nibble.
    return -1;
  }
  // Parse the low nibble.
  if ((low >= '0') && (low <= '9')) {
    result += low-'0';
  }
  else if ((low >= 'a') && (low <= 'f')) {
    result += 10+(low-'a');
  }
  else {
    // Couldn't parse the low nibble.
    return -1;
  }
  return result;
}

// Linear interpolation of value y within range y0...y1 given a value x
// and the range x0...x1.
float lerp(float x, float y0, float y1, float x0, float x1) {
  return y0 + (y1-y0)*((x-x0)/(x1-x0));
}

// Pulse the pixels from their color down to black (off) and back
// up every pulse period milliseconds.
void pulseAnimation() {
  // Calculate how far we are into the current pulse period.
  int n = millis() % pulsePeriodMS;
  // Pulse up or down depending on how far along into the period.
  if (n < (pulsePeriodMS/2)) {
    // In the first half so pulse up.
    // Interpolate between black/off and full color using n.
    uint8_t cr = (uint8_t)lerp(n, 0, red,   0, pulsePeriodMS/2-1);
    uint8_t cg = (uint8_t)lerp(n, 0, green, 0, pulsePeriodMS/2-1);
    uint8_t cb = (uint8_t)lerp(n, 0, blue,  0, pulsePeriodMS/2-1);
    // Light the pixels.
    lightPixels(pixels.Color(cr, cg, cb));
  }
  else {
    // In the second half so pulse down.
    // Interpolate between full color and black/off color using n.
    uint8_t cr = (uint8_t)lerp(n, red,   0, pulsePeriodMS/2, pulsePeriodMS-1);
    uint8_t cg = (uint8_t)lerp(n, green, 0, pulsePeriodMS/2, pulsePeriodMS-1);
    uint8_t cb = (uint8_t)lerp(n, blue,  0, pulsePeriodMS/2, pulsePeriodMS-1);
    // Light the pixels.
    lightPixels(pixels.Color(cr, cg, cb));
  }
  
}

// Input a value 0 to 255 to get a color value.
// The colours are a transition r - g - b - back to r.
uint32_t Wheel(byte WheelPos) {
  WheelPos = 255 - WheelPos;
  if(WheelPos < 85) {
    return pixels.Color(255 - WheelPos * 3, 0, WheelPos * 3);
  }
  if(WheelPos < 170) {
    WheelPos -= 85;
    return pixels.Color(0, WheelPos * 3, 255 - WheelPos * 3);
  }
  WheelPos -= 170;
  return pixels.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}

void rainbowAnimation() {
  // Assume the rainbow cycles every 2.56 seconds so there's a
  // 10 millisecond delay every color change.
  int n = (millis()/10) % 256;
  lightPixels(Wheel(n));
}

void setup() {
  // Initialize serial output.
  Serial.begin(115200);
  delay(10);
  Serial.println("Smart Toilet Light with ESP8266");

  // Initialize NeoPixels and turn them off.
  pixels.begin();
  lightPixels(pixels.Color(0, 0, 0));

  // Connect to WiFi access point.
  Serial.println(); Serial.println();
  Serial.print("Connecting to ");
  Serial.println(WLAN_SSID);
  WiFi.begin(WLAN_SSID, WLAN_PASS);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println();
  Serial.println("WiFi connected");
  Serial.println("IP address: "); Serial.println(WiFi.localIP());

  // Setup MQTT subscription.
  mqtt.subscribe(&lightFeed);
}

void loop() {
  // Do any NeoPixel animation logic.
  if (animation == 1) {
    pulseAnimation();
  }
  else if (animation == 2) {
    rainbowAnimation();
  }
  
  // Ensure the connection to the MQTT server is alive (this will make the first
  // connection and automatically reconnect when disconnected).  See the MQTT_connect
  // function definition further below.
  MQTT_connect();

  // Check if any new data has been received from the light feed.
  Adafruit_MQTT_Subscribe *subscription;
  while ((subscription = mqtt.readSubscription(10))) {
    if (subscription == &lightFeed) {
      // Received data from the light feed!
      // Parse the data to see how to change the light.
      char* data = (char*)lightFeed.lastread;
      int dataLen = strlen(data);
      Serial.print("Got: ");
      Serial.println(data);
      if (dataLen < 1) {
        // Stop processing if not enough data was received.
        continue;
      }
      // Check the first character to determine the light change command.
      switch (data[0]) {
        case 'S':
          // Solid color.
          // Expect 6 more characters with the hex red, green, blue color.
          if (dataLen >= 7) {
            // Parse out the RGB color bytes.
            int r = parseHexByte(&data[1]);
            int g = parseHexByte(&data[3]);
            int b = parseHexByte(&data[5]);
            if ((r < 0) || (g < 0) || (b < 0)) {
              // Couldn't parse the color, stop processing.
              break; 
            }
            // Light the pixels!
            lightPixels(pixels.Color(r, g, b));
            // Change the animation to none/stop animating.
            animation = 0;
          }
          break;
        case 'P':
          // Pulse animation.
          // Expect 8 more characters with the hex red, green, blue color, and
          // a byte value with the frequency to pulse within a 10 second period.
          // I.e. to make it the light pulse once every 2 seconds send the value
          // 5 so that the light pulses 5 times within a ten second period (every
          // 2 seconds).
          if (dataLen >= 9) {
            // Parse out the RGB color and frequency bytes.
            int r = parseHexByte(&data[1]);
            int g = parseHexByte(&data[3]);
            int b = parseHexByte(&data[5]);
            int f = parseHexByte(&data[7]);
            if ((r < 0) || (g < 0) || (b < 0) || (f < 0)) {
              // Couldn't parse the data, stop processing.
              break; 
            }
            // Change the color for the pulse animation.
            red   = r;
            green = g;
            blue  = b;
            // Calculate the pulse length in milliseconds from the specified frequency.
            pulsePeriodMS = (10.0 / (float)f) * 1000.0;
            // Change the animation to pulse.
            animation = 1;
          }
          break;
        case 'R':
          // Rainbow cycle animation.
          animation = 2;
          break;
      }
    }
  }

  // Ping the MQTT server periodically to prevent the connection from being closed.
  if (millis() >= nextPing) {
    // Attempt to send a ping.
    if(! mqtt.ping()) {
      // Disconnect if the ping failed.  Next loop iteration a reconnect will be attempted.
      mqtt.disconnect();
    }
    // Set the time for the next ping.
    nextPing = millis() + PING_SEC*1000L;
  }
}

// Function to connect and reconnect as necessary to the MQTT server.
// Should be called in the loop function and it will take care of connecting.
void MQTT_connect() {
  int8_t ret;

  // Stop if already connected.
  if (mqtt.connected()) {
    return;
  }

  Serial.print("Connecting to MQTT... ");

  uint8_t retries = 3;
  while ((ret = mqtt.connect()) != 0) { // connect will return 0 for connected
       Serial.println(mqtt.connectErrorString(ret));
       Serial.println("Retrying MQTT connection in 5 seconds...");
       mqtt.disconnect();
       delay(5000);  // wait 5 seconds
       retries--;
       if (retries == 0) {
         // basically die and wait for WDT to reset me
         while (1);
       }
  }
  Serial.println("MQTT Connected!");
}

Load the sketch in the Arduino IDE.  You should see it look something like the following:

There are a few #define values at the top of the sketch that you will need to change:

  • WLAN_SSID - Set this to the name of your WiFi access point.
  • WLAN_PASS - Set this to the password for your WiFi access point.
  • AIO_USERNAME - Set this to your Adafruit IO username (you can find it on http://accounts.adafruit.com).
  • AIO_KEY - Your Adafruit IO key.  You can find this by clicking the View AIO Keys button on the Adafruit IO settings page.

There are other #define values below that you can optionally change, however I recommend keeping them at their defaults unless you know your hardware is setup in a different way.

Upload the sketch to your Feather Huzzah ESP8266 board.  Then open the serial monitor in Arduino and set it to 115200 baud.  You should see the board print information as it connects to the WiFi network and then Adafruit IO.  For example:

Make sure you see the sketch connecting to your WiFi network and Adafruit IO (the MQTT Connected! message).  If you see an error connecting to WiFi check that you have the right SSID name & password.  If you see an error connecting to MQTT/Adafruit IO check you have the right username and Adafruit IO key set and try again.

Once the sketch is running and connecting to Adafruit IO you're ready to start controlling it from the web!

Adafruit IO

To control the animation and color of the light you can use Adafruit IO.  If you aren't familiar with Adafruit IO you'll want to read through the following guides first:

By default the software for this project will look for a feed named toilet-light in your Adafruit IO account.  You can send simple text commands to this feed to change the color and animation of the light.

The easiest way to change and test the light is to directly send a color value to the feed.  From the Adafruit IO feeds list for your account find the feed named toilet-light.  If you don't see a feed named toilet light then use the Create Feed button to create one (name it toilet-light).  

Click the toilet-light feed to go to its feed page:

Click the blue + icon in the lower right to manually set the value of the toilet-light feed.  This will bring up a dialog where you can enter a new feed value.

Enter the value SFF0000 (those are zeros, not the letter O), then press Save Changes.  After a moment you should see your toilet light LED turn red!

The value that you set the toilet-light feed is a command that can control the color and animation of the LED.  You can send the following types of commands:

  • Srrggbb - Set the LED to a solid color specified by the hex rr, gg, bb values (red, green, blue).  For example the hex RGB color FF00AA (red = 255, green = 0, blue = 170) could be set with the command: SFF00AA
  • Prrggbbff - Pulse the LED from full color to off repeatedly using the specified hex rr, gg, bb values (red, green, blue), and at the specified hex frequency ff.  Frequency is specified as a hex byte value (0-255) and is the number of times the light should pulse from off to on and back off again in a 10 second period.  For example to pulse the LED red every 2 seconds you would send: PFF000005
  • R - Animate the LED with a rainbow color cycle.  There's no parameter or other information to send, just set the feed to the value R!

Try setting the toilet-light feed to different solid, pulsing, and rainbow cycle values.  That's all there is to controlling the light from Adafruit IO!  Continue on to learn about interesting ways to drive the light, like from weather triggers on IFTTT and Zapier.

If This Then That is a great internet service that simplifies home automation.  IFTTT can listen to services like GMail, Dropbox, Twitter, etc. and take actions like controlling lights in your home or even sending data to Adafruit IO.  Using IFTTT you can trigger your toilet light to change color in many interesting ways.  For example you can have IFTTT change the color of your toilet light based on the current weather in your area.

To setup IFTTT to show weather on the toilet light you'll first need to sign up for an IFTTT account.  Then create a new recipe:

Click 'this' to pick what action will trigger the toilet light.  Search for weather and you'll see IFTTT's native weather trigger channel (the blue cloud and yellow sun on the right):

Click the weather trigger channel to select it and see the available triggers.  Note that you might need to first add the weather trigger channel and configure it to your location before you can continue.

For this example you want the Current condition changes to trigger which will fire when the weather in your area changes to rain, snow, clouds, or clear conditions.  However read the other triggers to see what other weather actions you might use with the toilet light.

After selecting the Current condition changes to trigger you can now pick the specific condition that this trigger will action on.  For example to change the light when it's raining pick the Rain condition in the Current condition drop down:

Now click Create Trigger to continue on to the 'that' configuration for this recipe:

Click 'that' and you'll see the available action channels.  Search for or select the Adafruit channel.  Note that you might need to do some one time setup to authorize IFTTT the first time you select Adafruit's channel.

Select Adafruit's channel and pick the Send data to Adafruit IO action:

Now select the toilet-light feed in the Feed name drop down (if you don't see the feed then make sure you've setup and changed the toilet light LED color by following all the steps in the previous page).

For the Data to save value you can specify a command to change the toilet light color.  For example the command PFF000005 will pulse the toilet light red every 2 seconds.  Use any command like changing to a solid color, pulsing other colors, or animating a rainbow cycle--check out the previous page for the command syntax.

Click Create Action to finish setting up the recipe.

Give the recipe a descriptive name and then click Create Recipe to finish and enable it.  When the weather changes to rain you should see the toilet light pulse red!

Try setting up more recipes to change the light when weather conditions change to snow, clouds, or clear skys:

That's all there is to setting up the toilet light with IFTTT!  You can hook up almost any action to control the toilet light.  For example have it flash when you have too many unread emails or missed a phone call.  The possibilities are almost endless!

Zapier is another great service for automating and connecting other online services.  Just like IFTTT you can use Zapier to trigger on actions and send data to Adafruit IO.  For example you can setup Zapier to change the toilet light color based on the weather.

First you will need to create an account on Zapier's website.  Then login and click Make a Zap:

In the list of triggers tha appears search for weather and select the Weather by Zapier trigger.

Choose the Will it Rain Today? trigger for a simple trigger to change the toilet light color if it will rain.

Follow the steps on the page to pick the right latitude and longitude for your location.

After clicking continue Zapier will confirm the latitude and longitude you chose, and test that it can access weather data.  Click Fetch & Continue to move on.

After the test runs it should succeed and you can click Continue to pick the action that will be taken when Zapier detects it will rain.

In the list of actions search for Adafruit to find the Adafruit IO action.  Choose the action and you'll see it has a Send Value to Feed option:

After clicking Save + Continue you'll need to setup your Adafruit IO account with Zapier.  If you've already setup the account then choose it from the list, otherwise click Connect a New Account and go through the authorization process.

Again click Save + Continue to move on to set the feed and feed value.  For the Value pick a command to change the toilet light color, like PFF000005 to pulse the light red every two seconds.  You can use any toilet light feed command, like setting a solid color or rainbow cycle, see the command syntax on the previous page.

For the Feed pick the toilet-light feed that powers your toilet light. 

Click Continue and Zapier will move on to test the Adafruit IO connection.  Click Create & Continue to perform the test.

After the test runs you're almost done!  Click Finish to move on and give the zap a name.

Name the zap and then click OFF to toggle the Zap ON!  Your toilet light should now pulse red when Zapier performs a rain check every morning.  Cool!

Explore other triggers on Zapier's site to see more interesting ways to control your toilet light.  For example have it flash when you have too many unread emails or missed a phone call.  The possibilities are almost endless!

This guide was first published on Apr 01, 2016. It was last updated on Mar 16, 2024.