Ein ESP32-PIR-Bewegungsmelder-MQTT-Knoten macht aus einem günstigen Infrarotdetektor eine Ereignisquelle, auf die jedes Dashboard und jede Automation reagieren kann. In diesem Tutorial schließen Sie einen HC-SR501 oder AM312 an, erfassen Bewegung per Interrupt, entprellen unruhige Auslösungen, veröffentlichen Ereignisse an broker.connectmq.com und halten einen Retained-Zeitstempel der letzten Bewegung bereit, der neuen Subscribern genau verrät, wann sich zuletzt etwas bewegt hat.
Wie sich PIR-Sensoren verhalten
Ein passiver Infrarotsensor (PIR) erkennt Änderungen der Infrarotstrahlung in seinem Sichtfeld, typischerweise einen sich bewegenden warmen Körper. Das Modul wandelt das in ein einfaches digitales Signal um: High, solange Bewegung erkannt wird, sonst Low. Für MQTT kommt es auf die Details an, denn der Ausgang liefert kein sauberes Signal mit einem Impuls pro Ereignis.
| Modul | Versorgung | Ausgang | Einstellbar | Hinweise |
|---|---|---|---|---|
| HC-SR501 | 5 V (VIN-Pin) | 3,3 V High | Empfindlichkeit, Haltezeit, Jumper für Auslösemodus | Große Linse, einige Meter Reichweite, braucht Aufwärmzeit |
| AM312 | 2,7 bis 12 V | 3,3 V High | Nichts | Winzig, feste Haltezeit von ein paar Sekunden, gut für 3,3-V-Aufbauten |
| HC-SR505 | 4,5 bis 20 V | 3,3 V High | Nichts | Kompakt, feste Haltezeit von mehreren Sekunden |
Das sollten Sie wissen, bevor Sie Code schreiben:
- Aufwärmphase. Nach dem Einschalten geben PIR-Module bis zu einer Minute lang zufällige Auslösungen aus, während sich der Sensor stabilisiert. Ignorieren Sie Bewegungen in diesem Zeitfenster.
- Haltezeit. Der Ausgang bleibt nach der letzten erkannten Bewegung für eine eingestellte Zeit auf High. Beim HC-SR501 legt ein Potentiometer sie auf einige Sekunden bis mehrere Minuten fest.
- Retrigger-Modus. Steht der Jumper des HC-SR501 auf „H“ (Wiederholung), hält anhaltende Bewegung den Ausgang auf High. Im Modus „L“ fällt der Ausgang nach der Haltezeit ab, auch wenn die Bewegung andauert, und löst dann erneut aus. „H“ ist meist das, was Sie wollen.
- Fehlauslösungen. Sonnenlicht, Heizkörper, Lüftungsschlitze und WLAN-Antennen direkt neben dem Sensor können alle zu Störimpulsen führen.
Verdrahtung
Für den HC-SR501: VCC an den 5V-Pin (VIN) des ESP32, GND an GND, OUT an GPIO 27. Der Ausgang liefert 3,3 V und kann daher direkt an den Eingang des ESP32 angeschlossen werden. Für den AM312: VCC an 3V3, GND an GND, OUT an GPIO 27. Halten Sie den Sensor etwas von der ESP32-Antenne entfernt; eine zu nahe Platzierung ist eine bekannte Ursache für Phantomauslösungen.
Ereignisdesign: Entprellung, Abklingzeit und Topics
Jede Flanke des PIR-Ausgangs zu veröffentlichen, überflutet Subscriber und verschwendet das Nachrichtenkontingent pro Client auf dem öffentlichen MQTT-Broker. Der Sketch macht aus dem Rohsignal zwei saubere Zustände:
- Bewegung erkannt, wenn der Ausgang auf High geht und das vorherige Ereignis älter als eine Abklingzeit ist.
- Bewegung beendet, wenn der Ausgang für eine Ruhephase auf Low war.
| Topic | Retained | Payload |
|---|---|---|
connectmq/demo/pir9d4k/motion |
Ja | {"state":"detected","ts":"2026-10-16T18:03:11Z","count":14} |
connectmq/demo/pir9d4k/motion/last |
Ja | 2026-10-16T18:03:11Z |
connectmq/demo/pir9d4k/status |
Ja | online / offline |
Das Topic motion/last ist die zentrale Idee: Jeder Client, der abonniert – selbst Stunden später –, erhält sofort den Zeitpunkt der letzten Erkennung. Die Zeitstempel stammen von NTP; die Anleitung zum ESP32-NTP-MQTT-Zeitstempel erklärt die hier verwendeten Details der Zeitsynchronisierung.
Der ESP32-PIR-Bewegungsmelder-MQTT-Sketch
#include <WiFi.h>
#include <PubSubClient.h>
#include <time.h>
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* PREFIX = "connectmq/demo/pir9d4k"; // eigene zufällige ID verwenden
const uint8_t PIR_PIN = 27;
const uint32_t WARMUP_MS = 60000; // Auslösungen nach dem Einschalten ignorieren
const uint32_t COOLDOWN_MS = 10000; // Mindestabstand zwischen „detected“-Ereignissen
const uint32_t CLEAR_MS = 30000; // so lange Low -> „clear“
WiFiClient net;
PubSubClient mqtt(net);
String clientId, tMotion, tLast, tStatus;
volatile bool edgeSeen = false;
volatile uint32_t lastEdgeMs = 0;
bool motionActive = false;
uint32_t lastDetectedMs = 0;
uint32_t lastHighMs = 0;
uint32_t motionCount = 0;
void IRAM_ATTR onPirRise() {
edgeSeen = true;
lastEdgeMs = millis();
}
bool isoNow(char* buf, size_t len) {
time_t now = time(nullptr);
if (now < 1700000000) return false; // noch nicht synchronisiert
struct tm utc;
gmtime_r(&now, &utc);
strftime(buf, len, "%Y-%m-%dT%H:%M:%SZ", &utc);
return true;
}
void publishState(const char* state) {
char ts[24] = "unknown";
bool synced = isoNow(ts, sizeof(ts));
char payload[128];
snprintf(payload, sizeof(payload), "{\"state\":\"%s\",\"ts\":\"%s\",\"count\":%lu}",
state, ts, (unsigned long)motionCount);
mqtt.publish(tMotion.c_str(), payload, true);
if (strcmp(state, "detected") == 0 && synced) {
mqtt.publish(tLast.c_str(), ts, true);
}
Serial.println(payload);
}
void connectWifi() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) delay(300);
Serial.printf("Wi-Fi OK, IP %s\n", WiFi.localIP().toString().c_str());
}
void connectMqtt() {
while (!mqtt.connected()) {
if (mqtt.connect(clientId.c_str(), tStatus.c_str(), 1, true, "offline")) {
mqtt.publish(tStatus.c_str(), "online", true);
Serial.println("MQTT connected");
} else {
Serial.printf("MQTT failed, state %d\n", mqtt.state());
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
pinMode(PIR_PIN, INPUT_PULLDOWN);
attachInterrupt(digitalPinToInterrupt(PIR_PIN), onPirRise, RISING);
clientId = "esp32-pir-" + String((uint32_t)ESP.getEfuseMac(), HEX);
tMotion = String(PREFIX) + "/motion";
tLast = String(PREFIX) + "/motion/last";
tStatus = String(PREFIX) + "/status";
connectWifi();
configTime(0, 0, "pool.ntp.org", "time.cloudflare.com");
mqtt.setServer(MQTT_HOST, MQTT_PORT);
connectMqtt();
Serial.println("Warming up PIR sensor...");
}
void loop() {
if (WiFi.status() != WL_CONNECTED) connectWifi();
if (!mqtt.connected()) connectMqtt();
mqtt.loop();
uint32_t now = millis();
bool pinHigh = digitalRead(PIR_PIN) == HIGH;
if (pinHigh) lastHighMs = now;
if (now < WARMUP_MS) { // Rauschen der Aufwärmphase verwerfen
edgeSeen = false;
return;
}
// Steigende Flanke (vom Interrupt erfasst) oder Ausgang noch High -> Bewegung
if (edgeSeen || pinHigh) {
edgeSeen = false;
if (!motionActive && now - lastDetectedMs >= COOLDOWN_MS) {
motionActive = true;
lastDetectedMs = now;
motionCount++;
publishState("detected");
}
}
// Ausgang lange genug Low -> beendet
if (motionActive && !pinHigh && now - lastHighMs >= CLEAR_MS) {
motionActive = false;
publishState("clear");
}
}
Die Interrupt-Service-Routine setzt nur ein Flag und eine Zeit. Veröffentlichen Sie nie aus einem Interrupt heraus: Netzwerkcode ist nicht interruptsicher, und der ESP32 stürzt ab. Das Flag stellt sicher, dass selbst ein sehr kurzer Impuls eines AM312 nicht verloren geht, während mqtt.loop() beschäftigt ist.
ESP32-PIR-Bewegungsmelder-MQTT-Ereignisse testen
Abonnieren Sie Ihr Präfix und winken Sie nach der einminütigen Aufwärmphase vor dem Sensor:
mosquitto_sub -h broker.connectmq.com -p 1883 -i pir-view-9d4k -t "connectmq/demo/pir9d4k/#" -v
Sie sehen einen Zustand detected und etwa 30 Sekunden, nachdem im Raum Ruhe eingekehrt ist, clear. Beenden Sie den Subscriber, warten Sie und starten Sie ihn erneut: Der Retained-Wert motion/last kommt sofort an. Der öffentliche MQTT-Broker ist geteilt, für alle lesbar und für Tests und Prototypen gedacht, verwenden Sie ihn also nicht für ein echtes Zuhause oder Büro. Bewegungsdaten verraten, wann ein Ort leer ist. Die Seite zum öffentlichen MQTT-Broker beschreibt seinen vorgesehenen Einsatzzweck.
Das Timing abstimmen
Drei Konstanten steuern das Verhalten:
- COOLDOWN_MS begrenzt, wie oft
detectedausgelöst werden kann. Zehn Sekunden passen für ein Flurlicht; für ein Sicherheitsprotokoll möchten Sie vielleicht jede neue Anwesenheit nach einer kurzen Lücke erfassen. - CLEAR_MS entscheidet, wann der Raum als leer gilt. Addieren Sie die eigene Haltezeit des Moduls hinzu: Bei einer HC-SR501-Haltezeit von 5 Sekunden und CLEAR_MS von 30 Sekunden kommt
clearetwa 35 Sekunden nach der letzten Bewegung. - WARMUP_MS sollte die Einschwingzeit des Moduls abdecken. Sechzig Sekunden sind für den HC-SR501 sicher.
Drehen Sie das Haltezeit-Potentiometer des HC-SR501 auf Minimum und überlassen Sie das Timing der Firmware. Software-Timing lässt sich leichter aus der Ferne ändern und ist über alle Module hinweg einheitlich.
Auf Bewegungsereignisse reagieren
Der Broker stellt die Ereignisse zu; was damit geschieht, entscheidet etwas anderes. Typische Konsumenten sind Home Assistant mit einem MQTT-Binärsensor, ein Node-RED-Flow, der ein Licht schaltet und eine Benachrichtigung sendet, oder eine Kamera, die bei erkannter Bewegung ein Bild aufnimmt; das ESP32-CAM-MQTT-Projekt ergänzt diesen Sensor auf natürliche Weise. Jeder Konsument abonniert motion als gewöhnlicher MQTT-Client.
Die Retained-Will-Nachricht offline ist für Sicherheitsanwendungen ebenso wichtig. Ein Bewegungsmelder, der stillschweigend ausfällt, sieht genau wie ein ruhiger Raum aus. Das Tutorial zu ESP32 MQTT Last Will erklärt, wie der Broker diese Nachricht in Ihrem Namen veröffentlicht.
Fehlerbehebung
- Ständige Auslösungen. Der Sensor ist zu nah an der ESP32-Antenne oder auf eine Wärmequelle gerichtet. Versetzen Sie ihn, verringern Sie die Empfindlichkeit und prüfen Sie die Logik der Aufwärmphase.
- Überhaupt keine Auslösungen. Prüfen Sie, ob der HC-SR501 mit 5 V statt 3,3 V versorgt wird und ob OUT am konfigurierten Pin hängt.
- Erkannt, aber nie beendet. Retrigger-Modus bei ständiger Bewegung oder eine längere Haltezeit als erwartet. Drehen Sie das Haltezeit-Potentiometer herunter.
- ESP32 startet bei Bewegung neu. Sie erledigen Arbeit im Interrupt-Handler. Beschränken Sie ihn auf das Setzen von Flags.
- Zeitstempel zeigt unknown. NTP hat noch nicht synchronisiert; erlauben Sie ausgehendes UDP 123.
Einrichtung für die Produktion
Für echte Räumlichkeiten verwenden Sie einen authentifizierten, verschlüsselten Broker. Erstellen Sie einen unter app.connectmq.com, wechseln Sie auf WiFiClientSecure an Port 8883 mit dem Zertifikat ISRG Root X1 und verbinden Sie sich mit YOUR_CLIENT_ID, YOUR_USERNAME und YOUR_PASSWORD. Im ConnectMQ-Portal können Sie das Topic motion im Live-Topic-Explorer beobachten, während Sie den Sensor abstimmen.
Wenn Sie mehrere Sensoren betreiben, geben Sie jedem Raum eine eigene Präfix-Ebene, etwa site/floor1/hallway/motion, statt eines flachen Topics. Konsumenten können dann einen Raum abonnieren oder per Wildcard das ganze Stockwerk, und eine Alarmregel mit Schwellwert auf dem Bewegungszähler eines sensiblen Raums liefert Ihnen eine einfache Warnung außerhalb der Geschäftszeiten ohne zusätzlichen Backend-Code. Dasselbe Muster funktioniert in Werkhallen und Lagern, etwa um Beleuchtung und Hallenlüftung in ungenutzten Bereichen für das Energiemonitoring zu erfassen; dort passt der Präfix in die ISA-95-Struktur eines Unified Namespace (UNS), zum Beispiel acme/werk-bremen/lager/halle-2/motion. Bewegungsdaten von Beschäftigten sind dabei personenbezogen im Sinne der DSGVO und gehören nur zweckgebunden und abgestimmt auf einen privaten Broker.
Betreiben Sie Ihre Bewegungsalarme mit ConnectMQ
Bewegungsalarme sind nur nützlich, wenn der Weg vom Sensor zum Smartphone zuverlässig und privat ist. ConnectMQ ist ein Cloud-MQTT-Broker mit TLS, Retained Messages, Last-Will-Unterstützung und einem kostenlosen Serverless-Plan ohne Kreditkarte. Verbinden Sie diesen Knoten, binden Sie Ihr bevorzugtes Automationstool an und finden Sie weitere Geräteprojekte in der Sammlung zum ESP32-IoT-Tutorial.
Häufig gestellte Fragen
Warum löst mein PIR-Sensor direkt nach dem Einschalten aus?
PIR-Module brauchen bis zu einer Minute, um sich zu stabilisieren, und liefern in dieser Zeit zufällige Signale. Ignorieren Sie Auslösungen in einem Aufwärmfenster, wie es der Sketch tut.
Sollten Bewegungsereignisse Retained Messages sein?
Speichern Sie den aktuellen Zustand und den Zeitstempel der letzten Bewegung als retained, damit neue Subscriber sofort Kontext erhalten. Einmalige Benachrichtigungen, die nur einmal ausgelöst werden sollen, speichern Sie nicht.
Kann ich Deep Sleep mit einem PIR-Sensor nutzen?
Ja. Konfigurieren Sie den PIR-Ausgang als ext0-Weckquelle, verbinden Sie sich nach dem Aufwachen mit dem WLAN, veröffentlichen Sie das Ereignis und gehen Sie wieder schlafen. Rechnen Sie mit einigen Sekunden Verzögerung, bis die Nachricht ankommt.
Ist ein Interrupt wirklich nötig?
Bei einem HC-SR501 mit langer Haltezeit reicht Polling aus. Module mit kurzen Impulsen wie der AM312 können verpasst werden, während der Netzwerkcode läuft – das Interrupt-Flag macht die Erkennung zuverlässig.



