In summer the days are long; in winter they are short. But how short, exactly, and how much darker is a cloudy day than a sunny one? This tracker finds out. A photoresistor looks out of the window and the ESP32 measures the light every five minutes. Open its web page on your phone to see a chart of the last 24 hours, the light right now, and how many hours of daylight there were today. Leave it running for a few weeks and you can watch the days get longer or shorter.
What you'll learn
- How a photoresistor turns light into a number
- How to keep the last 24 hours of readings in a ring buffer
- How to send data to the browser as JSON and draw a chart there
- How to work out daylight hours from a list of readings
What you need
- ESP32 board, expansion board and USB cable (ESP32 IoT Starter Kit)
- Photoresistor module (45-in-1 Sensor Kit)
- 3 long female-to-male jumper wires
- A 2.4 GHz Wi-Fi network
- The printed parts from the 3D printing box below (window pod), or the cardboard version described there
- Breadboard and jumper wires (ESP32 IoT Starter Kit)
Wiring

| From | To |
|---|---|
| Photoresistor module S | ESP32 GPIO 34 |
| Photoresistor module + | 3V3 |
| Photoresistor module − | GND |
Sensor modules from different makers can label and order their pins differently. Always follow the labels printed on your module.
GPIO 34 can only read, which is perfect for a sensor. It is one of the analog pins that keep working while Wi-Fi is on.
The sketch
Replace YOUR_WIFI_NAME and YOUR_WIFI_PASSWORD with your own network details (2.4 GHz only).
Change TIME_ZONE to your own time zone. The example is for Central Europe; search for "POSIX time zone" and your city to find yours (for example EET-2EEST,M3.5.0/3,M10.5.0/4 for Estonia or GMT0BST,M3.5.0/1,M10.5.0 for the UK).
#include <WiFi.h>
#include <WebServer.h>
#include <time.h>
const char* WIFI_NAME = "YOUR_WIFI_NAME";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* TIME_ZONE = "CET-1CEST,M3.5.0,M10.5.0/3"; // your time zone
const int LIGHT_PIN = 34;
const bool INVERT = false; // true if the number goes DOWN when it gets brighter
const int DAYLIGHT = 40; // light level (0-100) that counts as daylight
const int SAMPLES = 288; // one every 5 minutes = 24 hours
const unsigned long EVERY = 5UL * 60 * 1000;
WebServer server(80);
byte levels[SAMPLES];
time_t stamps[SAMPLES];
int count = 0; // how many readings are stored
int next = 0; // where the next one goes
unsigned long lastSample = 0;
// Light from 0 (dark) to 100 (bright), the average of 20 readings
int lightLevel() {
long sum = 0;
for (int i = 0; i < 20; i++) {
sum += analogRead(LIGHT_PIN);
delay(2);
}
int level = map(sum / 20, 0, 4095, 0, 100);
return INVERT ? 100 - level : level;
}
void takeSample() {
levels[next] = lightLevel();
stamps[next] = time(nullptr);
next = (next + 1) % SAMPLES; // ring buffer: the oldest reading is overwritten
if (count < SAMPLES) count++;
Serial.printf("Light %d\n", levels[(next + SAMPLES - 1) % SAMPLES]);
}
// Minutes of daylight since midnight
int daylightToday() {
time_t now = time(nullptr);
struct tm today;
localtime_r(&now, &today);
int minutes = 0;
for (int i = 0; i < count; i++) {
struct tm t;
localtime_r(&stamps[i], &t);
if (t.tm_yday == today.tm_yday && levels[i] >= DAYLIGHT) minutes += 5;
}
return minutes;
}
void handleData() {
String json = "{\"now\":" + String(lightLevel()) + ",\"daylight\":" + String(DAYLIGHT) +
",\"today\":" + String(daylightToday()) + ",\"points\":[";
for (int i = 0; i < count; i++) {
int k = (next - count + i + SAMPLES) % SAMPLES; // oldest first
struct tm t;
localtime_r(&stamps[k], &t);
char hm[6];
strftime(hm, sizeof(hm), "%H:%M", &t);
if (i > 0) json += ",";
json += "[\"" + String(hm) + "\"," + String(levels[k]) + "]";
}
json += "]}";
server.send(200, "application/json", json);
}
const char PAGE[] = R"html(<!doctype html><meta name=viewport content='width=device-width'><title>Daylight</title>
<style>body{font-family:sans-serif;max-width:640px;margin:24px auto;padding:0 16px}svg{width:100%;height:220px;background:#F6F8FC;border-radius:12px}</style>
<h1>Daylight</h1><p id=now>Loading...</p><svg id=chart viewBox='0 0 288 100' preserveAspectRatio=none></svg>
<p id=range></p><p id=today></p>
<script>
fetch('/data').then(r => r.json()).then(d => {
const line = 100 - d.daylight;
const pts = d.points.map((p, i) => i + ',' + (100 - p[1])).join(' ');
document.getElementById('chart').innerHTML =
"<line x1=0 x2=288 y1=" + line + " y2=" + line + " stroke='#F2C94C' stroke-dasharray='4'/>" +
"<polyline fill=none stroke='#1B3A7B' stroke-width=1.5 points='" + pts + "'/>";
document.getElementById('now').textContent = 'Light now: ' + d.now + ' of 100';
if (d.points.length) document.getElementById('range').textContent = 'From ' + d.points[0][0] + ' to ' + d.points[d.points.length - 1][0] + '. The dashed line is the daylight level.';
const h = Math.floor(d.today / 60), m = d.today % 60;
document.getElementById('today').textContent = 'Daylight today so far: ' + h + ' h ' + m + ' min';
});
setTimeout(() => location.reload(), 60000);
</script>)html";
void setup() {
Serial.begin(115200);
WiFi.begin(WIFI_NAME, WIFI_PASSWORD);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.print(" connected. Open http://");
Serial.println(WiFi.localIP());
configTzTime(TIME_ZONE, "pool.ntp.org");
Serial.print("Waiting for internet time");
while (time(nullptr) < 1700000000) {
delay(500);
Serial.print(".");
}
Serial.println(" done.");
server.on("/", []() { server.send(200, "text/html", PAGE); });
server.on("/data", handleData);
server.begin();
takeSample();
lastSample = millis();
}
void loop() {
server.handleClient();
if (millis() - lastSample >= EVERY) {
lastSample = millis();
takeSample();
}
}
Try it
Open the Serial Monitor at 115200 baud: after connecting, the ESP32 prints its address, such as http://192.168.1.42. Open it in a browser on a phone or computer in the same Wi-Fi.
- Cover the pod with your hand and watch "Light now" go down when you reload the page. If it goes up instead, set
INVERTtotrueand upload again. - Stick the pod to a window and leave the ESP32 plugged in. A new point appears on the chart every five minutes.
- Look at the chart in the evening. Where the blue line crosses the dashed line is roughly sunrise and sunset. If the room lights count as daylight, raise
DAYLIGHT.
How it works
A photoresistor's resistance drops when light falls on it. On the module it forms a voltage divider with a fixed resistor, so the voltage on S changes with the light, and analogRead() turns it into a number from 0 to 4095. The sketch averages 20 readings and scales them to 0–100.
The readings live in two arrays of 288 places: 288 × 5 minutes is exactly 24 hours. next points to the place for the next reading, and when it reaches the end, it starts again at 0 and overwrites the oldest reading. This is a ring buffer, and it means the ESP32 never runs out of memory, however long it runs.
The ESP32 doesn't draw the chart itself. The page asks /data for the numbers in JSON, a text format that JavaScript understands directly, and the browser turns them into an SVG line. Daylight minutes are simply counted: every reading from today above the daylight level adds five minutes.
Try this next
- Add a second chart line for the room light, with a second photoresistor inside.
- Save each day's daylight minutes in Preferences and show the last 7 days as a bar chart.
- Switch on a lamp with a relay when it gets dark outside.
Troubleshooting
- The light is always 0 or always 100: check S on GPIO 34 and + on 3V3 (not 5V).
- The page says "Loading..." for ever: the ESP32 is still waiting for internet time. Check the Serial Monitor.
-
The times are an hour or two off: set
TIME_ZONEto your own time zone. - 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.