Build a dial that shows tomorrow's weather

The ESP32 downloads the forecast from the free Open-Meteo service, turns a servo needle to sunny, cloudy, rain, snow or storm on a printed dial, and colours an LED by the temperature.
  • Level:Advanced
  • Duration: 2 h
  • Ages 12+
  • Platform: ESP32

Tools for this lesson

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

Build a dial that shows tomorrow's weather – EEMENTOR STEM™ lesson

Weather forecasts are made by huge computers, and many weather services share them for free. Open-Meteo is one of them: it needs no account and no key, and answers with the forecast for any place on Earth. In this capstone the ESP32 downloads tomorrow's forecast every 30 minutes, reads the weather code from the answer and turns a servo needle to the matching symbol on a printed dial: sunny, partly cloudy, cloudy, rain, snow or storm. An RGB LED glows blue for cold days and red for hot ones, and a button switches between today and tomorrow.

What you'll learn

  • How to call a web API and read its JSON answer with ArduinoJson
  • How weather codes describe the weather with numbers
  • How to map a value onto sections of a dial
  • How to build a finished, useful device from a print, a servo and a web service

What you need

  • ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
  • Servo motor (ESP32 IoT Starter Kit)
  • RGB LED and 3 resistors, 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 (dial, stand and needle), or the cardboard version described there
  • Breadboard and jumper wires (ESP32 IoT Starter Kit)

Wiring

Breadboard wiring diagram: Build a dial that shows tomorrow's weather
Breadboard view. Check each connection against the wiring table below.
From To
Servo orange / yellow wire ESP32 GPIO 13
Servo red wire ESP32 5V
Servo brown / black wire GND
ESP32 GPIO 25 220 Ω resistor, then the RGB LED's red leg
ESP32 GPIO 26 220 Ω resistor, then the RGB LED's green leg
ESP32 GPIO 33 220 Ω resistor, then the RGB LED's blue leg
RGB LED longest leg (common) GND
Push button, one leg ESP32 GPIO 4
Push button, the diagonally opposite leg GND

Find your location

  1. Open a map, such as Google Maps or OpenStreetMap, and find your town.
  2. Right-click on it (or press and hold on a phone). The map shows two numbers, such as 59.437, 24.754. The first is the latitude, the second the longitude.
  3. Put them into LATITUDE and LONGITUDE in the sketch. Two decimal places are enough.

The sketch

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

In the Arduino IDE, open Library Manager and install ArduinoJson (by Benoit Blanchon, version 7) and ESP32Servo (by Kevin Harrington).

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <ESP32Servo.h>

const char* WIFI_NAME = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* LATITUDE = "59.44";          // Tallinn; use your own place
const char* LONGITUDE = "24.75";
const bool TEST_SWEEP = false;          // true: park the needle at 90 degrees for assembly

const int SERVO_PIN = 13;
const int RED_PIN = 25, GREEN_PIN = 26, BLUE_PIN = 33;
const int BUTTON_PIN = 4;
const unsigned long EVERY = 30UL * 60 * 1000;   // new forecast every 30 minutes

// The six sections of the dial, from left to right
const char* const SECTIONS[] = {"Sunny", "Partly cloudy", "Cloudy", "Rain", "Snow", "Storm"};

Servo needle;
int codes[2] = {-1, -1};                // weather code for today and tomorrow
float maxTemp[2];
int showDay = 1;                        // 0 = today, 1 = tomorrow
bool lastButton = HIGH;
unsigned long lastFetch = 0;
bool fetchedOnce = false;

// WMO weather code -> section of the dial (0 to 5)
int sectionFor(int code) {
  if (code <= 1) return 0;                          // clear, mainly clear
  if (code == 2) return 1;                          // partly cloudy
  if (code == 3 || code == 45 || code == 48) return 2;   // overcast, fog
  if (code >= 95) return 5;                         // thunderstorm
  if ((code >= 71 && code <= 77) || code == 85 || code == 86) return 4;   // snow
  return 3;                                         // drizzle, rain, showers
}

void colour(int r, int g, int b) {
  analogWrite(RED_PIN, r);
  analogWrite(GREEN_PIN, g);
  analogWrite(BLUE_PIN, b);
}

// blue below 0 degrees, green around 15, red above 30
void temperatureColour(float t) {
  float x = constrain((t + 5) / 35.0, 0.0, 1.0);
  if (x < 0.5) colour(0, x * 2 * 255, (1 - x * 2) * 255);
  else colour((x - 0.5) * 2 * 255, (1 - (x - 0.5) * 2) * 255, 0);
}

bool fetchForecast() {
  String url = String("https://api.open-meteo.com/v1/forecast?latitude=") + LATITUDE + "&longitude=" + LONGITUDE +
               "&daily=weather_code,temperature_2m_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) {
    Serial.println("Could not read the answer");
    return false;
  }
  for (int day = 0; day < 2; day++) {
    codes[day] = doc["daily"]["weather_code"][day] | -1;
    maxTemp[day] = doc["daily"]["temperature_2m_max"][day] | 0.0;
  }
  return codes[0] >= 0;
}

void showForecast() {
  if (codes[showDay] < 0) return;
  int section = sectionFor(codes[showDay]);
  int angle = 165 - section * 30;        // sections are 30 degrees wide, starting on the left
  needle.write(angle);
  temperatureColour(maxTemp[showDay]);
  Serial.printf("%s: code %d = %s, up to %.1f C\n", showDay ? "Tomorrow" : "Today",
                codes[showDay], SECTIONS[section], maxTemp[showDay]);
}

void setup() {
  Serial.begin(115200);
  pinMode(RED_PIN, OUTPUT);
  pinMode(GREEN_PIN, OUTPUT);
  pinMode(BLUE_PIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  needle.attach(SERVO_PIN);
  needle.write(90);
  if (TEST_SWEEP) {
    Serial.println("Assembly mode: the needle stays at 90 degrees");
    while (true) delay(1000);
  }
  WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
  Serial.print("Connecting to Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println(" connected");
}

void loop() {
  if (!fetchedOnce || millis() - lastFetch >= EVERY) {
    lastFetch = millis();
    if (WiFi.status() == WL_CONNECTED && fetchForecast()) {
      fetchedOnce = true;
      showForecast();
    } else {
      WiFi.reconnect();
      lastFetch = millis() - EVERY + 60000;   // try again in a minute
    }
  }

  bool button = digitalRead(BUTTON_PIN);
  if (button == LOW && lastButton == HIGH) {
    showDay = 1 - showDay;              // switch between today and tomorrow
    showForecast();
    delay(200);
  }
  lastButton = button;
}

Check the forecast

  1. Open the Serial Monitor at 115200 baud. After connecting you see a line such as "Tomorrow: code 61 = Rain, up to 9.4 C".
  2. Compare it with a weather app for your town. Open-Meteo combines several national weather services, so small differences are normal.
  3. Press the button to see today's weather; press it again for tomorrow.
  4. To see the raw answer, paste the address from fetchForecast() into a browser, with your latitude and longitude.

How it works

A web API is a web address made for programs instead of people. The ESP32 asks for two days (forecast_days=2) of two daily values: the weather code and the highest temperature. Open-Meteo answers in JSON, a text format of names and values, such as "weather_code":[3,61]. ArduinoJson turns that text into something the sketch can read with doc["daily"]["weather_code"][1], which is tomorrow's code. The | -1 gives a safe value if something is missing.

The codes come from the World Meteorological Organization (WMO): 0 is a clear sky, 61 is light rain, 95 a thunderstorm and so on. sectionFor() groups the 28 codes into the six symbols on the dial, and each section is 30 degrees wide. Section 0 is on the left, at 165 degrees, because a servo's 0 degrees is usually on the right when you look at it from the front.

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.

Try this next

  • Ask for precipitation_probability_max too and blink the LED when the chance of rain is above 60 %.
  • Add a second servo needle for the temperature, like the dial thermometer.
  • Show a three-day forecast by pressing the button three times.

Troubleshooting

  • "Open-Meteo answered -1": the ESP32 couldn't reach the internet. Check your Wi-Fi's internet access.
  • "Open-Meteo answered 400": the latitude or longitude is wrong. Use a dot, not a comma, as the decimal sign.
  • The needle points at the wrong symbol: remount the needle at 90 degrees (see assembly), or adjust the 165 in showForecast().
  • The board restarts when the servo moves: servos draw bursts of current. Use a good USB port or a powered hub, and check that the servo's red wire is on 5V.
  • 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.

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