Cheap HC-SR501 PIR sensors are great, worked well with the Micropython, but since I ported code to ESPHome, mine kept firing on nothing, probably a fly in front of the lens, sunlight flicker, or plain electrical noise or something. A single spurious on → off pulse every so often was enough to trigger my garage lights all night.
Hardware trick first: on the HC-SR501 there are two potentiometers.
- Time delay (right pot): turn it all the way to the minimum (fully left/counter-clockwise). This makes the sensor’s output pulse as short as possible instead of latching
onfor many seconds. - Sensitivity (left pot): I left mine around the 8 o’clock position, not maxed out, to avoid picking up every tiny disturbance.
With the time delay at minimum, a real event (like a car pulling into the garage) produces a rapid train of on → off → on → off → on... pulses, roughly 4–6 seconds apart, for as long as there’s actual motion. A false trigger (bug, glitch, thermal noise) produces exactly one isolated on → off pulse and then nothing.
Software trick: instead of trying to debounce this with filters alone, I used a tiny bit of logic in ESPHome: ignore the first pulse always, and only report motion to Home Assistant once a second pulse arrives within a 6-second window of the previous one. As long as pulses keep arriving within that window, the reported “motion” stays on; once they stop, it turns off after a short grace period.
Working config parts below:
yaml
# --- MOTION SENSOR ---
globals:
- id: pir_last_edge_time
type: uint32_t
restore_value: no
initial_value: '0'
script:
- id: motion_off_timer
mode: restart
then:
- delay: 7s
- binary_sensor.template.publish:
id: confirmed_motion
state: OFF
binary_sensor:
- platform: gpio
pin:
number: GPIO22
mode: INPUT_PULLDOWN
name: "Garage PIR Motion (raw)"
id: raw_pir
internal: true # not exposed to Home Assistant, used for logic only
filters:
# reject sub-200ms electrical "clicks" on boot / noise
- delayed_on: 200ms
on_press:
then:
- lambda: |-
uint32_t now = millis();
uint32_t elapsed = now - id(pir_last_edge_time);
if (elapsed <= 6000) {
// second ON within 5-6s of the previous one -> real motion
id(confirmed_motion).publish_state(true);
id(motion_off_timer).execute();
ESP_LOGD("pir", "Motion confirmed, %u ms since previous pulse", (unsigned int) elapsed);
} else {
ESP_LOGD("pir", "First/isolated pulse, ignoring");
}
id(pir_last_edge_time) = now;
- platform: template
name: "Garage PIR Motion"
id: confirmed_motion
device_class: motion
icon: "mdi:motion-sensor"
# --- OPTIONAL DIAGNOSTIC SENSORS ---
sensor:
- platform: wifi_signal
name: "Motion sensor Wi-Fi Signal"
unit_of_measurement: "dBm"
update_interval: 60s
icon: "mdi:wifi"
Tune the 6000 ms window and the 7s timeout to match your own PIR’s timing pot and how long you expect real motion events to keep re-triggering it. Worked well for me and I am sharing in case it saves someone else a few false alarms.
I have created a few years ago Autodesk Fusion design for 3D-printer. Two versions, another for ESP32-Wroom-32U with external antenna (as in the picture) and another for ESP32-Wroom-32D with internal antenna. Use and modify freely.

