Motion-Activated Night Light for Hallways and Stairs
A WS2812B strip fades up to a warm glow when someone walks by at night and turns itself off a minute later. A photoresistor keeps it from switching on during the day.
The scenario
It's night and you head to the kitchen. As soon as you step into the hallway, a warm glow fades up under the baseboard without blinding you. Once you've passed, it fades out just as smoothly a minute later. During the day the night light ignores you completely.
Parts
- ESP32 DevKit
- WS2812B 5 V strip, 30–60 LEDs
- HC-SR501 PIR sensor or the tiny AM312
- Photoresistor (LDR) + 10 kΩ resistor
- 5 V 2 A power supply, 330 Ω resistor on the data line, 1000 µF capacitor
Wiring
| Part | ESP32 |
|---|---|
| Strip DIN | GPIO 16 via 330 Ω |
| PIR OUT | GPIO 27 |
| Photoresistor | 3V3 → LDR → GPIO 34 → 10 kΩ → GND |
Power the strip and ESP32 from the same 5 V supply with a shared ground (see the WLED project for more on powering strips). Since the night light runs at 20 % brightness, 30 LEDs draw only ~0.4 A.
Library
Adafruit NeoPixel.
Code
#include <Adafruit_NeoPixel.h>
const int LED_PIN = 16;
const int NUM_LEDS = 30;
const int PIR_PIN = 27;
const int LDR_PIN = 34;
const int DARK_THRESHOLD = 1200; // 0..4095: lower is darker
const unsigned long HOLD_MS = 60000; // stay on one minute after the last motion
const uint8_t MAX_LEVEL = 50; // ~20 % brightness — no glare at night
Adafruit_NeoPixel strip(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
bool isOn = false;
int level = 0; // current brightness 0..MAX_LEVEL
unsigned long lastMotion = 0;
unsigned long lastFade = 0;
void render() {
uint8_t l = strip.gamma8(map(level, 0, MAX_LEVEL, 0, 255)) * MAX_LEVEL / 255;
// Warm white ~2200 K
strip.fill(strip.Color(l, l * 45 / 100, l * 12 / 100));
strip.show();
}
void setup() {
Serial.begin(115200);
pinMode(PIR_PIN, INPUT);
strip.begin();
strip.clear();
strip.show();
}
void loop() {
bool motion = digitalRead(PIR_PIN) == HIGH;
// Only switch on in the dark. Once lit, the LDR sees our own light,
// so motion just extends the timer.
if (motion && (isOn || analogRead(LDR_PIN) < DARK_THRESHOLD)) {
isOn = true;
lastMotion = millis();
}
if (isOn && millis() - lastMotion > HOLD_MS) {
isOn = false;
}
// Smooth fades: ~1 s up, ~3 s down
unsigned long step = isOn ? 20 : 60;
if (millis() - lastFade > step) {
lastFade = millis();
int target = isOn ? MAX_LEVEL : 0;
if (level != target) {
level += level < target ? 1 : -1;
render();
}
}
}
Tuning
- Darkness threshold. Temporarily add
Serial.println(analogRead(LDR_PIN));toloop()and check the value in the evening with the lights off. SetDARK_THRESHOLDslightly above it. - Colour. For cooler light raise the blue component:
strip.Color(l, l * 70 / 100, l * 40 / 100). - HC-SR501: turn the time pot to minimum — the timer now lives in code.
Good to know
- Mount the motion sensor at 1–1.5 m or on the ceiling, and the strip under the baseboard or stair treads.
gamma8()makes brightness changes look even to the eye: without gamma correction the first steps would look like a jump.- For stairs: light the LEDs as a wave from bottom to top or top to bottom, depending on which of two PIR sensors fired first.
Ideas
- Flash the strip with WLED and control it from Home Assistant, using the PIR as an automation trigger.
- A "sunrise" in the bedroom: the light slowly brightens 20 minutes before your alarm.