Will it rain tomorrow? Do you need a coat now? This little weather station tells you at a glance. Every 15 minutes the ESP32 asks Open-Meteo, a free weather service, for the forecast for your town. The 1602 LCD shows three pages: the weather now (temperature, humidity, wind and a short description), today (lowest and highest temperature and the chance of rain) and tomorrow. The pages change by themselves every five seconds, and the button jumps to the next one.
What you'll learn
- How to ask a web service for data over HTTPS
- How to read numbers out of a JSON answer with ArduinoJson
- How to drive the 1602 LCD from the ESP32
- How to turn weather codes into words
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)
- 1 push button (ESP32 IoT Starter Kit)
- A 2.4 GHz Wi-Fi network
- The printed parts from the 3D printing box below (weather station stand), or the cardboard version described there
- Breadboard and jumper wires (ESP32 IoT Starter Kit)
Wiring

| From | To |
|---|---|
| LCD VSS, RW and K | GND |
| LCD VDD | 5V |
| LCD V0 | Middle pin (W) of the potentiometer |
| LCD RS | ESP32 GPIO 19 |
| LCD E | ESP32 GPIO 23 |
| LCD D4, D5, D6, D7 | ESP32 GPIO 18, 17, 16, 4 |
| LCD D0 to D3 | Not connected |
| LCD A (backlight +) | 220 Ω resistor, then 5V |
| Potentiometer outer pins | 5V and GND |
| Push button, one leg | ESP32 GPIO 27 |
| Push button, the diagonally opposite leg | GND |
The LCD runs on 5 V, but because RW is connected to GND, it only listens to the ESP32 and never sends 5 V back. The ESP32's 3.3 V signals are enough for it to read.
Find your town
Open-Meteo needs your town's position as latitude and longitude. Search for your town on a map service, or on our Open-Meteo page, and note the two numbers, for example 59.44 and 24.75 for Tallinn. Two decimals are plenty.
The sketch
In the Arduino IDE, open Library Manager and install the ArduinoJson library by Benoit Blanchon. LiquidCrystal comes with the Arduino IDE.
Replace YOUR_WIFI_NAME and YOUR_WIFI_PASSWORD with your own network details (2.4 GHz only).
Put your town's name (up to 10 letters) and position into PLACE, LATITUDE and LONGITUDE.
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <LiquidCrystal.h>
const char* WIFI_NAME = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* PLACE = "Tallinn"; // shown on the first page
const char* LATITUDE = "59.44";
const char* LONGITUDE = "24.75";
LiquidCrystal lcd(19, 23, 18, 17, 16, 4); // RS, E, D4, D5, D6, D7
const int BUTTON_PIN = 27;
const byte DEGREE = 223; // the LCD's own degree sign
float nowTemp = 0, wind = 0;
int humidity = 0, nowCode = 0;
float minTemp[2], maxTemp[2];
int rain[2], dayCode[2];
bool haveData = false;
int page = 0;
unsigned long lastFetch = 0, lastPage = 0;
bool lastButton = HIGH;
// Open-Meteo uses the WMO weather codes
const char* describe(int c) {
if (c == 0) return "Clear sky";
if (c <= 2) return "Some clouds";
if (c == 3) return "Cloudy";
if (c <= 48) return "Fog";
if (c <= 57) return "Drizzle";
if (c <= 67) return "Rain";
if (c <= 77) return "Snow";
if (c <= 82) return "Showers";
if (c <= 86) return "Snow showers";
return "Thunderstorm";
}
bool fetchWeather() {
String url = String("https://api.open-meteo.com/v1/forecast?latitude=") + LATITUDE + "&longitude=" + LONGITUDE +
"¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code" +
"&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max" +
"&timezone=auto&forecast_days=2";
WiFiClientSecure client;
client.setInsecure();
HTTPClient http;
http.begin(client, url);
int status = http.GET();
if (status != 200) {
Serial.printf("Open-Meteo answered %d\n", status);
http.end();
return false;
}
JsonDocument doc;
DeserializationError error = deserializeJson(doc, http.getString());
http.end();
if (error) return false;
nowTemp = doc["current"]["temperature_2m"] | 0.0;
humidity = doc["current"]["relative_humidity_2m"] | 0;
wind = doc["current"]["wind_speed_10m"] | 0.0;
nowCode = doc["current"]["weather_code"] | 0;
for (int d = 0; d < 2; d++) {
minTemp[d] = doc["daily"]["temperature_2m_min"][d] | 0.0;
maxTemp[d] = doc["daily"]["temperature_2m_max"][d] | 0.0;
rain[d] = doc["daily"]["precipitation_probability_max"][d] | 0;
dayCode[d] = doc["daily"]["weather_code"][d] | 0;
}
Serial.printf("Now %.1f C, %s\n", nowTemp, describe(nowCode));
return true;
}
void showPage() {
lcd.clear();
if (!haveData) {
lcd.print("Getting weather");
return;
}
if (page == 0) { // now: "Tallinn 12.3°C" / "Cloudy 65% 12"
lcd.print(PLACE);
lcd.setCursor(9, 0);
lcd.print(nowTemp, 1);
lcd.write(DEGREE);
lcd.print("C");
lcd.setCursor(0, 1);
lcd.print(describe(nowCode));
lcd.setCursor(13, 1);
lcd.print(humidity);
lcd.print("%");
} else { // today or tomorrow: "Today 3/12°C" / "Rain 40%"
int d = page - 1;
lcd.print(d == 0 ? "Today" : "Tomorrow");
lcd.setCursor(9, 0);
lcd.print((int)round(minTemp[d]));
lcd.print("/");
lcd.print((int)round(maxTemp[d]));
lcd.write(DEGREE);
lcd.setCursor(0, 1);
lcd.print(describe(dayCode[d]));
lcd.setCursor(13, 1);
lcd.print(rain[d]);
lcd.print("%");
}
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
lcd.begin(16, 2);
lcd.print("Connecting...");
WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) delay(500);
haveData = fetchWeather();
lastFetch = millis();
showPage();
}
void loop() {
if (millis() - lastFetch > 15UL * 60 * 1000) { // new forecast every 15 minutes
lastFetch = millis();
if (fetchWeather()) haveData = true;
showPage();
}
bool button = digitalRead(BUTTON_PIN);
bool next = (button == LOW && lastButton == HIGH) || millis() - lastPage > 5000;
lastButton = button;
if (next) {
page = (page + 1) % 3;
lastPage = millis();
showPage();
delay(30);
}
}
client.setInsecure() keeps the connection encrypted but skips checking the server's certificate. That keeps the sketch short and is fine for a learning project. For a device you leave running for years, load the server's root certificate with client.setCACert() instead.
Read the screen
| Page | Top line | Bottom line |
|---|---|---|
| Now | Your town and the temperature | The weather and the humidity |
| Today | Lowest / highest temperature | The weather and the chance of rain |
| Tomorrow | Lowest / highest temperature | The weather and the chance of rain |
How it works
The ESP32 asks Open-Meteo a question in the form of a web address: the position, which values it wants (current= and daily=) and how many days. The answer is JSON, text with names and values in curly brackets. deserializeJson() turns it into something the sketch can read by name, like doc["current"]["temperature_2m"]. The | 0.0 after each value is a fallback in case it is missing.
Weather services describe the weather with WMO codes: 0 is a clear sky, 61 to 67 rain, 95 and above thunderstorms. describe() turns a code into a word that fits on the LCD.
The 1602 LCD has its own little character set. Number 223 in it is a degree sign, which lcd.write() sends as a single character.
Asking every 15 minutes is plenty: the forecast doesn't change faster, and it is kind to a free service.
Try this next
- Light a red LED when the chance of rain tomorrow is over 50 %: take an umbrella!
- Add a page with the sunrise and sunset times (
daily=sunrise,sunset). - Show the weather for two towns, like yours and a friend's.
Troubleshooting
- The LCD lights up but shows nothing, or only black boxes: turn the potentiometer slowly from one end to the other until the text appears.
- It says "Getting weather" for ever: check the Serial Monitor. A number like -1 means no internet; check the Wi-Fi.
- Strange characters on the LCD: check D4 to D7 on GPIO 18, 17, 16 and 4, in that order.
- The temperature is for the wrong place: check that latitude and longitude aren't swapped.
- 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.
- A printed part is too tight or too loose: printers differ by a few tenths of a millimetre. Sand or file a tight hole, and fix a loose part with a drop of hot glue or a strip of tape.