Build an internet alarm clock

A desk clock that sets itself from the internet, shows the time and room temperature on the LCD, and has an alarm that you set from your phone.
  • Level:Advanced
  • Duration: 1 h 50 min
  • Ages 13+
  • Platform: ESP32

Tools for this lesson

Helpful for this build. Open a tool to see tips and more projects that use it.

Build an internet alarm clock – EEMENTOR STEM™ lesson

Most clocks need to be set by hand, and they slowly drift. An internet clock never does: it asks a time server for the exact time. In this capstone you build an alarm clock around that idea. The ESP32 fetches the time over Wi-Fi and adjusts for your time zone and summer time. The LCD shows the day, date, time and room temperature. You set the alarm from a web page on your phone, and the setting is saved, so it survives a power cut. A button on the desk stops the alarm.

What you'll learn

  • How to get the exact time from the internet with NTP
  • How time zones and summer time are handled with one line of text
  • How to build a web form and read what the user typed
  • How to save settings permanently with the Preferences library

What you need

  • ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
  • 1602 LCD display, 10 kΩ potentiometer and 1 resistor, 220 Ω (ESP32 IoT Starter Kit)
  • DHT11 module, KY-015 (ESP32 IoT Starter Kit)
  • Passive buzzer module (45-in-1 Sensor Kit)
  • Button module (45-in-1 Sensor Kit)
  • Breadboard and jumper wires (ESP32 IoT Starter Kit)
  • A 2.4 GHz Wi-Fi network

Before you start

The LCD has 16 connections in a row. If yours has no pins, solder 16 pins from the 40-pin male header in the kit first, so it plugs into the breadboard. Ask an adult to help if you're new to soldering.

Wiring

The LCD needs 5 V, so the breadboard's + rail carries 5 V in this project. The DHT11 must run on 3.3 V here, so its + pin goes straight to the ESP32's 3V3 pin, not to the rail.

Breadboard wiring diagram: Build an internet alarm clock
Breadboard view. Check each connection against the wiring table below.
From To
LCD VSS, RW and K GND
LCD VDD 5V
LCD V0 Middle pin (W) of the potentiometer
LCD RS ESP32 GPIO 13
LCD E ESP32 GPIO 14
LCD D4, D5, D6, D7 ESP32 GPIO 27, 26, 25, 33
LCD A (backlight +) 220 Ω resistor, then 5V
Potentiometer outer pins 5V and GND
DHT11 S (signal) ESP32 GPIO 32
DHT11 + ESP32 3V3
DHT11 − GND
Passive buzzer module S ESP32 GPIO 23
Passive buzzer module − GND
Button module S ESP32 GPIO 22
Button module − GND
Button module + Not connected

Sensor modules from different makers can label and order their pins differently. Always follow the labels printed on your module.

Choose your time zone

The sketch has one line called TIME_ZONE. Replace its text with the one for where you live. The text also contains the rules for summer time, so the clock changes by itself in spring and autumn.

Where you live TIME_ZONE text
UK, Ireland, Portugal GMT0BST,M3.5.0/1,M10.5.0
Central Europe (Germany, France, Spain, Italy, Sweden…) CET-1CEST,M3.5.0,M10.5.0/3
Estonia, Finland, Latvia, Lithuania, Greece EET-2EEST,M3.5.0/3,M10.5.0/4
US Eastern EST5EDT,M3.2.0,M11.1.0
US Pacific PST8PDT,M3.2.0,M11.1.0
India IST-5:30
UAE <+04>-4

Build it in stages

This is a bigger build than the earlier lessons, so don't wire everything at once. Add one part, test it, then add the next. When something stops working, you know it's the part you just added.

  1. The clock. Wire the LCD and the potentiometer, set your Wi-Fi details and time zone, and upload. The screen first shows the address of the clock's web page, then the day, date and time.
  2. Temperature. Add the DHT11. The bottom line shows the room temperature.
  3. The alarm. Add the buzzer and the button. Open the address from the screen in a browser on the same Wi-Fi, set the alarm to two minutes from now and tap Save.
  4. Stop it. When the alarm rings, press the button, or tap Stop the alarm on the web page.
Example screen for: Build an internet alarm clock
Example screen with sample values. Yours may look different.

The sketch

Install the DHT sensor library by Adafruit. Everything else is part of the ESP32 board package.

Replace YOUR_WIFI_NAME and YOUR_WIFI_PASSWORD with your own network details (2.4 GHz only).

#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>
#include <LiquidCrystal.h>
#include <DHT.h>
#include <time.h>

const char* ssid = "YOUR_WIFI_NAME";
const char* password = "YOUR_WIFI_PASSWORD";
const char* TIME_ZONE = "EET-2EEST,M3.5.0/3,M10.5.0/4";  // Estonia; change for your country

// LCD pins: RS, E, D4, D5, D6, D7
LiquidCrystal lcd(13, 14, 27, 26, 25, 33);
DHT dht(32, DHT11);
const int BUZZER_PIN = 23;
const int BUTTON_PIN = 22;

WebServer server(80);
Preferences settings;

int alarmHour = 7;
int alarmMinute = 0;
bool alarmOn = true;
bool ringing = false;
bool buzzerOn = false;
int lastRungMinute = -1;          // stops the alarm from starting twice in the same minute
unsigned long ringStart = 0;
unsigned long lastScreen = 0;
unsigned long lastTemperature = 0;
float temperature = NAN;

void stopAlarm() {
  ringing = false;
  buzzerOn = false;
  noTone(BUZZER_PIN);
}

void handleRoot() {
  char alarmText[6];
  snprintf(alarmText, sizeof(alarmText), "%02d:%02d", alarmHour, alarmMinute);

  String html = "<!DOCTYPE html><html><head>"
    "<meta name='viewport' content='width=device-width, initial-scale=1'>"
    "<title>Alarm clock</title><style>"
    "body{font-family:sans-serif;max-width:420px;margin:30px auto;padding:0 16px;text-align:center}"
    "input,button{font-size:1.4em;margin:10px;padding:8px 14px}"
    "</style></head><body><h1>Alarm clock</h1>"
    "<form action='/set'><p><input type='time' name='time' value='";
  html += alarmText;
  html += "' required></p><p><label><input type='checkbox' name='on' value='1'";
  if (alarmOn) html += " checked";
  html += "> Alarm on</label></p><button>Save</button></form>";
  if (ringing) html += "<form action='/stop'><button>Stop the alarm</button></form>";
  html += "</body></html>";
  server.send(200, "text/html", html);
}

void goHome() {
  server.sendHeader("Location", "/");
  server.send(303);
}

void handleSet() {
  String text = server.arg("time");                 // for example "07:30"
  if (text.length() == 5 && text[2] == ':') {
    alarmHour = constrain(text.substring(0, 2).toInt(), 0, 23);
    alarmMinute = constrain(text.substring(3).toInt(), 0, 59);
  }
  alarmOn = server.hasArg("on");                    // a checkbox is only sent when ticked
  settings.putInt("hour", alarmHour);
  settings.putInt("minute", alarmMinute);
  settings.putBool("on", alarmOn);
  lastRungMinute = -1;
  goHome();
}

void handleStop() {
  stopAlarm();
  goHome();
}

void showClock(struct tm &now) {
  char line[17];
  strftime(line, sizeof(line), "%a %d.%m  %H:%M", &now);    // "Mon 13.10  07:30"
  lcd.setCursor(0, 0);
  lcd.print(line);

  lcd.setCursor(0, 1);
  if (ringing) {
    lcd.print("** WAKE UP! **  ");
    return;
  }
  char temperatureText[8] = "--.-";
  if (!isnan(temperature)) snprintf(temperatureText, sizeof(temperatureText), "%.1f", temperature);
  if (alarmOn) snprintf(line, sizeof(line), "%4sC  Al %02d:%02d ", temperatureText, alarmHour, alarmMinute);
  else snprintf(line, sizeof(line), "%4sC  Al off   ", temperatureText);
  lcd.print(line);
}

void setup() {
  Serial.begin(115200);
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  lcd.begin(16, 2);
  dht.begin();

  settings.begin("clock", false);                   // open our saved settings
  alarmHour = settings.getInt("hour", 7);           // 7 is used if nothing was saved yet
  alarmMinute = settings.getInt("minute", 0);
  alarmOn = settings.getBool("on", true);

  lcd.print("Connecting...");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  configTzTime(TIME_ZONE, "pool.ntp.org");          // real time from the internet

  lcd.clear();
  lcd.print("Set alarm at:");
  lcd.setCursor(0, 1);
  lcd.print(WiFi.localIP());
  Serial.print("Open this address in your browser: http://");
  Serial.println(WiFi.localIP());
  delay(6000);
  lcd.clear();

  server.on("/", handleRoot);
  server.on("/set", handleSet);
  server.on("/stop", handleStop);
  server.begin();
}

void loop() {
  server.handleClient();
  unsigned long ms = millis();

  if (ringing) {
    // two short beeps every second
    unsigned long part = (ms - ringStart) % 1000;
    bool wanted = part < 150 || (part > 250 && part < 400);
    if (wanted != buzzerOn) {
      buzzerOn = wanted;
      if (wanted) tone(BUZZER_PIN, 2000);
      else noTone(BUZZER_PIN);
    }
    bool pressed = digitalRead(BUTTON_PIN) == LOW;
    if (pressed || ms - ringStart > 120000) stopAlarm();   // button, or after 2 minutes
  }

  if (ms - lastTemperature >= 5000 || lastTemperature == 0) {
    lastTemperature = ms;
    float reading = dht.readTemperature();
    if (!isnan(reading)) temperature = reading;
  }

  if (ms - lastScreen >= 1000) {                    // once per second
    lastScreen = ms;
    struct tm now;
    if (!getLocalTime(&now, 10)) {
      lcd.setCursor(0, 0);
      lcd.print("Waiting for time");
      return;
    }

    int thisMinute = now.tm_yday * 1440 + now.tm_hour * 60 + now.tm_min;
    if (alarmOn && !ringing && now.tm_hour == alarmHour && now.tm_min == alarmMinute && thisMinute != lastRungMinute) {
      ringing = true;
      ringStart = ms;
      lastRungMinute = thisMinute;
    }
    showClock(now);
  }
}

How it works

NTP (Network Time Protocol) is how computers all over the world agree on the time. configTzTime() asks a public time server once, and then keeps asking in the background every so often, so the ESP32's own clock never drifts far. NTP always answers in universal time (UTC). The TIME_ZONE text tells the ESP32 how far your local time is from UTC and on which dates summer time begins and ends. getLocalTime() then fills a struct tm with ready-to-use numbers: hour, minute, day of the week and so on.

The web page contains a form. When you tap Save, the browser opens an address such as /set?time=07:30&on=1, and server.arg("time") reads the value from it. The sketch checks that the text really looks like a time before using it. Never trust what comes in from a web page without checking it.

Preferences stores small values in the ESP32's flash memory under a name. putInt("hour", 7) saves a value and getInt("hour", 7) reads it back, with a default for the very first start. That's why your alarm time is still there after a power cut.

The alarm starts when the hour and minute match. lastRungMinute remembers the minute in which it last rang, so stopping the alarm doesn't let it start again a second later.

Try this next

  • Add a snooze: a short press pauses the alarm for five minutes, a long press stops it.
  • Ring only on school days. now.tm_wday is 0 on Sunday and 6 on Saturday.
  • Play a melody instead of beeps, using an array of notes.
  • Add the photoresistor module and use a transistor from the kit to dim the backlight at night.

Troubleshooting

  • It never connects to Wi-Fi: check the network name and password (both are case-sensitive) and make sure the network is 2.4 GHz. The ESP32 can't use 5 GHz networks.
  • The screen lights up but shows only blocks or nothing: turn the potentiometer slowly from one end to the other.
  • The time is wrong by a whole number of hours: the TIME_ZONE text doesn't match where you live. Copy it exactly from the table.
  • "Waiting for time" stays on the screen: the ESP32 is connected to Wi-Fi but can't reach the internet. Some school and guest networks block time servers.
  • The alarm stops immediately or never stops with the button: some button modules work the other way round. Change == LOW to == HIGH in the line that reads the button.
  • The temperature shows --.-: check the DHT11 signal wire on GPIO 32 and that its + pin is on 3V3.
  • No sound: make sure you're using the passive buzzer module, and update the ESP32 board package if the IDE reports that tone is unknown.

Build it yourself

Everything used in this lesson comes in one kit, with parts tested by our engineers.

ESP32 IoT Starter Kit | Arduino IDE Compatible | Free Tutorials $57.00

Shop the kit