A poor man's motion detector for the garage (HC-SR501 + ESPHome)

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 onoff 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 on for 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.