Pendahuluan

Setelah Home Assistant (#21), ESPHome (#22), dan Node-RED (#23), sekarang kita tambahkan sensor gerak PIR — komponen klasik lampu koridor, garasi, dan ruang penyimpanan.

Artikel ini fokus pada firmware ESP32: baca PIR lewat interrupt, terapkan debounce agar tidak spam event, dan hold time (hysteresis) supaya lampu tidak berkedip saat orang diam di ruangan. Status gerak + lampu dipublish ke broker Mosquitto pribadi (#16) — siap ditampilkan di HA atau Node-RED.

Prasyarat: Sudah paham MQTT dasar (#7), kontrol relay MQTT (#8), broker Mosquitto + auth (#16) jalan, dan opsional sudah integrasi Home Assistant (#21) atau Node-RED (#23).

Yang Kamu Butuhkan

  • ESP32 DevKit
  • Modul PIR HC-SR501 (atau setara) — output digital HIGH saat gerak
  • Modul relay 1 channel (sama seperti #8)
  • Lampu latihan kecil (LED / lampu desk) — bukan AC 220V tanpa pengalaman
  • Arduino IDE + library PubSubClient, ArduinoJson

Estimasi biaya: Modul PIR HC-SR501 ~Rp 10–20 rb + relay yang sudah dipakai di proyek sebelumnya.

Mengapa Debounce & Hold Time?

MasalahPenyebabSolusi di artikel ini
Lampu kedip-kedip cepatPIR memicu berkali-kali dalam milidetikDebounce 50 ms setelah interrupt
Lampu mati saat orang diamPIR sudah LOW padahal orang masih di ruanganHold time (hysteresis) 60 detik sejak gerak terakhir
Event MQTT berlebihanPublish di setiap ping ISRPublish hanya saat transisi ON/OFF lampu

Arsitektur: PIR → ESP32 → Mosquitto → Smart Home

  [ HC-SR501 PIR ]
      |  GPIO interrupt (RISING)
      v
  [ ESP32 ]
      |  relay GPIO 26 → lampu
      |  publish: kodingindonesia/esp32/pir/gerak  (JSON)
      |  subscribe: kodingindonesia/esp32/lampu/kontrol  (ON/OFF/AUTO)
      v
  [ Mosquitto #16 ]
      |
      +-- Home Assistant (#21) — binary_sensor + switch
      +-- Node-RED (#23) — automasi visual

Topic MQTT (konsisten Seri 1):

  • Gerak: kodingindonesia/esp32/pir/gerak — JSON {"gerak":true,"lampu":"ON"} / {"gerak":false,"lampu":"OFF"}
  • Kontrol manual: kodingindonesia/esp32/lampu/kontrolON / OFF / AUTO (sama #8/#9)

Wiring PIR + Relay

  • PIR VCC → 5V · GND → GND · OUT → GPIO 27
  • Relay IN → GPIO 26 · VCC → 5V · GND → GND
  • Potensiometer delay & sensitivitas di modul HC-SR501 — atur di hardware (lihat troubleshooting). Mode retrigger (H) disarankan agar output tetap HIGH selama ada gerak.

GPIO aman: Hindari GPIO 6–11 (flash). GPIO 27 cocok untuk input PIR (modul HC-SR501 output 3.3V/5V). Pin input-only murni di ESP32: GPIO 34–39. Relay tetap di GPIO 26 seperti seri sebelumnya.

Kode Lengkap: Interrupt + Debounce + Hold Time

Ganti WiFi, IP broker, dan password MQTT (sesuai #16):

#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

const char* ssid     = "NamaWiFiKamu";
const char* password = "PasswordWiFiKamu";

const char* mqttServer   = "192.168.1.50";
const int   mqttPort     = 1883;
const char* mqttUser     = "kindo_esp32";
const char* mqttPass     = "GANTI_PASSWORD_MQTT";

const char* topicPir      = "kodingindonesia/esp32/pir/gerak";
const char* topicKontrol  = "kodingindonesia/esp32/lampu/kontrol";

#define PIR_PIN   27
#define RELAY_PIN 26

const bool RELAY_ON  = LOW;
const bool RELAY_OFF = HIGH;

const unsigned long DEBOUNCE_MS = 50;
const unsigned long HOLD_MS     = 60000; // lampu tetap nyala 60 detik setelah gerak terakhir

volatile bool pirFlag = false;
unsigned long lastIsrMs = 0;
unsigned long lastMotionMs = 0;

bool lampuMenyala = false;
bool otomasiAktif = true;

WiFiClient espClient;
PubSubClient mqttClient(espClient);

void publishStatus() {
  bool gerakAktif = digitalRead(PIR_PIN) == HIGH;
  StaticJsonDocument<96> doc;
  doc["gerak"] = gerakAktif;
  doc["lampu"] = lampuMenyala ? "ON" : "OFF";

  char buffer[96];
  serializeJson(doc, buffer);

  if (mqttClient.publish(topicPir, buffer)) {
    Serial.print("Publish ");
    Serial.println(buffer);
  }
}

void setLampu(bool nyala) {
  if (lampuMenyala == nyala) return;
  lampuMenyala = nyala;
  digitalWrite(RELAY_PIN, nyala ? RELAY_ON : RELAY_OFF);
  publishStatus();
  Serial.println(nyala ? "Lampu: ON" : "Lampu: OFF");
}

void IRAM_ATTR pirISR() {
  pirFlag = true;
}

void callbackMQTT(char* topic, byte* payload, unsigned int length) {
  String pesan;
  for (unsigned int i = 0; i < length; i++) {
    pesan += (char)payload[i];
  }
  pesan.trim();
  pesan.toUpperCase();

  if (pesan == "ON") {
    otomasiAktif = false;
    setLampu(true);
  } else if (pesan == "OFF") {
    otomasiAktif = false;
    setLampu(false);
  } else if (pesan == "AUTO") {
    otomasiAktif = true;
    Serial.println("Mode otomasi PIR aktif kembali");
  }
}

void koneksiWiFi() {
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi OK");
}

void koneksiMQTT() {
  mqttClient.setServer(mqttServer, mqttPort);
  mqttClient.setCallback(callbackMQTT);
  mqttClient.setBufferSize(256);

  while (!mqttClient.connected()) {
    if (mqttClient.connect("ESP32-PIR", mqttUser, mqttPass)) {
      mqttClient.subscribe(topicKontrol);
      Serial.println("MQTT OK");
    } else {
      delay(5000);
    }
  }
}

void handlePirEvent() {
  unsigned long now = millis();
  if (now - lastIsrMs < DEBOUNCE_MS) {
    // Gerak berulang cepat — tetap perpanjang hold time jika lampu sudah nyala
    if (otomasiAktif && lampuMenyala) {
      lastMotionMs = now;
    }
    return;
  }
  lastIsrMs = now;
  lastMotionMs = now;

  if (otomasiAktif && !lampuMenyala) {
    setLampu(true);
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(RELAY_PIN, OUTPUT);
  pinMode(PIR_PIN, INPUT);
  digitalWrite(RELAY_PIN, RELAY_OFF);

  attachInterrupt(digitalPinToInterrupt(PIR_PIN), pirISR, RISING);

  koneksiWiFi();
  koneksiMQTT();
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) koneksiWiFi();
  if (!mqttClient.connected()) koneksiMQTT();
  mqttClient.loop();

  if (pirFlag) {
    pirFlag = false;
    handlePirEvent();
  }

  if (otomasiAktif && lampuMenyala) {
    if (digitalRead(PIR_PIN) == HIGH) {
      lastMotionMs = millis(); // pin masih HIGH — perpanjang hold tanpa interrupt baru
    } else if (millis() - lastMotionMs > HOLD_MS) {
      setLampu(false);
    }
  }
}

Penjelasan Bagian Kritis

  1. IRAM_ATTR pirISR() — ISR sependek mungkin: hanya set flag. Jangan delay() atau MQTT di interrupt.
  2. DebounceDEBOUNCE_MS dicek di handlePirEvent(), bukan di ISR.
  3. Hold time (hysteresis)lastMotionMs diperbarui tiap gerak valid; lampu mati jika sudah HOLD_MS tanpa gerak baru. Saat lampu sudah nyala, gerak berulang (meski kena debounce) tetap memperpanjang lastMotionMs.
  4. Override manual — MQTT ON/OFF mematikan otomasi sementara; kirim AUTO untuk kembali ke mode PIR.
  5. Publish hematsetLampu() hanya publish saat status lampu benar-benar berubah; field gerak di JSON dibaca langsung dari pin PIR (digitalRead), bukan disamakan dengan status lampu.
  6. mqttClient.loop() — wajib dipanggil di loop() agar perintah MQTT manual diterima (sama seperti #8).
  7. Polling pin PIR di loop() — selama digitalRead(PIR_PIN) masih HIGH, lastMotionMs diperbarui. Ini mencegah lampu mati prematur saat orang diam tapi sensor masih mendeteksi gerak (mode retrigger HC-SR501).

Pro tip: Untuk ruang ramai (koridor sekolah), naikkan HOLD_MS ke 2–5 menit. Untuk ruang kecil, 30–60 detik biasanya cukup.

Integrasi Home Assistant (#21)

Tambahkan ke configuration.yaml (sesuaikan broker):

mqtt:
  binary_sensor:
    - name: "ESP32 PIR Gerak"
      unique_id: esp32_pir_gerak_kindo
      state_topic: "kodingindonesia/esp32/pir/gerak"
      value_template: "{{ value_json.gerak }}"
      payload_on: true
      payload_off: false
      device_class: motion

Switch lampu bisa pakai entitas yang sama seperti #21 — topic lampu/kontrol tidak berubah.

Automasi contoh — nyalakan lampu saat gerak terdeteksi (Settings → Automations → Edit in YAML):

alias: Lampu koridor nyala saat PIR
trigger:
  - platform: mqtt
    topic: "kodingindonesia/esp32/pir/gerak"
condition:
  - condition: template
    value_template: "{{ trigger.payload_json.gerak == true }}"
action:
  - service: switch.turn_on
    target:
      entity_id: switch.lampu_esp32_relay

Untuk matikan otomatis, andalkan HOLD_MS di firmware — atau tambah automasi HA dengan for: beberapa menit tanpa gerak.

Integrasi Node-RED (#23)

  1. mqtt in — topic kodingindonesia/esp32/pir/gerak
  2. jsonfunction — filter gerak aktif:
    if (msg.payload.gerak === true) {
        return msg;
    }
    return null;
  3. Sambungkan ke notifikasi, ui_text, atau mqtt out — pola wiring sama Langkah 4–6 di #23

Uji Coba (Checklist)

  1. Sketch mengarah ke broker pribadi Mosquitto (#16) — bukan test.mosquitto.org
  2. Upload sketch → Serial Monitor 115200 — WiFi & MQTT harus OK
  3. Tunggu warm-up PIR ~30–60 detik setelah power on (HC-SR501 stabil)
  4. Gerakkan tangan di depan PIR — lampu nyala + log publish JSON
  5. Diam 60 detik — lampu mati otomatis (sesuai HOLD_MS)
  6. Verifikasi dari terminal:
    mosquitto_sub -h 192.168.1.50 -p 1883 \
      -u kindo_esp32 -P 'PASSWORD_ANDA' \
      -t "kodingindonesia/esp32/pir/gerak" -v
  7. Override manual:
    mosquitto_pub -h 192.168.1.50 -p 1883 \
      -u kindo_esp32 -P 'PASSWORD_ANDA' \
      -t "kodingindonesia/esp32/lampu/kontrol" -m "ON"
    mosquitto_pub -h 192.168.1.50 -p 1883 \
      -u kindo_esp32 -P 'PASSWORD_ANDA' \
      -t "kodingindonesia/esp32/lampu/kontrol" -m "OFF"
  8. Kirim AUTO untuk aktifkan mode PIR lagi:
    mosquitto_pub -h 192.168.1.50 -p 1883 \
      -u kindo_esp32 -P 'PASSWORD_ANDA' \
      -t "kodingindonesia/esp32/lampu/kontrol" -m "AUTO"
  9. Gerak berulang saat lampu nyala — hold time harus reset (lampu tidak mati prematur)

Tips & Troubleshooting

  • PIR selalu HIGH: Atur potensiometer sensitivitas & delay di modul; jauhkan dari AC / angin panas langsung
  • Tidak ada interrupt: Cek wiring OUT ke GPIO 27; HC-SR501 butuh warm-up ~30–60 detik setelah power on
  • Lampu flicker: Naikkan DEBOUNCE_MS atau HOLD_MS; jangan publish di ISR
  • MQTT tidak connect: ESP32 harus ke broker pribadi (#16) — bukan test.mosquitto.org
  • Relay tidak klik: Cek active LOW/HIGH — sama troubleshooting #8
  • WiFi 2.4 GHz: ESP32 tidak support jaringan 5 GHz saja

Keamanan & Produksi

  • Jangan hardcode password MQTT di sketch yang di-share — gunakan build flag atau NVS (#12)
  • Sensor gerak + lampu di area publik — pertimbangkan notifikasi HA, bukan hanya lampu lokal
  • MQTT over internet → wajib TLS (#17)

Langkah Selanjutnya (Seri 2)

  • Artikel #17: MQTT TLS — amankan Mosquitto
  • Artikel #18: Simpan histori event PIR ke MySQL via subscriber Python
  • Artikel #34: NTP — timestamp akurat di log gerak
  • Capstone greenhouse (#39) — PIR + sensor + pompa relay

Sensor PIR melengkapi Jalur C smart home: dari dashboard HA/Node-RED hingga automasi gerak di firmware ESP32. Lanjutkan di halaman artikel Koding Indonesia.