Ein Raspberry Pi Abstandssensor MQTT Projekt mit dem HC-SR04 löst zwei häufige Aufgaben mit einem einzigen günstigen Bauteil: Wie voll ist ein Tank, und ist etwas (oder jemand) da? In diesem Tutorial schließen Sie den Ultraschallsensor sicher an, erfassen mit gpiozero stabile Messwerte in Python und veröffentlichen sowohl einen Füllstand in Prozent als auch Anwesenheits-Events per MQTT, damit ein Dashboard oder eine Automation darauf reagieren kann.
So misst der HC-SR04 Abstände
Der HC-SR04 sendet einen kurzen 40-kHz-Ultraschallimpuls, sobald sein TRIG-Pin einen Puls von 10 Mikrosekunden erhält, und hält dann seinen ECHO-Pin auf High, bis die Reflexion zurückkommt. Die Länge dieses Echopulses, multipliziert mit der Schallgeschwindigkeit und geteilt durch zwei, ergibt den Abstand. Der Sensor arbeitet von wenigen Zentimetern bis zu einigen Metern, am besten gegen flache, harte Oberflächen im rechten Winkel, etwa eine Wasseroberfläche oder eine Wand.
Für reale Projekte sind zwei Folgen wichtig. Erstens muss der Pi den Echopuls in Software messen, daher fügt das Linux-Scheduling etwas Jitter hinzu; den filtern Sie heraus. Zweitens ändert sich die Schallgeschwindigkeit mit der Lufttemperatur, rechnen Sie also mit einer kleinen Drift zwischen einem kalten Morgen und einem heißen Nachmittag. Für einen Füllstands- oder Anwesenheitsmelder sind beide Effekte beherrschbar.
Verdrahtung mit Spannungsteiler
Der HC-SR04 läuft mit 5 V, und auch sein ECHO-Ausgang liefert 5 V, was einen GPIO-Pin des Raspberry Pi beschädigen kann. Verwenden Sie an ECHO immer einen Spannungsteiler:
| HC-SR04-Pin | Anschluss |
|---|---|
| VCC | Pi 5 V (physischer Pin 2) |
| GND | Pi GND (physischer Pin 6) |
| TRIG | GPIO23 (physischer Pin 16), direkt |
| ECHO | 1-k-Widerstand zu GPIO24 (physischer Pin 18); 2-k-Widerstand von GPIO24 nach GND |
Das Paar aus 1 k und 2 k teilt 5 V auf etwa 3,3 V herunter. Eine Kombination aus 1 k und 1,8 k oder 2,2 k funktioniert ebenfalls. TRIG kann direkt angeschlossen werden, weil der 3,3-V-Ausgang des Pi ausreicht, um den Sensor auszulösen. Wenn Sie die 3,3-V-kompatible Variante des Moduls kaufen, prüfen Sie deren Dokumentation; möglicherweise brauchen Sie den Teiler dann nicht.
Die Software installieren
sudo apt update
sudo apt install -y python3-venv python3-lgpio
mkdir -p ~/distance-mqtt && cd ~/distance-mqtt
python3 -m venv --system-site-packages .venv
source .venv/bin/activate
pip install "paho-mqtt>=2.0" gpiozero
Schnelltest ohne MQTT:
python -c "from gpiozero import DistanceSensor; from time import sleep; s = DistanceSensor(echo=24, trigger=23, max_distance=4); sleep(1); print(round(s.distance * 100, 1), 'cm')"
Richten Sie den Sensor auf eine Wand und vergleichen Sie das Ergebnis mit einem Maßband. Ein Messwert, der immer exakt dem Wert von max_distance entspricht, bedeutet, dass kein Echo erkannt wird – meist ein Verdrahtungsproblem an ECHO.
Zwei Modi: Füllstand und Anwesenheit
Derselbe Messwert lässt sich auf zwei Arten interpretieren, und das Skript veröffentlicht beide:
| Modus | Berechnung | Topic | Veröffentlichung | Retained |
|---|---|---|---|---|
| Tankfüllstand | (empty_distance - distance) / (empty_distance - full_distance) |
.../level |
Alle 30 s | Ja |
| Anwesenheit | Abstand unter einem Schwellwert, mit Hysterese | .../presence |
Nur bei Änderung | Ja |
| Rohwert | Median-Abstand in cm | in beiden Payloads |
Da beide Topics retained sind, zeigt ein jederzeit geöffnetes Dashboard sofort den aktuellen Füllstand und ob jemand anwesend ist. Anwesenheit nur bei Änderung zu veröffentlichen hält den Datenverkehr winzig und macht Automationen einfach: abonnieren und auf jede Nachricht reagieren.
Den Tank kalibrieren
Montieren Sie den Sensor oben am Tank, senkrecht nach unten gerichtet, mit Abstand zu Zulauf und Wänden. Messen Sie dann zwei Abstände:
EMPTY_CM: Sensor bis zum Boden (oder bis zum niedrigsten nutzbaren Pegel).FULL_CM: Sensor bis zum Wasser an der Voll-Markierung. Halten Sie zwischen Sensorfläche und höchstem Wasserstand mindestens den Mindestmessabstand des Sensors von einigen Zentimetern ein.
Setzen Sie beide Werte als Umgebungsvariablen. Das Skript begrenzt das Ergebnis auf 0 bis 100 Prozent, damit Spritzer oder ein leerer Tank keine unmöglichen Werte erzeugen.
Das Raspberry Pi Abstandssensor MQTT Skript
Speichern Sie es als distance_mqtt.py:
#!/usr/bin/env python3
"""HC-SR04 tank level and presence detection over MQTT."""
import json
import os
import statistics
import time
import uuid
import paho.mqtt.client as mqtt
from gpiozero import DistanceSensor
BROKER = os.getenv("MQTT_HOST", "broker.connectmq.com")
PORT = int(os.getenv("MQTT_PORT", "1883"))
USERNAME = os.getenv("MQTT_USERNAME")
PASSWORD = os.getenv("MQTT_PASSWORD")
CLIENT_ID = os.getenv("MQTT_CLIENT_ID", f"distance-{uuid.uuid4().hex[:8]}")
PREFIX = os.getenv("MQTT_PREFIX", "connectmq/demo/9c3d55/tank1")
EMPTY_CM = float(os.getenv("EMPTY_CM", "120"))
FULL_CM = float(os.getenv("FULL_CM", "20"))
PRESENCE_ON_CM = float(os.getenv("PRESENCE_ON_CM", "60"))
PRESENCE_OFF_CM = float(os.getenv("PRESENCE_OFF_CM", "80"))
LEVEL_INTERVAL = 30
SAMPLES = 7
sensor = DistanceSensor(echo=24, trigger=23, max_distance=4, queue_len=5)
def measure_cm():
"""Median of several readings, ignoring out-of-range values."""
values = []
for _ in range(SAMPLES):
d = sensor.distance * 100
if 2 < d < 399: # 400 cm bedeutet: kein Echo innerhalb von max_distance
values.append(d)
time.sleep(0.06) # Echos zwischen den Pings abklingen lassen
return statistics.median(values) if values else None
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code.is_failure:
print(f"Connection refused: {reason_code}")
return
print(f"Connected as {CLIENT_ID}")
client.publish(f"{PREFIX}/status", "online", qos=1, retain=True)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID)
client.on_connect = on_connect
client.will_set(f"{PREFIX}/status", "offline", qos=1, retain=True)
client.reconnect_delay_set(min_delay=1, max_delay=60)
if USERNAME:
client.username_pw_set(USERNAME, PASSWORD)
if PORT == 8883:
client.tls_set()
client.connect(BROKER, PORT, keepalive=30)
client.loop_start()
present = None
last_level = 0.0
try:
while True:
distance = measure_cm()
now = time.time()
if distance is None:
print("No valid echo")
time.sleep(1)
continue
# Anwesenheit mit Hysterese: unter ON einschalten, über OFF ausschalten.
new_present = present
if distance < PRESENCE_ON_CM:
new_present = True
elif distance > PRESENCE_OFF_CM:
new_present = False
if new_present is not None and new_present != present:
present = new_present
payload = {"present": present, "distance_cm": round(distance, 1), "ts": int(now)}
client.publish(f"{PREFIX}/presence", json.dumps(payload), qos=1, retain=True)
print("presence", payload)
if now - last_level >= LEVEL_INTERVAL:
span = EMPTY_CM - FULL_CM
percent = max(0.0, min(100.0, (EMPTY_CM - distance) / span * 100))
payload = {"level_pct": round(percent, 1), "distance_cm": round(distance, 1), "ts": int(now)}
client.publish(f"{PREFIX}/level", json.dumps(payload), qos=1, retain=True)
print("level", payload)
last_level = now
time.sleep(0.5)
except KeyboardInterrupt:
pass
finally:
client.publish(f"{PREFIX}/status", "offline", qos=1, retain=True).wait_for_publish(2)
client.loop_stop()
client.disconnect()
sensor.close()
In einem echten Einsatz verwenden Sie meist einen Modus pro Sensor: Ein Tanksensor braucht keine Anwesenheits-Events, und ein Sensor an einer Tür braucht keinen Füllstand. Beide in einem Skript zu haben erleichtert das Experimentieren, und Sie können den nicht benötigten Block einfach löschen.
Warum Medianfilter und Hysterese wichtig sind
Ultraschall-Messwerte springen gelegentlich: ein Streuecho von der Tankwand, eine Welle auf dem Wasser oder ein Timing-Aussetzer unter Linux. Beim Mitteln zieht ein einziger falscher Wert das Ergebnis mit; der Median aus sieben Messwerten ignoriert ihn einfach. Die Hysterese löst ein anderes Problem. Ohne sie würde eine Person genau an der Schwelle die Anwesenheit mehrmals pro Sekunde ein- und ausschalten und Abonnenten mit Events fluten. Mit getrennten Ein- und Ausschaltabständen ändert sich der Zustand einmal und bleibt stabil.
Die Ausgabe testen
mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-9c3d55 \
-t 'connectmq/demo/9c3d55/tank1/#' -v
Bewegen Sie Ihre Hand auf den Sensor zu: Eine presence-Nachricht mit "present": true erscheint, wenn Sie 60 cm unterschreiten, und false, sobald Sie sich über 80 cm entfernen. Alle 30 Sekunden meldet eine level-Nachricht den Füllstand in Prozent auf Basis Ihrer Kalibrierung. Denken Sie daran, dass der öffentliche MQTT-Broker für alle lesbar ist; verwenden Sie daher ein zufälliges Präfix und halten Sie echte Standortnamen aus Test-Topics heraus.
Automatisieren mit den Daten
Liegen Füllstand und Anwesenheit auf MQTT, wird Automation zu einer Frage des Subscribers statt des Sensors:
- Pumpensteuerung: Ein zweites Skript oder ein Node-RED-Flow abonniert
levelund schaltet über die Steuerung aus Raspberry Pi Relais per MQTT eine Pumpe ein, wenn der Füllstand unter 20 Prozent fällt, und über 90 Prozent wieder aus. - Alarme: Auf einem privaten ConnectMQ-Broker warnt Sie eine Schwellwert-Alarmregel auf
level_pctvor einem fast leeren Tank, wie unter MQTT-Schwellwert-Alarme beschrieben. - Hausautomation: Home Assistant kann das Retained-Topic
presenceals Belegungssensor lesen und Licht einschalten.
Brauchen Sie einen batteriebetriebenen Füllstandssensor an einem entlegenen Tank ohne Netzstrom, nutzt der Aufbau ESP32-Wassertank-Füllstandssensor denselben Payload mit Deep Sleep, sodass beide dasselbe Dashboard speisen können.
Fehlerbehebung
| Problem | Wahrscheinliche Ursache und Lösung |
|---|---|
| Misst immer 400 cm | Kein Echo erkannt; Teiler und ECHO-Verdrahtung prüfen |
| Messwerte springen stark | Sensor sieht die Tankwand; mittig und mit Abstand zu den Rändern montieren |
| Messwerte driften im Tagesverlauf | Schallgeschwindigkeit ändert sich mit der Temperatur; neu kalibrieren oder Temperaturkompensation ergänzen |
| Kondensationsfehler im Tank | Feuchtigkeit auf den Wandlern; wasserdichtes Ultraschallmodul verwenden |
GPIO busy |
Ein anderes Skript nutzt GPIO23 oder GPIO24 |
Einrichtung für den Produktivbetrieb
Für einen Tank auf einem Hof, in einer Werkstatt oder einem Gebäude wechseln Sie vom öffentlichen MQTT-Broker auf einen privaten. ConnectMQ ist ein verwalteter MQTT-Broker mit TLS auf Port 8883 und generierten Zugangsdaten. Setzen Sie MQTT_PORT=8883, MQTT_USERNAME=YOUR_USERNAME, MQTT_PASSWORD=YOUR_PASSWORD, MQTT_CLIENT_ID=YOUR_CLIENT_ID und ein aussagekräftiges Präfix wie farm1/water/tank1 als Umgebungsvariablen und betreiben Sie das Skript als systemd-Dienst. Das Raspberry Pi MQTT Tutorial enthält die Vorlage für die Dienstdatei und erklärt das hier verwendete Reconnect-Verhalten.
Jetzt mit ConnectMQ starten
Legen Sie Ihre Füllstands- und Anwesenheitsdaten auf einen Cloud-MQTT-Broker, den jedes Dashboard und jede Automation erreicht. Registrieren Sie sich bei ConnectMQ für den kostenlosen Tarif Serverless (Free) ohne Kreditkarte, erstellen Sie einen Broker und verbinden Sie Ihren Pi in wenigen Minuten über TLS.
Häufig gestellte Fragen
Brauche ich für den HC-SR04 wirklich einen Spannungsteiler?
Ja, beim Standardmodul mit 5 V. Sein ECHO-Pin gibt 5 V aus, und die GPIO-Pins des Raspberry Pi sind nur für 3,3 V ausgelegt.
Wie genau ist der HC-SR04 am Raspberry Pi?
Unter guten Bedingungen auf etwa einen Zentimeter genau, sobald die Messwerte mit einem Medianfilter bereinigt sind. Software-Timing unter Linux und Temperaturänderungen sorgen für etwas Streuung.
Kann der HC-SR04 den Wasserstand in einem geschlossenen Tank messen?
Ja, solange er senkrecht auf die Oberfläche zeigt und trocken bleibt. In feuchten Tanks hält ein wasserdichtes Ultraschallmodul länger.
Warum wird die Anwesenheit nur bei Änderung veröffentlicht?
Das hält den Datenverkehr minimal und macht Automationen einfach. Da die Nachricht retained ist, erhalten neue Abonnenten trotzdem sofort den aktuellen Zustand.



