← Projects
electronics intermediate Jul 15, 2026 ◑ 1 views ◯ 5 min read

Build a Wall-Mounted Smart Clock and Weather Display (ESP32 + RTC)

Build time: 1 day
Tools needed: Laser cutter or 3D printer
Parts List
esp32wall clockweather stationrtcds3231laser cut3d printed enclosurediytft display

A genuinely good wall clock replacement that also shows the weather — built on an ESP32 with a color display, pulling live time and forecast data over WiFi, in an enclosure you actually design rather than a bare dev board stuck to the wall. This is the project version of "my clock broke" that ends with something better than what broke.

Design Goals

The difference between "a screen showing the time" and "a nice clock for your wall" is almost entirely the physical presentation. This build treats the enclosure as seriously as the electronics — a laser-cut or CNC-routed face, a 3D printed housing, and a layout that reads clearly from across a room, not just up close.

Hardware

ComponentNotes ESP32 dev boardAny standard ESP32 works fine — this project's demands are light Round or square TFT display, 2.8"-4" A round display genuinely sells the "this is a clock" read at a glance better than a rectangular one, though either works fine functionally RTC module (DS3231)Keeps accurate time through brief power/WiFi outages via battery backup — without it, the clock loses its sense of time every time it loses power or WiFi, which for a WALL CLOCK is a real problem 5V USB power supplyWall-powered, no battery needed for a stationary wall clock

Getting a Weather API Key

OpenWeatherMap's free tier is more than sufficient for a single wall display checking every 10-30 minutes — sign up at openweathermap.org, generate an API key, and note your location's coordinates (or city name) for the forecast endpoint.

Wiring

Core Firmware Structure

#include <WiFi.h> #include <HTTPClient.h> #include <ArduinoJson.h> #include <RTClib.h> #include <TFT_eSPI.h> RTC_DS3231 rtc; TFT_eSPI tft = TFT_eSPI(); const char* WEATHER_API_KEY = "your_key_here"; const char* LAT = "35.4"; // your latitude const char* LON = "-82.9"; // your longitude float currentTemp = 0; String currentCondition = ""; unsigned long lastWeatherFetch = 0; const unsigned long WEATHER_INTERVAL = 15 * 60 * 1000; // 15 minutes void setup() { tft.init(); tft.setRotation(0); tft.fillScreen(TFT_BLACK); Wire.begin(); rtc.begin(); // If the RTC lost power (dead coin cell, first boot), set it from compile time as a fallback if (rtc.lostPower()) { rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); } WiFi.begin("your-ssid", "your-password"); while (WiFi.status() != WL_CONNECTED) delay(500); // Sync RTC from NTP once on boot for accurate real-world time syncTimeFromNTP(); fetchWeather(); } void syncTimeFromNTP() { configTime(-5 * 3600, 0, "pool.ntp.org"); // adjust UTC offset for your timezone struct tm timeinfo; if (getLocalTime(&timeinfo)) { rtc.adjust(DateTime(timeinfo.tm_year+1900, timeinfo.tm_mon+1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec)); } } void fetchWeather() { HTTPClient http; String url = "http://api.openweathermap.org/data/2.5/weather?lat=" + String(LAT) + "&lon=" + String(LON) + "&units=imperial&appid=" + String(WEATHER_API_KEY); http.begin(url); if (http.GET() == 200) { JsonDocument doc; deserializeJson(doc, http.getString()); currentTemp = doc["main"]["temp"]; currentCondition = doc["weather"][0]["main"].as<String>(); } http.end(); } void loop() { DateTime now = rtc.now(); if (millis() - lastWeatherFetch > WEATHER_INTERVAL) { fetchWeather(); lastWeatherFetch = millis(); } drawClockFace(now); delay(1000); }

Drawing a Clean Clock Face

Rather than just printing digits, drawing an actual analog-style face (or a large, well-spaced digital layout) makes a real difference in how "finished" this looks from across a room:

void drawClockFace(DateTime now) { tft.fillScreen(TFT_BLACK); // Large centered digital time char timeStr[9]; sprintf(timeStr, "%02d:%02d:%02d", now.hour(), now.minute(), now.second()); tft.setTextSize(4); tft.setTextColor(TFT_WHITE); tft.setCursor(30, 60); tft.println(timeStr); // Date below char dateStr[16]; sprintf(dateStr, "%02d/%02d/%04d", now.month(), now.day(), now.year()); tft.setTextSize(2); tft.setCursor(50, 110); tft.println(dateStr); // Weather block tft.setTextSize(3); tft.setTextColor(TFT_CYAN); tft.setCursor(40, 160); tft.print(String((int)currentTemp) + "F"); tft.setTextSize(2); tft.setTextColor(TFT_WHITE); tft.setCursor(40, 200); tft.print(currentCondition); }

Refine the layout to fit your specific display's resolution and your own visual taste — the actual positioning above is a starting point, not a fixed design.

Designing the Enclosure

This is where the project earns its place on a wall instead of a desk:

Design the face plate's cutout to your exact display's active area (not the full PCB size — most display boards have a visible screen area smaller than the board itself), and mock up the fit with cardboard or scrap material before committing to your final material.

Nice-to-Have Additions

AdditionValue Ambient light sensor + auto-dimmingDims the display at night so it's not glaringly bright in a dark room — genuinely important for something staying on 24/7 on a wall Multi-day forecastOpenWeatherMap's free tier includes a forecast endpoint, not just current conditions — cycle between "now" and "next 3 days" on a timer Second time zoneIf you regularly coordinate with someone elsewhere, a small secondary time readout costs almost nothing to add NTP re-sync scheduleBeyond the boot-time sync, re-sync from NTP once every 24 hours to correct any RTC drift accumulated over time

Why the RTC Actually Matters Here

It's tempting to skip the DS3231 and just re-fetch time from NTP on every loop — but that means the clock face freezes or shows stale time the moment WiFi drops, which for a wall clock (a thing whose entire job is "always show the correct time") is a real functional failure. The RTC keeps ticking independently, accurate to within a couple seconds a month, regardless of what WiFi is doing — NTP just becomes a periodic correction rather than a hard dependency for the clock to function at all.

The end result: something that actually replaces a broken wall clock, does more than the one that broke, and looks like you meant to build it that way — because you did.