ESP32 OLED Weather Clock

IoTBeginnerIntermediateAdvanced

Display accurate NTP time and live OpenWeatherMap weather conditions on a 128x64 OLED screen, with a multi-page dashboard showing temperature, humidity, wind speed, and a 7-day forecast served from a local web page.

Overview

In this beginner project you will connect the ESP32 to Wi-Fi, synchronise its internal clock with an NTP server, and display the current time and date on a 128x64 SSD1306 OLED screen. The display updates every second showing hours, minutes, seconds, day of the week, and full date. No external RTC module is needed because NTP keeps the ESP32 clock accurate for the life of the Wi-Fi session.

Components
  • 1× ESP32 DevKit V1
  • 1× SSD1306 OLED 128x64 I2C — 0.96 inch; I2C address 0x3C
  • 1× Wi-Fi router or hotspot — 2.4 GHz band
  • 1× Breadboard and jumper wires
Wiring
Component PinESP32 PinNotes
OLED SDAGPIO 21I2C data
OLED SCLGPIO 22I2C clock
OLED VCC3V3
OLED GNDGND
Arduino Code
esp32-oled-weather-clock_beginner.ino
// ESP32 OLED Weather Clock - Beginner
// NTP time on SSD1306 OLED
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <WiFi.h>
#include <time.h>

const char* SSID="YourSSID";
const char* PASS="YourPassword";
const char* NTP="pool.ntp.org";
const long  GMT_OFFSET=0;     // seconds east of UTC; adjust for your zone
const int   DST_OFFSET=0;     // daylight saving offset in seconds

Adafruit_SSD1306 oled(128,64,&Wire,-1);

const char* DAYS[]={"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"};
const char* MONTHS[]={"Jan","Feb","Mar","Apr","May","Jun",
                       "Jul","Aug","Sep","Oct","Nov","Dec"};

void setup(){
  Serial.begin(115200);
  Wire.begin(21,22);
  oled.begin(SSD1306_SWITCHCAPVCC,0x3C);
  oled.setTextColor(WHITE);

  oled.clearDisplay(); oled.setTextSize(1);
  oled.setCursor(0,0); oled.println("Connecting WiFi...");
  oled.display();

  WiFi.begin(SSID,PASS);
  while(WiFi.status()!=WL_CONNECTED) delay(500);
  configTime(GMT_OFFSET,DST_OFFSET,NTP);

  // Wait for NTP sync
  struct tm ti;
  while(!getLocalTime(&ti)) delay(500);
  oled.clearDisplay(); oled.setCursor(0,0);
  oled.println("NTP synced!");
  oled.display(); delay(1000);
}

void loop(){
  struct tm ti;
  if(!getLocalTime(&ti)) return;

  oled.clearDisplay();

  // Large time display
  oled.setTextSize(2);
  oled.setCursor(10,5);
  oled.printf("%02d:%02d:%02d",ti.tm_hour,ti.tm_min,ti.tm_sec);

  // Day and date
  oled.setTextSize(1);
  oled.setCursor(0,35);
  oled.println(DAYS[ti.tm_wday]);
  oled.setCursor(0,46);
  oled.printf("%d %s %d",ti.tm_mday,MONTHS[ti.tm_mon],ti.tm_year+1900);

  // Week number
  oled.setCursor(0,57);
  char week[12]; strftime(week,12,"Week %V",&ti);
  oled.print(week);

  oled.display();
  delay(1000);
}
How It Works
01

NTP Time Synchronisation: configTime() sets the ESP32 SNTP client with the UTC offset, daylight saving offset, and NTP server address. The ESP32 sends a UDP request to pool.ntp.org which responds with a UTC timestamp accurate to within 50 ms over the internet.

02

getLocalTime() Struct Population: getLocalTime() populates a tm struct with local time fields: tm_hour, tm_min, tm_sec, tm_mday, tm_mon (0-indexed), tm_year (years since 1900), and tm_wday (0=Sunday). Adding offsets converts these to the correct local display values.

03

Two-Size OLED Layout: setTextSize(2) doubles the character size to 12x16 pixels for the time, making it readable at a glance. setTextSize(1) uses 6x8 pixel characters for the day, date, and week number below.

04

Week Number: strftime() with the %V format code computes the ISO 8601 week number (1-53). This is useful for planning calendars and is not available directly from the tm struct fields.

Applications
  • Desktop clock for maker desk or workshop
  • Time display for photography or video recording setups
  • Office clock showing day and week number for scheduling
  • Classroom clock with large readable time display
Troubleshooting

OLED stays blank after startup

Check I2C address: run an I2C scanner sketch and confirm the OLED responds at 0x3C. Some OLED modules use 0x3D. Change the address in oled.begin() accordingly.

Time shows 1970 or does not update

The NTP sync is asynchronous. Wait at least 5 seconds after configTime() before calling getLocalTime(). Add a while(!getLocalTime(&ti)) loop with a 500 ms delay.

Time is off by several hours

Adjust the GMT_OFFSET constant. For UTC+5 use 5*3600=18000. For UTC-5 use -5*3600=-18000. Set DST_OFFSET=3600 if daylight saving is currently active.

Display flickers every second

Call oled.clearDisplay() before drawing and oled.display() after. Do not call display() more than once per loop iteration. The flicker is from clearing the full screen buffer; use partial updates to reduce it.

Upgrades
  • Add a battery and RTC module (DS3231) to maintain time without Wi-Fi
  • Add a brightness control: dim the OLED at night using a photoresistor
  • Add a second time zone on the lower part of the display for remote teams
  • Add alarms stored in NVS that flash the OLED and sound a buzzer
FAQ

You need an ESP32 DevKit, TODO: sensor, TODO: output, a breadboard, jumper wires, and a USB cable for power and programming.

Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.

Start with Beginner if you are new to IoT Projects. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.

Overview

The intermediate build adds real-time weather data from the OpenWeatherMap free API. The OLED cycles through two pages every 5 seconds: a clock page with time and date, and a weather page showing temperature, humidity, weather description, wind speed, and a sunrise/sunset indicator. A button lets you manually switch between pages. Data refreshes every 10 minutes to stay within the free API rate limit.

Components
  • 1× ESP32 DevKit V1
  • 1× SSD1306 OLED 128x64 I2C
  • 1× Tactile push button — Manual page switch
  • 1× Wi-Fi router — Internet access required
  • 1× OpenWeatherMap API key — Free account at openweathermap.org
  • 1× Breadboard and wires
Wiring
Component PinESP32 PinNotes
OLED SDA/SCLGPIO 21/22
Page buttonGPIO 0Internal pull-up; active LOW
Arduino Code
esp32-oled-weather-clock_intermediate.ino
// ESP32 OLED Weather Clock - Intermediate (OpenWeatherMap + auto-page)
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <time.h>

const char* SSID="YourSSID", *PASS="YourPass";
const char* OWM_KEY="YourOpenWeatherMapKey";
const char* CITY="London";
const char* COUNTRY="GB";
const long  GMT_OFFSET=0, DST_OFFSET=0;

Adafruit_SSD1306 oled(128,64,&Wire,-1);

float   temp_c=0, humidity=0, wind_ms=0;
String  description="--";
long    sunrise=0, sunset=0;
unsigned long lastWeather=0;
int page=0;

void fetchWeather(){
  HTTPClient http;
  String url="http://api.openweathermap.org/data/2.5/weather?q=";
  url+=CITY; url+=","; url+=COUNTRY;
  url+="&units=metric&appid="; url+=OWM_KEY;
  http.begin(url);
  if(http.GET()==200){
    StaticJsonDocument<1024> doc;
    deserializeJson(doc,http.getString());
    temp_c    = doc["main"]["temp"];
    humidity  = doc["main"]["humidity"];
    wind_ms   = doc["wind"]["speed"];
    description=doc["weather"][0]["description"].as<String>();
    sunrise   = doc["sys"]["sunrise"];
    sunset    = doc["sys"]["sunset"];
  }
  http.end();
}

void showClock(){
  struct tm ti; getLocalTime(&ti);
  oled.clearDisplay();
  oled.setTextSize(2); oled.setCursor(8,5);
  oled.printf("%02d:%02d:%02d",ti.tm_hour,ti.tm_min,ti.tm_sec);
  oled.setTextSize(1); oled.setCursor(0,35);
  char buf[32]; strftime(buf,32,"%A %d %b %Y",&ti);
  oled.println(buf);
  oled.display();
}

void showWeather(){
  oled.clearDisplay(); oled.setTextSize(1);
  oled.setCursor(0,0);  oled.printf("%.1f C  Hum:%.0f%%n",temp_c,humidity);
  oled.setCursor(0,10); oled.printf("Wind: %.1f m/sn",wind_ms);
  oled.setCursor(0,20); oled.println(description.substring(0,20));

  // Sunrise/sunset icons using ASCII blocks
  oled.setCursor(0,35);
  time_t sr=sunrise, ss=sunset;
  struct tm *sr_tm=localtime(&sr), *ss_tm=localtime(&ss);
  oled.printf("Rise %02d:%02d  Set %02d:%02d",
    sr_tm->tm_hour,sr_tm->tm_min,
    ss_tm->tm_hour,ss_tm->tm_min);

  oled.setCursor(0,50);
  oled.print(CITY);
  oled.display();
}

void setup(){
  Serial.begin(115200);
  Wire.begin(21,22);
  oled.begin(SSD1306_SWITCHCAPVCC,0x3C);
  oled.setTextColor(WHITE);
  pinMode(0,INPUT_PULLUP);
  WiFi.begin(SSID,PASS);
  while(WiFi.status()!=WL_CONNECTED) delay(500);
  configTime(GMT_OFFSET,DST_OFFSET,"pool.ntp.org");
  struct tm ti; while(!getLocalTime(&ti)) delay(500);
  fetchWeather(); lastWeather=millis();
}

void loop(){
  if(digitalRead(0)==LOW){ page^=1; delay(200); }
  if(millis()-lastWeather>600000){ fetchWeather(); lastWeather=millis(); }

  // Auto-cycle every 5 seconds
  static unsigned long lastSwitch=0;
  if(millis()-lastSwitch>5000){ page^=1; lastSwitch=millis(); }

  if(page==0) showClock(); else showWeather();
  delay(1000);
}
How It Works
01

OpenWeatherMap HTTP Request: HTTPClient fetches the /data/2.5/weather endpoint with the city name, country code, metric units, and API key. The response is a JSON object with nested fields for temperature, humidity, wind, description, and Unix timestamps for sunrise and sunset.

02

ArduinoJson Parsing: StaticJsonDocument<1024> allocates a fixed-size parse buffer on the stack. deserializeJson() parses the HTTP response and populates the document. Individual values are accessed with array-style notation: doc["main"]["temp"].

03

Dual-Page Auto-Cycle: The page variable toggles every 5 seconds using XOR (page^=1). A button press also toggles immediately. The showClock() and showWeather() functions each call oled.clearDisplay() and oled.display() to render their content atomically.

04

Rate-Limited API Refresh: Weather data refreshes every 10 minutes (600,000 ms). The OpenWeatherMap free tier allows 60 calls per minute, so 10-minute intervals use just 144 calls per day, well within the 1000-call daily limit.

Applications
  • Smart home dashboard showing indoor time and outdoor weather
  • Desk accessory for remote workers checking local weather before commuting
  • Reception area display combining a clock and weather conditions
  • Weather station companion device for indoor OLED display
Troubleshooting

fetchWeather() always returns HTTP 401

A 401 error means the API key is invalid or not yet activated. New API keys take up to 2 hours to activate on OpenWeatherMap. Check the key in your account dashboard.

Temperature reads 0 after a successful fetch

The city name or country code is incorrect, returning a valid HTTP 200 with an empty JSON body. Verify by printing the raw HTTP response to Serial and checking the city name spelling.

OLED flickers on page auto-cycle

Move the auto-cycle timer outside the 1-second delay loop. The page changes mid-draw if the timer triggers during rendering. Check the lastSwitch comparison independently of the 1-second delay.

Upgrades
  • Add a BME280 sensor to show both indoor and outdoor temperature side by side
  • Display a weather icon using bitmap_t arrays for sun, cloud, and rain symbols
  • Add a buzzer that chimes on the hour
  • Store the weather city name in NVS and allow changing it via a web config page
FAQ

You need an ESP32 DevKit, TODO: sensor, TODO: output, a breadboard, jumper wires, and a USB cable for power and programming.

Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.

Start with Beginner if you are new to IoT Projects. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.

Overview

The advanced build adds a three-page OLED dashboard (clock, current weather, forecast summary), a local web server serving a full 7-day forecast page with a JavaScript temperature chart, an MQTT sync channel so multiple ESP32 clocks in the same building share one weather fetch, and a Wi-Fi Manager captive portal so you can change Wi-Fi credentials without reflashing.

Components
  • 1× ESP32 DevKit V1
  • 1× SSD1306 OLED 128x64 I2C
  • 1× Tactile push button — Page cycle
  • 1× Wi-Fi router with internet
  • 1× OpenWeatherMap account — Free API key
  • 1× MQTT broker (optional) — For multi-device sync
Wiring
Component PinESP32 PinNotes
OLED SDA/SCLGPIO 21/22
Page buttonGPIO 0Internal pull-up
Arduino Code
esp32-oled-weather-clock_advanced.ino
// ESP32 OLED Weather Clock - Advanced (3-page + web forecast + WiFiManager + MQTT)
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <WiFiManager.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <PubSubClient.h>
#include <WebServer.h>
#include <time.h>

Adafruit_SSD1306 oled(128,64,&Wire,-1);
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
WebServer server(80);

const char* OWM_KEY="YourKey";
const char* CITY="London,GB";
const char* MQTT_HOST="192.168.1.100";
const char* MQTT_TOPIC="weather/shared";

float temp=0,feels=0,humidity=0,wind=0;
String desc="--";
String forecastJson="{}";
int page=0;
unsigned long lastFetch=0;

void fetchAll(){
  HTTPClient http;
  // Current weather
  http.begin(String("http://api.openweathermap.org/data/2.5/weather?q=")+CITY+"&units=metric&appid="+OWM_KEY);
  if(http.GET()==200){
    StaticJsonDocument<1024> doc;
    deserializeJson(doc,http.getString());
    temp=doc["main"]["temp"]; feels=doc["main"]["feels_like"];
    humidity=doc["main"]["humidity"]; wind=doc["wind"]["speed"];
    desc=doc["weather"][0]["description"].as<String>();
    // Share over MQTT
    char buf[256]; serializeJson(doc,buf);
    mqtt.publish(MQTT_TOPIC,buf);
  }
  http.end();
  // 7-day forecast
  http.begin(String("http://api.openweathermap.org/data/2.5/forecast?q=")+CITY+"&cnt=7&units=metric&appid="+OWM_KEY);
  if(http.GET()==200) forecastJson=http.getString();
  http.end();
}

void serveForecast(){
  StaticJsonDocument<2048> doc;
  deserializeJson(doc,forecastJson);
  String body="<h2>7-Day Forecast</h2><canvas id=c width=600 height=200></canvas>"
    "<script>var d=[";
  for(auto& item:doc["list"].as<JsonArray>()){
    body+=String((float)item["main"]["temp"])+",";
  }
  body+="];"
    "var ctx=document.getElementById("c").getContext("2d");"
    "ctx.beginPath();"
    "d.forEach((v,i)=>ctx.lineTo(i*(600/d.length),200-v*3));"
    "ctx.stroke();</script>";
  server.send(200,"text/html",body);
}

void showPage(){
  oled.clearDisplay(); oled.setTextSize(1);
  if(page==0){
    // Clock
    struct tm ti; getLocalTime(&ti);
    oled.setTextSize(2); oled.setCursor(8,5);
    oled.printf("%02d:%02d",ti.tm_hour,ti.tm_min);
    oled.setTextSize(1); oled.setCursor(0,36);
    char buf[24]; strftime(buf,24,"%a %d %b %Y",&ti); oled.println(buf);
  } else if(page==1){
    // Current weather
    oled.printf("%.1fC (feels %.1fC)n",temp,feels);
    oled.printf("Hum:%.0f%% Wind:%.1fm/sn",humidity,wind);
    oled.println(desc.substring(0,21));
  } else {
    // Forecast hint
    oled.println("Forecast at:");
    oled.println(WiFi.localIP().toString());
    oled.println("/forecast");
  }
  oled.display();
}

void setup(){
  Serial.begin(115200);
  Wire.begin(21,22);
  oled.begin(SSD1306_SWITCHCAPVCC,0x3C);
  oled.setTextColor(WHITE);
  oled.clearDisplay(); oled.setCursor(0,0);
  oled.println("Starting..."); oled.display();

  WiFiManager wm;
  wm.autoConnect("ESP32-WeatherClock");

  configTime(0,0,"pool.ntp.org");
  struct tm ti; while(!getLocalTime(&ti)) delay(500);

  mqtt.setServer(MQTT_HOST,1883);
  server.on("/forecast",serveForecast);
  server.begin();

  fetchAll(); lastFetch=millis();
  pinMode(0,INPUT_PULLUP);
}

void loop(){
  server.handleClient();
  if(!mqtt.connected()) mqtt.connect("ESP32Clock");
  mqtt.loop();
  if(digitalRead(0)==LOW){ page=(page+1)%3; delay(200); }
  static unsigned long ls=0;
  if(millis()-ls>5000){ page=(page+1)%3; ls=millis(); }
  if(millis()-lastFetch>600000){ fetchAll(); lastFetch=millis(); }
  showPage();
  delay(1000);
}
How It Works
01

WiFiManager Captive Portal: WiFiManager creates a temporary access point named "ESP32-WeatherClock" if no saved credentials are found. A captive portal web page lets you enter the home Wi-Fi SSID and password from any smartphone without touching the code. Credentials are saved to NVS automatically.

02

Three-Page OLED Cycle: Page 0 shows the NTP clock. Page 1 shows current weather from OpenWeatherMap. Page 2 shows the web forecast URL. Pages cycle every 5 seconds or on button press. The (page+1)%3 modulo arithmetic wraps from page 2 back to page 0.

03

MQTT Weather Sharing: After each API fetch, the current weather JSON is published to weather/shared on the MQTT broker. A second ESP32 clock in the same building can subscribe to this topic and skip its own API fetch, reducing API call count and ensuring display consistency.

04

7-Day Forecast Web Page: The /forecast endpoint parses the stored forecast JSON and renders a JavaScript canvas line chart of the next 7 temperature points. The chart is served as an inline HTML page accessible from any browser on the local network using the IP shown on the OLED page 2.

Applications
  • Smart home dashboard clock synced across multiple rooms
  • Office lobby display with live weather and 7-day forecast
  • Weather station OLED companion showing NTP-accurate time
  • IoT demonstration combining Wi-Fi, REST API, MQTT, and web server
Troubleshooting

WiFiManager portal does not appear after reset

Hold the page button while powering on to force portal mode, or call wm.resetSettings() once in setup() to clear saved credentials and trigger the portal on every boot during development.

Forecast page shows an empty chart

The /data/2.5/forecast endpoint requires a valid city name and API key. Print forecastJson to Serial after fetching to confirm the response is not an error message.

MQTT and web server conflict under load

Move the MQTT loop and weather fetch to a FreeRTOS task on core 0. Run the web server on core 1. This prevents a heavy API response from blocking MQTT keep-alive packets.

Upgrades
  • Add a BME280 sensor and publish indoor temperature to the same MQTT topic
  • Use the OpenWeatherMap One Call 3.0 API for hourly precipitation probability
  • Add a second OLED for a room-by-room indoor vs outdoor temperature comparison
  • Implement deep sleep between display updates for battery-powered operation
FAQ

You need an ESP32 DevKit, TODO: sensor, TODO: output, a breadboard, jumper wires, and a USB cable for power and programming.

Only the Advanced stage uses Wi-Fi. Beginner and Intermediate builds run offline on the ESP32 with USB power.

Start with Beginner if you are new to IoT Projects. Use Intermediate for OLED feedback and Advanced for dashboards or connected monitoring.