Intelligentes Wasserschaden-Erkennungssystem mit Ventilsteuerung
Komponenten und Materialien
| Aluminium-Gehäuse | × | 1 |
| Arduino UNO | × | 1 |
| Arduino Ethernet Shield 2 | × | 1 |
| Netzteil | × | 1 |
| AC-Gehäuse | × | 1 |
| LED-Set mit Vorwiderständen | × | 1 |
| Relais (1 dual 5V und 1 dual 12V) | × | 1 |
| Motorisiertes Ventil | × | 1 |
| JST-Stecker | × | 1 |
| Wassersensor | × | 1 |
Projektübersicht
Während eines Gesprächs mit einem Freund wurde mir bewusst, wie groß das Problem von Wasserschäden ist. Mein Freund musste die gesamte Einrichtung seines Kellers ersetzen, nur weil ein Rohr brach, während er bei der Arbeit war.
Dieses Projekt ähnelt dem, das ich für den Microsoft IoT-Wettbewerb veröffentlicht habe, basiert jedoch auf Arduino statt auf Raspberry Pi. Mein Ansatz ist es, einfache Verantwortlichkeiten an einfache Geräte (wie Arduino) zu delegieren, anstatt viele Aufgaben auf einer zentralen Plattform (Raspberry Pi, DragonBoard oder PC) zu bündeln. Diese Geräte erledigen ihre Kernaufgaben zuverlässig und verbinden sich optional über das Netzwerk mit anderen Geräten für erweiterte Dienste. Bei Netzwerkausfall funktionieren sie weiterhin autonom.
Das System überwacht Wasseraustritte lokal und löst Aktionen wie das Schließen des Hauptwasserzulaufs aus, je nach erkanntem Fehler. Zusätzlich veröffentlicht es Nachrichten an einen MQTT-Broker, sodass das Gerät lokal Wasser verwaltet, aber auch in ein umfassenderes Home-Automation-System eingebunden ist.
Aufbau und Installation
Der Hauptwasserzulauf befindet sich unten im Bild. Das erste Gerät (unten) wird von den städtischen Wasserversorgern zur Druckkontrolle installiert. Das selbstgebaute System (oben) enthält das motorisierte Ventil (blau), das parallel zu einem manuellen Ventil installiert ist. Im Normalbetrieb ist das manuelle Ventil geschlossen; bei Stromausfall kann es manuell geöffnet werden, um die Wasserversorgung aufrechtzuerhalten.
Das Ventil enthält einen 12V-DC-Motor, der je nach Polung im oder gegen den Uhrzeigersinn dreht. Ein Rückmeldeschleife signalisiert, ob das Ventil vollständig geöffnet oder geschlossen ist. Zur Öffnung wird eine positive Spannung an den oberen linken Anschluss gelegt.
Steuerungseinheit
Von links nach rechts: Netzstecker, Reset-Taste, Status-LEDs, Anschlüsse (Sensoren, Motor), Ethernet- und USB-Schnittstelle.
LED-Statusanzeigen
- LED1 (Rot): Dauerhaft = lokales Wasser erkannt, Blinkend = remote erkanntes Wasser, Aus = kein Leck
- LED2 (Gelb): Dauerhaft = Ventilsteuerung fehlerhaft, Blinkend = MQTT-Broker nicht erreichbar, Aus = alles OK
- LED3 (Blau): Dauerhaft = System überwacht aktiv, Blinkend = Ventil geschlossen, Aus = System aus oder stromlos
Der Sensor wird an Orten platziert, an denen Wasseraustritte wahrscheinlich sind.
Wichtige Sicherheitshinweise
ACHTUNG! Das verwendete AC/DC-Netzteil hat zwei Ausgänge: 12V DC für das motorisierte Ventil (gesteuert über zwei Relais) und exakt 5V DC für den Arduino. Daher wird der Arduino direkt am 5V-Kreis versorgt, nicht über den Vin-Pin (der mindestens 6V benötigt). Sobald das AC/DC-Netzteil angeschlossen ist, darf niemals der Arduino DC-Anschluss oder das USB-Kabel angeschlossen werden. Zum Debuggen über USB muss ein spezielles Kabel ohne Stromleitungen (nur Datenleitungen) verwendet werden. Die Verbindung zwischen AC-Gehäuse und Netzteil führt 110V – niemals anfassen!
Programmcode
Wassererkennung und Ventilsteuerung (Arduino)
#include <PubSubClient.h>
#include <Network.h>
#include <Timer.h>
#include <SPI.h>
#include <Ethernet.h>
#include <Dns.h>
#include <avr/wdt.h>
/* Hardware-Übersicht:
- Hauptgehäuse: Arduino Uno mit Ethernet Shield
- Rote LED: Wasser lokal (dauerhaft) / remote (blinkend)
- Gelbe LED: Ventilfehler (dauerhaft) / MQTT unerreichbar (blinkend)
- Blaue LED: System OK (dauerhaft) / Ventil geschlossen (blinkend)
- Taster: Selbsttest beim Drücken
- Dual-Relais für motorisiertes Ventil
- Dual-Relais für Endschalter des Ventils
- Wasserdetektoren (alle parallel an 3 Frontanschlüssen)
Ventilanschlüsse:
- Gelb/Blau: Motorstromversorgung
- Schwarz: Endschalter-Eingang (GND)
- Rot: GND bei geschlossener Position
- Grün: GND bei geöffneter Position
(Hinweis: Endschalter halten nach Abschaltung nicht zwingend Kontakt)
*/
// Netzwerk-Konfiguration
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xCF, 0xFC, 0xEE };
IPAddress ip(192, 168, 12, 215);
IPAddress server(192, 168, 12, 130);
EthernetClient ethClient;
PubSubClient client(ethClient);
#define mqttClientPrefix "GLX"
#define mqttClientLocation "BASEMENT"
#define mqttClientUID "001"
#define mqttClientStatusTopic "Status"
#define mqttClientFaultTopic "Fault"
const int mqttInterval = 20;
int mqttIntervalCnt = 0;
int isConnectedToBroker = -1;
// Pin-Belegung
const int SystemLedPin = A0;
const int FaultLedPin = A1;
const int AlarmLedPin = A2;
const int WaterDetectorPin = 2;
const int ToggleButtonPin = 3;
const int SdCardPin = 4;
const int ValveClosedPin = 5;
const int ValveOpenedPin = 6;
const int ValveControl1 = 8;
const int ValveControl2 = 7;
// Hinweis: D10-D13 sind für Ethernet Shield reserviert
// Statusvariablen
int isWaterDetected = 0;
int isWaterDetectedRemotely = 0;
int isValveClosed = -1;
const int valveTimeOut = 15;
int isConnectedToValve = -1;
volatile boolean isResetRequested = 0;
const int mainLoopDelay = 500;
void(* resetFunc) (void) = 0;
void setup() {
wdt_disable();
Serial.begin(9600);
Serial.println(F("Setup beginnt"));
// Hardware-Initialisierung
pinMode(SystemLedPin, OUTPUT);
pinMode(FaultLedPin, OUTPUT);
pinMode(AlarmLedPin, OUTPUT);
pinMode(WaterDetectorPin, INPUT);
pinMode(ToggleButtonPin, INPUT);
pinMode(ValveOpenedPin, INPUT);
pinMode(ValveClosedPin, INPUT);
pinMode(ValveControl1, OUTPUT); digitalWrite(ValveControl1, HIGH);
pinMode(ValveControl2, OUTPUT); digitalWrite(ValveControl2, HIGH);
pinMode(SdCardPin, OUTPUT); digitalWrite(SdCardPin, HIGH);
testLeds();
// Netzwerk & MQTT
client.setServer(server, 1883);
client.setCallback(MQTTBrokerCallback);
Ethernet.begin(mac, ip);
Serial.print(F("Aktuelle IP: ")); Serial.print(Ethernet.localIP());
Serial.print(F(" - MQTT Broker IP: ")); Serial.println(server);
// Ventil initial öffnen
if (openValve() == 0) {
Serial.println(F("Ventil offen, Überwachung aktiv"));
subscribeToRemoteWaterSensors();
} else {
Serial.println(F("Ventil nicht öffnen. Bypass nutzen."));
}
enableInterruptOnResetButton();
delay(1500);
Serial.println(F("Setup beendet"));
}
void loop() {
configureLedsWithInitialStates();
// Reset-Button prüfen
if (isResetRequested == 1) {
Serial.println(F("Reset angefordert"));
publishStatus();
wdt_enable(WDTO_1S);
delay(5000);
}
// Wassersensor lesen
readLocalWaterSensor();
if (isWaterDetected == 1 || isWaterDetectedRemotely == 1) {
if (isValveClosed == 0) closeValve();
}
// MQTT-Veröffentlichung
if (mqttIntervalCnt == 0) {
if (isWaterDetected == 1) publishFault();
publishStatus();
mqttIntervalCnt = mqttInterval;
} else {
if (isConnectedToValve == 0) {
Serial.println(F("System defekt - Ventil nicht steuerbar"));
} else {
Serial.print(F("."));
}
mqttIntervalCnt--;
}
delay(mainLoopDelay / 2);
client.loop();
configureLedsWithFinalStates();
delay(mainLoopDelay / 2);
}
// Lokale Wassersensor-Verwaltung
void readLocalWaterSensor() {
isWaterDetected = !getDebouncedValue(WaterDetectorPin, 100, 10);
Serial.print(isWaterDetected);
}
// Reset-Button-Verwaltung
void enableInterruptOnResetButton() {
isResetRequested = 0;
attachInterrupt(1, onResetRequested, CHANGE);
}
void onResetRequested() {
detachInterrupt(1);
isResetRequested = 1;
}
// Ventil öffnen
int openValve() {
Serial.print(F("Öffne Ventil..."));
setupRelays(1); // Kurz schließen, um Endschalter zu prüfen
if (waitForEndOfCycle(ValveClosedPin) == 0) {
setupRelays(2); // Öffnen
if (waitForEndOfCycle(ValveOpenedPin) == 0) {
isConnectedToValve = 1;
isValveClosed = 0;
setupRelays(0);
Serial.println(F(""));
return 0;
}
}
setupRelays(0);
isConnectedToValve = 0;
return -1;
}
// Ventil schließen
int closeValve() {
Serial.print(F("Schließe Ventil..."));
setupRelays(2); // Kurz öffnen, um Endschalter zu prüfen
if (waitForEndOfCycle(ValveOpenedPin) == 0) {
setupRelays(1); // Schließen
if (waitForEndOfCycle(ValveClosedPin) == 0) {
isConnectedToValve = 1;
isValveClosed = 1;
setupRelays(0);
Serial.println(F("Ventil geschlossen. Alle Räume prüfen und Sensoren trocknen."));
return 0;
}
}
setupRelays(0);
isConnectedToValve = 0;
return -1;
}
// Relais-Steuerung
void setupRelays(int scenario) {
switch (scenario) {
case 0: // Alle AUS
digitalWrite(ValveControl1, HIGH);
digitalWrite(ValveControl2, HIGH);
break;
case 1: // Schließen
digitalWrite(ValveControl1, HIGH);
digitalWrite(ValveControl2, LOW);
break;
case 2: // Öffnen
digitalWrite(ValveControl1, LOW);
digitalWrite(ValveControl2, HIGH);
break;
default:
Serial.print(F("Unbekanntes Relais-Szenario: ")); Serial.println(scenario);
digitalWrite(ValveControl1, HIGH);
digitalWrite(ValveControl2, HIGH);
}
}
// Auf Endschalter warten
int waitForEndOfCycle(int limitSwitchPin) {
int cnt = valveTimeOut;
while (cnt > 0) {
if (getDebouncedValue(limitSwitchPin, 10, 10) == LOW) return 0;
cnt--;
Serial.print(F("."));
delay(1000);
}
Serial.println(F(" - Timeout beim Ventil. Stromversorgung und Kabel prüfen."));
return -1;
}
// Entprellung
int getDebouncedValue(int inputPin, int intervalInMs, int requiredConfirmations) {
int confirmations = 1;
int currentValue = digitalRead(inputPin);
while (confirmations <= requiredConfirmations) {
delay(intervalInMs);
if (currentValue == digitalRead(inputPin)) confirmations++;
else { confirmations = 1; currentValue = digitalRead(inputPin); }
}
return currentValue;
}
// LED-Steuerung
void configureLedsWithInitialStates() {
clearLeds();
if (isWaterDetectedRemotely == 1 || isWaterDetected == 1) digitalWrite(AlarmLedPin, HIGH);
if (isConnectedToValve == 0 || isConnectedToBroker == 0) digitalWrite(FaultLedPin, HIGH);
digitalWrite(SystemLedPin, HIGH);
}
void configureLedsWithFinalStates() {
if (isWaterDetectedRemotely == 1) digitalWrite(AlarmLedPin, LOW);
if (isConnectedToBroker == 0) digitalWrite(FaultLedPin, LOW);
if (isValveClosed == 1) digitalWrite(SystemLedPin, LOW);
}
void clearLeds() {
digitalWrite(AlarmLedPin, LOW);
digitalWrite(FaultLedPin, LOW);
digitalWrite(SystemLedPin, LOW);
}
void testLeds() {
clearLeds();
digitalWrite(AlarmLedPin, HIGH); delay(500);
digitalWrite(FaultLedPin, HIGH); delay(500);
digitalWrite(SystemLedPin, HIGH); delay(500);
clearLeds();
}
// MQTT-Funktionen
void MQTTBrokerCallback(char* topic, byte* payload, unsigned int length) {
Serial.print(F("MQTT-Nachricht. Topic=")); Serial.print(topic);
String payloadStr = (char*)payload;
String realPayload = payloadStr.substring(0, length);
Serial.print(F(", Inhalt=")); Serial.print(realPayload);
if (realPayload.indexOf("WaterDetected") > 0 && realPayload.indexOf(mqttClientLocation) == -1) {
isWaterDetectedRemotely = 1;
}
Serial.println();
}
String buildClientIdentifier() {
String data = mqttClientPrefix; data += "_"; data += mqttClientLocation; data += "_"; data += mqttClientUID;
return data;
}
String buildDeviceStatusTopic() {
String data = mqttClientPrefix; data += "/"; data += mqttClientLocation; data += "/"; data += mqttClientUID; data += "/"; data += mqttClientStatusTopic;
return data;
}
String buildFaultTopic() {
String data = mqttClientPrefix; data += "/"; data += mqttClientLocation; data += "/"; data += mqttClientUID; data += "/"; data += mqttClientFaultTopic;
return data;
}
String buildAnyFaultTopic() {
String data = mqttClientPrefix; data += "/+"; data += "/+"; data += "/"; data += mqttClientFaultTopic;
return data;
}
String buildDeviceStatusJson() {
String data = "{"; data += "\n";
data += "\"ResetByOperator\": "; data += (int)isResetRequested; data += ",\n";
data += "\"WaterDetected\": "; data += (int)isWaterDetected; data += ",\n";
data += "\"ValveClosed\": "; data += (int)isValveClosed; data += ",\n";
data += "\"ValveDetected\": "; data += (int)isConnectedToValve; data += "\n";
data += "}";
return data;
}
String buildFaultJson() {
String data = "{"; data += "\n";
data += "\"WaterDetected\": "; data += (int)isWaterDetected; data += ",\n";
data += "\"Location\": "; data += mqttClientLocation; data += "\n";
data += "}";
return data;
}
void publishMsg(char (&topic)[200], char (&payload)[200]) {
if (connectToBroker()) {
if (client.publish(topic, payload)) {
isConnectedToBroker = 1;
Serial.print(F("Gesendet an Topic: ")); Serial.println(topic);
} else {
isConnectedToBroker = 0;
Serial.print(F("NICHT gesendet: ")); Serial.println(topic);
}
client.loop();
}
}
void publishFault() {
char topicBuffer[200]; buildFaultTopic().toCharArray(topicBuffer, 200);
char payloadBuffer[200]; buildFaultJson().toCharArray(payloadBuffer, 200);
publishMsg(topicBuffer, payloadBuffer);
}
void publishStatus() {
char topicBuffer[200]; buildDeviceStatusTopic().toCharArray(topicBuffer, 200);
char payloadBuffer[200]; buildDeviceStatusJson().toCharArray(payloadBuffer, 200);
publishMsg(topicBuffer, payloadBuffer);
}
void subscribeToRemoteWaterSensors() {
if (connectToBroker()) {
char tempBuffer[200]; buildAnyFaultTopic().toCharArray(tempBuffer, 200);
if (client.subscribe(tempBuffer)) {
isConnectedToBroker = 1;
Serial.print(F("Abonniert: ")); Serial.println(tempBuffer);
} else {
isConnectedToBroker = 0;
Serial.println(F("Abonnement fehlgeschlagen"));
}
client.loop();
} else {
isConnectedToBroker = 0;
Serial.println(F("Keine Verbindung zu Broker"));
}
}
int connectToBroker() {
Serial.println(F(""));
Serial.print(F("Verbinde mit MQTT Broker... "));
char tempBuffer[200]; buildClientIdentifier().toCharArray(tempBuffer, 200);
if (client.connect(tempBuffer)) {
Serial.print(F("verbunden als ")); Serial.println(tempBuffer);
} else {
switch (client.state()) {
case -4: Serial.println(F("TIMEOUT")); break;
case -3: Serial.println(F("VERBINDUNG VERLOREN")); break;
case -2: Serial.println(F("NETZWERKFEHLER")); break;
case -1: Serial.println(F("GETRENNT")); break;
case 1: Serial.println(F("PROTOKOLLFEHLER")); break;
case 2: Serial.println(F("CLIENT ID ABGELEHNT")); break;
case 3: Serial.println(F("SERVER NICHT VERFÜGBAR")); break;
case 4: Serial.println(F("ANMELDEFEHLER")); break;
case 5: Serial.println(F("NICHT AUTORISIERT")); break;
default: Serial.print(F("Fehler: ")); Serial.println(client.state());
}
}
return client.connected();
}
MQTT-Bibliothek
Die verwendete PubSubClient-Bibliothek ist im Code enthalten. Falls benötigt, kann sie von GitHub heruntergeladen werden.
Schaltplan
Herstellungsprozess
- Quetschventile & Peristaltische Pumpen: Goldstandard für abrasive Kalkschlamm-Systeme
- Raspberry Pi Sensor & Aktuator Steuerung meistern: Beschleunigungsmesser, Servo und Datenstreaming
- Automatisiertes Pool-Befüllungs- und Überwachungssystem
- Debugging des Arduino Portenta H7 Dual-Core-Systems mit Segger J-Link
- Universelle Android-Fernbedienung mit Arduino, 1Sheeld & IR-Steuerung
- LCD-Animationen & Spiele mit Arduino UNO erstellen
- Smartes Sparschwein: Münzprüfer mit Arduino Nano steuern
- Arduino Power Control Center: N-FET, P-FET, Relais & RTC Kit
- Arduino Audio-Frequenz-Detektor – Lauteste Schallpeaks mit hochsensiblem Modul messen
- LED per Bluetooth mit Arduino steuern – Einfache DIY-Anleitung