← All projects
Lighting Beginner

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.

Difficulty
Beginner
Time
1 h
Category
Lighting

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)); to loop() and check the value in the evening with the lights off. Set DARK_THRESHOLD slightly 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.