2022-02-18

WiFi Doorbell

A web-enabled WiFi doorbell

When we moved in our house this year, one of the first things we missed was a doorbell. There was a bell button at the gate, but since the whole building had been gutted down to the concrete nobody knew where the cable ended. When I found a cut-off electric cable in the basement, I suspected that it could be the one leading to the button. With a little help of my beloved multimeter I could confirm that assumption.

However, that led to the next problem: Not only was the cable cut 2 cm off the wall, there was also no way to extend it to the first floor. And a doorbell in the basement is pretty much useless. There are WiFi solutions available that allow to put the bell in a different room, but they are expensive and need a 220 V hookup. So we settled with a wireless doorbell for the time being but it turned out not to be completely reliable, pretty ugly in comparison to the existing button and I did not like the battery based solution. So I set out to revive the old doorbell with the help of a microcontroller. Or two.

Parts list

The concept is easy: The first microcontroller detects the push on the button and sends a request to the second microcontroller via the home WiFi. That second microcontroller is connected to a speaker and plays a bell sound. Since I am pretty new to the Arduino world and wanted to keep things easy, I used Espressif ESP32 boards that have integrated WiFi. To make it even easier, I added a cheap MP3 player module.

Update 2023-02-18 I have updated the code for the speaker. It now covers a webhook, triggering specific sounds via network and an endpoint for restarting (as some versions of the dfplayer seem to be prone to get stuck from time to time).
There is now also a version of the code for ESP8266 boards like the Wemos D1 mini.

Assembly

Not much to see here. Basically just throwing all the modules together. In the end I left out the LED on the speaker side. (It is still addressed in the code, though.)

Case

I designed the two cases in Onshape, one for the sender and one for the speaker unit. The speaker case has a thin grid at the bottom and a little stand that is glued on in order to let the sound pass out.

Results

Code

While the sender does not much more than blinking the LED slightly differently depending on what it is doing, the speaker unit offers a bit more: It starts up a tiny web server that provides the interface for the sender and can also be used directly via the browser. It allows also to set the speaker volume:

Sender Code

/*
 * Sources:
 * https://www.arduino.cc/en/Tutorial/Button
 * https://techtutorialsx.com/2017/05/19/esp32-http-get-requests/
 */

#include <WiFi.h>
#include <HTTPClient.h>
 
const char* ssid     = "WiFi SSID";
const char* password = "WiFi password";
const char* bellUrl = "http://192.168.1.149/bell/on";

const int buttonPin = 21;    // the number of the pushbutton pin
const int ledPin =  23;      // the number of the LED pin
int buttonState = 0;

 
void setup() {

  Serial.begin(115200);
  btStop(); // turn off bluetooth

  pinMode(ledPin, OUTPUT);
  pinMode(buttonPin, INPUT);

  connectWifi();

}
 
void loop() {
 
  if ((WiFi.status() == WL_CONNECTED)) {

    buttonState = digitalRead(buttonPin);
    delay(100);

    if (buttonState == HIGH) {
      digitalWrite(ledPin, HIGH);
      
      HTTPClient http;
   
      http.begin(bellUrl);
      Serial.print("GET ");
      Serial.println(bellUrl);
      int httpCode = http.GET();
   
      if (httpCode > 0) {
          //String payload = http.getString();
          Serial.println(httpCode);
          //Serial.println(payload);
        }
      else {
        Serial.println("Error on HTTP request");
      }
   
      http.end();
      delay(200);
    }
    else {
      digitalWrite(ledPin, LOW); 
    }
  }
  else {
    Serial.println("WiFi not connected!");
    digitalWrite(ledPin, HIGH);
    delay(200);
    digitalWrite(ledPin, LOW);
    delay(50);
    digitalWrite(ledPin, HIGH);
    delay(200);
    digitalWrite(ledPin, LOW);
    connectWifi();
  }
 
}

void connectWifi() {
  boolean ledStatus = false;
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    ledStatus = !ledStatus;
    digitalWrite(ledPin, ledStatus);
  }
 
  Serial.println("WiFi connected.");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

Speaker Code: Version for ESP32


/**
 * WiFi Doorbell for ESP32 with dfplayer
 * by Tilman Liero
 * www.tilman.de
 **/

#include <DFRobotDFPlayerMini.h>
#include <WiFi.h>
#include <WebServer.h>
#include <HTTPClient.h>

DFRobotDFPlayerMini dfPlayer;
int volume = 22;

const char* ssid     = "SSID";
const char* password = "PASSWORD";

WiFiServer server(80);
String header;

String webhook = "http://www.google.com/";
WiFiClient webClient;
HTTPClient http;

void setup()
{
  // start serial output
  Serial.begin(115200);
  Serial.println();
  
  btStop(); // turn off bluetooth

  Serial.println("Connect dfplayer");
  Serial1.begin(9600, SERIAL_8N1, 18, 19);  // speed, type, TX, RX
  dfPlayer.begin(Serial1);
  delay(100);
  dfPlayer.setTimeOut(500); // Set serial communication time out 500ms
  dfPlayer.volume(volume);  // Set volume value (0~30).
  dfPlayer.EQ(DFPLAYER_EQ_NORMAL);
  dfPlayer.outputDevice(DFPLAYER_DEVICE_SD);

  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.hostname("Doorbell");
  WiFi.begin(ssid, password);
 
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("WiFi connected, IP address: ");
  Serial.println(WiFi.localIP());
  WiFi.setAutoReconnect(true);
  WiFi.persistent(true);

  server.begin();
  delay(500);
  dfPlayer.play(2);  // Play the second mp3

  Serial.println("Setup completed");
}


void loop() {

  WiFiClient client = server.available();   // Listen for incoming clients
  
  if (client) {
    String currentLine = "";                // make a String to hold incoming data from the client
    while (client.connected()) {            // loop while the client's connected
      if (client.available()) {             // if there's bytes to read from the client,
        char c = client.read();             // read a byte, then
        //Serial.write(c);                    // print it out the serial monitor
        header += c;
        if (c == '\n') {                    // if the byte is a newline character
          // if the current line is blank, you got two newline characters in a row.
          // that's the end of the client HTTP request, so send a response:
          if (currentLine.length() == 0) {
            // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
            // and a content-type so the client knows what's coming, then a blank line:
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type: text/html; charset=utf-8");
            client.println("Connection: close");
            client.println();
            
            // turns the GPIOs on and off
            if (header.indexOf("GET /bell/on") >= 0) {
              dfPlayer.play(1);  // play the first mp3
              Serial.println("RING");
            }
            else  if (header.indexOf("GET /play/") >= 0) { // yes, I know this is not RESTful
              String str1 = header;
              str1 = str1.substring(header.indexOf("GET /play/") + 10);
              int fileNo = str1.substring(0, str1.indexOf(" ")).toInt();
              
              Serial.print("Play file ");
              Serial.println(fileNo);
              
              if (fileNo <= 1) {
                dfPlayer.play(1);
              }
              else {
                dfPlayer.play(fileNo);
              }
            }
            else  if (header.indexOf("GET /setvolume/") >= 0) {
              String str1 = header;
              str1 = str1.substring(header.indexOf("GET /setvolume/") + 15);
              volume = str1.substring(0, str1.indexOf(" ")).toInt();
              
              if (volume < 0) {
                volume = 0;
              }
              else if (volume > 30) {
                volume = 30;
              }
              
              dfPlayer.volume(volume);
              Serial.print("Volume set to ");
              Serial.println(volume);
            }
            else if (header.indexOf("GET /restart") >= 0) {
              Serial.println("Restarting...");
              ESP.restart();
            }
            else if (header.indexOf("GET /dfreset") >= 0) {
              Serial.println("Reset dfplayer...");
              dfPlayer.reset();
            }
			
            // Display the HTML web page
            client.println("<!DOCTYPE html><html>");
            client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
            client.println("<link rel=\"icon\" href=\"data:,\">");
            client.println("<title>Doorbell</title>");
            client.println("<style>html { font-family: sans-serif; display: inline-block; margin: 0px auto; text-align: center; }");
            client.println(".button { background-color: #4CAF50; border: none; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}");
            client.println(".button2 { background-color: #4CAF50; border: none; color: white; padding: 4px 10px; text-decoration: none; margin: 1px; cursor: pointer;}</style></head>");
            
            client.println("<body><h1>Doorbell</h1>");
            client.println("<p>Volume: <a href=\"/setvolume/" + String(volume-1) + "\"><button class=\"button2\">−</button></a> " + String(volume) + " <a href=\"/setvolume/" + String(volume+1) + "\"><button class=\"button2\">+</button></a></p>");
            client.println("<p><a href=\"/bell/on\"><button class=\"button\">Ring</button></a></p>");
            client.println("</body></html>");
            
            // The HTTP response ends with another blank line
            client.println();

            // call webhook after ringing
            if (header.indexOf("GET /bell/on") >= 0) {
              
              if (http.begin(webClient, webhook)) {
                int httpCode = http.GET();
                
                if (httpCode > 0) {
                  if (httpCode == HTTP_CODE_OK) {
                    String payload = http.getString();
                    //Serial.println(payload);
                  }
                }
                else {
                  Serial.printf("HTTP Error: ", http.errorToString(httpCode).c_str());
                }
                // close connection
                http.end();
              }
              else {
                Serial.println("Could not send HTTP request");
              }
            }
            
            break;
          }
          else { // if you got a newline, then clear currentLine
            currentLine = "";
          }
        }
        else if (c != '\r') {  // if you got anything else but a carriage return character,
          currentLine += c;      // add it to the end of the currentLine
        }
      }
    }
    // Clear the header variable
    header = "";
    // Close the connection
    client.stop();
    Serial.println("Client disconnected");
    Serial.println("");
  }
}

Speaker Code: Version for Wemos D1 mini / ESP8266


/*
 * Doorbell Speaker for WEMOS D1 mini with dfplayer
 * by Tilman Liero
 * www.tilman.de
 * 
 * Sources:
 * dfplayer library: https://github.com/DFRobot/DFRobotDFPlayerMini via https://wolles-elektronikkiste.de/dfplayer-mini-ansteuerung-mit-dem-arduino
 * info on how to use dfplayer with WEMOS D1: https://forum.arduino.cc/t/dfplayer-mini-mit-wemos-d1-uno/677980/3
 * WEMOS D1 WiFi setup: https://averagemaker.com/2018/04/how-to-set-up-wifi-on-a-wemos.html
 * ESP8266 web server: https://randomnerdtutorials.com/esp8266-web-server/
 * ESP8266 GET request: https://makesmart.net/esp8266-http-get-request/
 */


#include <DFRobotDFPlayerMini.h>
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>

DFRobotDFPlayerMini dfPlayer;
int volume = 22;

const char* ssid     = "SSID";
const char* password = "PASSWORD";

WiFiServer server(80);
String header;

String webhook = "http://www.google.com/";
WiFiClient webClient;
HTTPClient http;

void setup()
{
  // start serial output
  Serial.begin(115200);
  Serial.println();

  Serial.println("Connect dfplayer");
  Serial1.begin(9600); // TX on PIN D3, RX on PIN D4
  dfPlayer.begin(Serial1);
  delay(100);
  dfPlayer.setTimeOut(500); // Set serial communication time out 500ms
  dfPlayer.volume(volume);  // Set volume value (0~30).
  dfPlayer.EQ(DFPLAYER_EQ_NORMAL);
  dfPlayer.outputDevice(DFPLAYER_DEVICE_SD);

  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.hostname("Doorbell");
  WiFi.begin(ssid, password);
 
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("WiFi connected, IP address: ");
  Serial.println(WiFi.localIP());
  WiFi.setAutoReconnect(true);
  WiFi.persistent(true);

  server.begin();
  delay(500);
  dfPlayer.play(2);  // Play the second mp3
  
  Serial.println("Setup completed");
}


void loop() {

  WiFiClient client = server.available();   // Listen for incoming clients
  
  if (client) {
    String currentLine = "";                // make a String to hold incoming data from the client
    while (client.connected()) {            // loop while the client's connected
      if (client.available()) {             // if there's bytes to read from the client,
        char c = client.read();             // read a byte, then
        //Serial.write(c);                    // print it out the serial monitor
        header += c;
        if (c == '\n') {                    // if the byte is a newline character
          // if the current line is blank, you got two newline characters in a row.
          // that's the end of the client HTTP request, so send a response:
          if (currentLine.length() == 0) {
            // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
            // and a content-type so the client knows what's coming, then a blank line:
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type: text/html; charset=utf-8");
            client.println("Connection: close");
            client.println();
            
            // turns the GPIOs on and off
            if (header.indexOf("GET /bell/on") >= 0) {
              dfPlayer.play(1);  // play the first mp3
              Serial.println("RING");
            }
            else  if (header.indexOf("GET /play/") >= 0) { // yes, I know this is not RESTful
              String str1 = header;
              str1 = str1.substring(header.indexOf("GET /play/") + 10);
              int fileNo = str1.substring(0, str1.indexOf(" ")).toInt();
              
              Serial.print("Play file ");
              Serial.println(fileNo);
              
              if (fileNo <= 1) {
                dfPlayer.play(1);
              }
              else {
                dfPlayer.play(fileNo);
              }
            }
            else if (header.indexOf("GET /setvolume/") >= 0) {
              String str1 = header;
              str1 = str1.substring(header.indexOf("GET /setvolume/") + 15);
              volume = str1.substring(0, str1.indexOf(" ")).toInt();
              
              if (volume < 0) {
                volume = 0;
              }
              else if (volume > 30) {
                volume = 30;
              }
              
              dfPlayer.volume(volume);
              Serial.print("Volume set to ");
              Serial.println(volume);
            }
            else if (header.indexOf("GET /restart") >= 0) {
              Serial.println("Restarting...");
              ESP.restart();
            }
            else if (header.indexOf("GET /dfreset") >= 0) {
              Serial.println("Reset dfplayer...");
              dfPlayer.reset();
            }
              
            // Display the HTML web page
            client.println("<!DOCTYPE html><html>");
            client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
            client.println("<link rel=\"icon\" href=\"data:,\">");
            client.println("<title>Doorbell</title>");
            client.println("<style>html { font-family: sans-serif; display: inline-block; margin: 0px auto; text-align: center; }");
            client.println(".button { background-color: #4CAF50; border: none; color: white; padding: 16px 40px; text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}");
            client.println(".button2 { background-color: #4CAF50; border: none; color: white; padding: 4px 10px; text-decoration: none; margin: 1px; cursor: pointer;}</style></head>");
            
            client.println("<body><h1>Doorbell</h1>");
            client.println("<p>Volume: <a href=\"/setvolume/" + String(volume-1) + "\"><button class=\"button2\">−</button></a> " + String(volume) + " <a href=\"/setvolume/" + String(volume+1) + "\"><button class=\"button2\">+</button></a></p>");
            client.println("<p><a href=\"/bell/on\"><button class=\"button\">Ring</button></a></p>");
            client.println("</body></html>");
            
            // The HTTP response ends with another blank line
            client.println();

            // call webhook after ringing
            if (header.indexOf("GET /bell/on") >= 0) {
              
              if (http.begin(webClient, webhook)) {
                int httpCode = http.GET();
                
                if (httpCode > 0) {
                  if (httpCode == HTTP_CODE_OK) {
                    String payload = http.getString();
                    //Serial.println(payload);
                  }
                }
                else {
                  Serial.printf("HTTP Error: ", http.errorToString(httpCode).c_str());
                }
                // close connection
                http.end();
              }
              else {
                Serial.println("Could not send HTTP request");
              }
            }
            
            break;
          }
          else { // if you got a newline, then clear currentLine
            currentLine = "";
          }
        }
        else if (c != '\r') {  // if you got anything else but a carriage return character,
          currentLine += c;      // add it to the end of the currentLine
        }
      }
    }
    // Clear the header variable
    header = "";
    // Close the connection
    client.stop();
    Serial.println("Client disconnected");
    Serial.println("");
  }
}