Ich will schon seit über einem Jahrzehnt den Wasserstand unserer Zisterne im Blick haben, um immerhin ein bisschen vorgewarnt zu sein, wenn die 3000 Liter im Sommer dann auf einmal aufgebraucht sind. Und natürlich auch, weil ich hiermit wieder ein kleines Bastelprojekt im Auge hatte.

Keine Methode, über die ich früher gestolpert war, erschien mir wirklich langfristig funktional. Sei es ein Ultraschall-Sensor, der den Abstand Wasserlinie/Deckel misst (und auf Grund der Feuchtigkeit wahrscheinlich schnell hinüber ist) oder einer Messung des Widerstandes zweier Eletroden.

Dann bin ich auf den TL136 Drucksensor aufmerksam gemacht worden, der extra dazu gemacht ist, in Flüssigkeiten versenkt zu werden.
Zusammen mit einem WLAN-fähigen ESP-Chip funke ich so via MQTT den Wasserstand an meinen Server.

Ich fand im Internet leider keine vollständige Anleitung für dieses Projekt. Entweder fehlten Schaltpläne und man musste sich die Schaltung zusammenreimen oder es fehlte der C-Code, den man auf den ESP-Chip flasht (wenn man ESPhome nicht nutzen möchte).
Deshalb hier mal meine Anleitung, die ich mir zusammengebastelt habe.

Projekt-Eckdaten

  • Wasserstandsmessung mit TL136 Sensor
  • Übergabe der Daten per WLAN/MQTT
  • Etwa-Wasserstand in % (einige Projekte messen/berechnen minütlich den Wasserstand in Litern...ich will nur wissen, ob die Zisterne voll oder so gut wie leer ist)
  • Betrieben mit einer kleinen Solarpanele, da ich nicht extra noch ein Stromkabel hinlegen möchte und auch keine Rund-um-die-Uhr-Meldung des aktuellen Wasserstands benötige, der sich ja nur bei Regen oder beim Gießen ändert (Hab' ich nicht hinbekommen)
  • Kür: Noch 4 kleine LED dran, damit man den Wasserstand auch offline sehen kann
  • Kür: Da noch Platz war: Ein DHT22 Temperatursensor mit verbaut, damit ich nun auch die Temperatur im Schopf sehen kann, wo ich die Elektronik untergebracht habe

Hardware

Verwendete Komponenten:

Optional:

  • Ich habe mir noch ein paar LEDs besorgt, um direkt vor Ort einen Füllstand zu sehen
  • Zum Messen von Temperatur und Luftfeuchte ein DHT22. Achtung: Es gibt zum selben Zweck einen DHT11, der misst aber nur volle Grade ohne Nachkommastelle. Außerdem gibt es diese Sensoren mit 3 oder 4 PINs. Bei jenen mit 4 PINs braucht's dann noch einen Widerstand dazu.

Schaltplan (ohne LEDs und Temperatursensor):

Wichtige Infos:

  • Beim Voltage Converter alle Jumper entfernen
  • Die Verbindung zwischen VOUT des "Voltage converters" und A0 des ADS1115 (grün) vorerst weg lassen, da wir erst noch die Ausgangsspannung kalibrieren müssen und der ADS1115 kaputt geht, wenn mehr als 3.3V rein kommen
  • Der TL136-Sensor muss auch erstmal noch nicht angeschlossen werden
  • Die Verbindung des ADS1115 von ADDR an die 3.3V des ESP (blau) ist nicht zwingend erforderlich. Man kann mehrere ADS1115 nebeneinander verwenden, indem man den ADDR-PIN mal mit VDD, mal mit GND und mal gar nicht verbindet. Später im Code muss allerdings berücksichtigt werden, mit was der ADDR-PIN verbunden ist! Ich habe in meinem Beispiel ADDR mit 3.3V/VDD verbunden
  • SCL und SDA des ADS1115 hängen bei mir am ESP D1+D2, da jene hier die SCL+SDA-Pins sind (siehe Pinout-Grafik unter "Software") Dies können je nach Modell auch andere Pins sein...man kann wahrscheinlich auch andere PINs verwenden, wenn man jene nachher im Code korrekt definiert

Erste Schritte:

Zwischen GND und VCC des Voltage Converters müssen 24V anliegen, da der TL136-Sensor mit 24V arbeitet. Dazu alle Komponenten verbinden (bis auf VOUT Voltage converter/A0 ADS1115 und den TL136-Sensor), den ESP an eine Stromquelle anhängen und mit einem Messgerät wie auf dem Bild zu sehen messen.
Am vor-geschaltenen LM2596 (nicht an dem Voltage Converter, wo wir gerade messen) die kleine Schraube drehen, bis 24V gemessen werden.
 
Nun müssen noch der obere und der untere Messbereich eingestellt werden.
Dazu benötigen wir eine maximal gefüllte Zisterne!

Optimalerweise sollten 0V aus dem Voltage Converter raus kommen, wenn die Zisterne leer ist und 3.3V, wenn sie voll ist, um das komplette Spektrum abzudecken, welches der A/V-Wandler ADS1115 verträgt.

Dazu den TL136 nun anschließen, aber nicht ins Wasser tauchen und das Schräubchen bei "Zero" so weit drehen, dass wir an den rechts gezeigten Messpunkten 0 V anliegen haben.

TL136 in die maximal gefüllte Zisterne legen und am "Span"-Schräubchen drehen, bis 3.3V anliegen.
Leider war die TL136-Version, die ich gekauft hatte, wohl zu überdimensioniert, so dass ich auch bei maximaler Füllung nur auf 1.3V komme.
Das ist nicht tragisch, aber reduziert Genauigkeit/Auflösung meiner Messung. Nur mehr als 3.3V sollten es nie werden.
Immerhin bruzelt mir so der ADS1115 nicht durch, sollte es mal Hochwasser geben und das Grundstück 2 Meter unter Wasser stehen.

Nun können wir auch den ADS1115 (grüne Linie) verbinden.

Software

An verschiedenen Stellen im Code muss man angeben, was man an welchen PIN des ESP angeschlossen hat. Darüber bin ich ziemlich arg gestolpert, weil der auf der Platine als D1 bezeichnete Anschluss im Code nicht zwingend als "Pin #1" bezeichnet werden muss.

Als ich mal auf den Trichter kam, hatte ich für meinen ESP im Internet einen Pinout-Plan wie etwa rechts abgebildet gefunden (Achtung: Anderer ESP, anderer Pinout-Plan erforderlich), der mir z.B. sagt, dass wenn ich etwas an D1 anschließe, im Code den Pin#5 (GPIO 5) verwenden muss.
(Für mich als Dummie) dämlich, aber ist so.

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <Wire.h>
#include <ADS1115_WE.h>

// WLAN
const char *ssid = "<SSID des WLAN>";
const char *password = "<WLAN-Passwort>";

// mqtt
const char *mqtt_broker = "<IP des MQTT-Servers>";
const char *mqtt_username = "<MQTT-Benutzername>";
const char *mqtt_password = "<MQTT-Passwort>";
const int mqtt_port = 1883;

// ADS1115
#define I2C_ADDRESS 0x49 // VDD+ADR miteinander verbunden
const int SDA_PIN = 4; // D1 - Standardwert, Zeile kann ggf auch weg gelassen werden
const int SCL_PIN = 5; // D2 - Standardwert, Zeile kann ggf auch weg gelassen werden
const float v_max = 1.25; // Bei voller Zisterne waren es bei mir nur 1.25 V. Perfekt wären 3.3V gewesen

WiFiClient espClient;
PubSubClient mqtt_client(espClient);
WiFiUDP ntpUDP;

NTPClient timeClient(ntpUDP, "<IP meiner Fritzbox, die als NPT fungieren kann>", 7200, 60000);
ADS1115_WE adc = ADS1115_WE(I2C_ADDRESS);

void connectToWiFi();

void connectToMQTTBroker();

void setup() {
  Serial.begin(115200);
  Wire.begin(SDA_PIN, SCL_PIN); // SDA_PIN, SCL_PIN können ggf. auch weg gelassen werden, wenn Standard-SDA/SCL-PINs verwendet
  connectToWiFi();
  mqtt_client.setServer(mqtt_broker, mqtt_port);
  connectToMQTTBroker();
  timeClient.begin();
  if(!adc.init()){
    Serial.println("ADS1115 not connected!");
    while (1);
  }
  // Konfiguration ADS1115
  adc.setVoltageRange_mV(ADS1115_RANGE_4096);   // Spannungsbereich +/-4.096 V
  //adc.setVoltageRange_mV(ADS1115_RANGE_2048); // Spannungsbereich +/-2.048 V - wird von mir verwendet, da bei mir nur max. 1.25 V gemessen werden
  adc.setCompareChannels(ADS1115_COMP_0_GND);   // Voltage Converter an A0 des ADS1115 angeschlossen. Für andere Anschlüsse 1/2/3 ADS1115_COMP_<1/2/3>_GND verwenden
  //adc.setMeasureMode(ADS1115_SINGLE);         // Messung jedes Mal explizit anfordern - Zu Testzwecken
  //adc.setMeasureMode(ADS1115_CONTINUOUS);     // Der Chip misst im Hintergrund ununterbrochen weiter - Zu Testzwecken
}

void connectToWiFi() {
    WiFi.begin(ssid, password);
    Serial.print("Connecting to WiFi");
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println("\nConnected to the WiFi network");
}

void connectToMQTTBroker() {
    while (!mqtt_client.connected()) {
        String client_id = "esp8266-zisterne";
        Serial.printf("Connecting to MQTT Broker as %s.....\n", client_id.c_str());
        if (mqtt_client.connect(client_id.c_str(), mqtt_username, mqtt_password)) {
            Serial.println("Connected to MQTT broker");
        } else {
            Serial.print("Failed to connect to MQTT broker, rc=");
            Serial.print(mqtt_client.state());
            Serial.println(" try again in 5 seconds");
            delay(5000);
        }
    }
}

int getFuellstand() {
  adc.startSingleMeasurement();
  // Warte kurz, bis die Wandlung fertig ist
  while(adc.isBusy()){
    delay(1);
  }
  //int16_t raw = adc.getRawResult(); // Zu Testzwecken
  float voltage = adc.getResult_V();
  char str[10];
  sprintf(str, "%f", voltage);
  mqtt_client.publish("zisterne/voltage", str, true);
  int fuellstand = round(100 * voltage / v_max);
  //Serial.print("Spannung: ");
  //Serial.println(voltage);
  //Serial.print("Fuellstand: ");
  //Serial.print(fuellstand);
  //Serial.println(" %");
  return fuellstand;
}

void sendTimestamp() {
  timeClient.update();  
  char str[20];
  sprintf(str, "%u", timeClient.getEpochTime());
  //Serial.println(timeClient.getEpochTime());
  mqtt_client.publish("zisterne/lastUpdate", str, true);
}

void sendFuellstand() {
  int fuellstand = getFuellstand();
  char str[20];
  sprintf(str, "%i", fuellstand);
  mqtt_client.publish("zisterne/fuellstand", str, true);
}

void loop() {
  if (!mqtt_client.connected()) {
    connectToMQTTBroker();
    }
  sendTimestamp();
  sendFuellstand();
  delay(60000);
}

Ich sende an meinen MQTT-Server sowohl den Füllstand in % an den Kanal zisterne/fuellstand, wie auch die gemessene Spannung zisterne/voltage (um den Füllstand evtl am Server auszurechnen, sollte sich mal rausstellen, dass die Berechnung im C-Code auf dem ESP nicht so ganz hinhaut) und nen Timestamp der Messung zisterne/lastUpdate wofür ich im Code einen NTP-Server anspreche, der mir beim Starten die aktuelle Uhrzeit aus dem Internet/Intranet holt.

Die Kür

Hier noch der Schaltplan mit LEDs und Temperatur-/Luftfeuchtigkeitssensor:

Bei den LEDs ist zu beachten, dass ich welche verwendet habe, die bereits einen Widerstand integriert haben (z.B. diese hier).
Verwendet man "blanke LEDs", benötigt es auch noch 4 Widerstände.

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <Wire.h>
#include <ADS1115_WE.h>
#include <DHT.h>

// WLAN
const char *ssid = "<SSID des WLAN>";
const char *password = "<WLAN-Passwort>";

// mqtt
const char *mqtt_broker = "<IP des MQTT-Servers>";
const char *mqtt_username = "<MQTT-Benutzername>";
const char *mqtt_password = "<MQTT-Passwort>";
const int mqtt_port = 1883;

// Temperatursensor
// Info: D4 ist irgendwie mit der internen LED des ESP verbunden. Blinkt bei jeder Messung
#define DHTPIN 2 // => D4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

// ADS1115
#define I2C_ADDRESS 0x49 // VDD+ADR miteinander verbunden
const int SDA_PIN = 4; // D1 - Standardwert, Zeile kann ggf auch weg gelassen werden
const int SCL_PIN = 5; // D2 - Standardwert, Zeile kann ggf auch weg gelassen werden
const float v_max = 1.25; // Bei voller Zisterne waren es bei mir nur 1.25 V. Perfekt wären 3.3V gewesen

// LEDs 1-4 an D5-D8, was GPIO 14/12/13/15 entspricht
#define LED_D5 14 // D5
#define LED_D6 12 // D6
#define LED_D7 13 // D7
#define LED_D8 15 // D8

WiFiClient espClient;
PubSubClient mqtt_client(espClient);
WiFiUDP ntpUDP;

NTPClient timeClient(ntpUDP, "<IP meiner Fritzbox, die als NPT fungieren kann>", 7200, 60000);
ADS1115_WE adc = ADS1115_WE(I2C_ADDRESS);

void connectToWiFi();

void connectToMQTTBroker();

void setup() {
  Serial.begin(115200);
  Wire.begin(SDA_PIN, SCL_PIN); // SDA_PIN, SCL_PIN können ggf. auch weg gelassen werden, wenn Standard-SDA/SCL-PINs
  connectToWiFi();
  mqtt_client.setServer(mqtt_broker, mqtt_port);
  connectToMQTTBroker();
  timeClient.begin();
  if(!adc.init()){
    Serial.println("ADS1115 not connected!");
    while (1); // Stoppt das Programm
  }
  // Konfiguration LED
  pinMode(LED_D5, OUTPUT);
  pinMode(LED_D6, OUTPUT);
  pinMode(LED_D7, OUTPUT);
  pinMode(LED_D8, OUTPUT);
  digitalWrite(LED_D5, LOW);
  digitalWrite(LED_D6, LOW);
  digitalWrite(LED_D7, LOW);
  digitalWrite(LED_D8, LOW);
  // Konfiguration ADS1115
  adc.setVoltageRange_mV(ADS1115_RANGE_4096);   // Spannungsbereich +/-4.096 V
  //adc.setVoltageRange_mV(ADS1115_RANGE_2048); // Spannungsbereich +/-2.048 V - wird von mir verwendet, da bei mir nur max. 1.25 V gemessen werden
  adc.setCompareChannels(ADS1115_COMP_0_GND);   // Voltage Converter an A0 des ADS1115 angeschlossen. Für andere Anschlüsse 1/2/3 ADS1115_COMP_<1/2/3>_GND verwenden
  //adc.setMeasureMode(ADS1115_SINGLE);         // Messung jedes Mal explizit anfordern - Zu Testzwecken
  //adc.setMeasureMode(ADS1115_CONTINUOUS);     // Der Chip misst im Hintergrund ununterbrochen weiter - Zu Testzwecken
  // Temperatursensor
  dht.begin();
}

void connectToWiFi() {
    WiFi.begin(ssid, password);
    Serial.print("Connecting to WiFi");
    while (WiFi.status() != WL_CONNECTED) {
        setLED(0, 1, 1, 0);
        delay(500);
        Serial.print(".");
    }
    Serial.println("\nConnected to the WiFi network");
}

void connectToMQTTBroker() {
    while (!mqtt_client.connected()) {
        String client_id = "esp8266-zisterne";
        Serial.printf("Connecting to MQTT Broker as %s.....\n", client_id.c_str());
        if (mqtt_client.connect(client_id.c_str(), mqtt_username, mqtt_password)) {
            Serial.println("Connected to MQTT broker");
        } else {
            Serial.print("Failed to connect to MQTT broker, rc=");
            Serial.print(mqtt_client.state());
            Serial.println(" try again in 5 seconds");
            setLED(1, 0, 0, 1);
            delay(5000);
        }
    }
}

int getFuellstand() {
  adc.startSingleMeasurement();
  // Warte kurz, bis die Wandlung fertig ist
  while(adc.isBusy()){
    delay(1);
  }
  //int16_t raw = adc.getRawResult(); // Zu Testzwecken
  float voltage = adc.getResult_V();
  char str[10];
  sprintf(str, "%f", voltage);
  mqtt_client.publish("zisterne/voltage", str, true);
  int fuellstand = round(100 * voltage / v_max);
  //Serial.print("Spannung: ");
  //Serial.println(voltage);
  //Serial.print("Fuellstand: ");
  //Serial.print(fuellstand);
  //Serial.println(" %");
  return fuellstand;
}

void sendTimestamp() {
  timeClient.update();  
  char str[20];
  sprintf(str, "%u", timeClient.getEpochTime());
  //Serial.println(timeClient.getEpochTime());
  mqtt_client.publish("zisterne/lastUpdate", str, true);
}

void sendFuellstand() {
  int fuellstand = getFuellstand();
  char str[20];
  sprintf(str, "%i", fuellstand);
  mqtt_client.publish("zisterne/fuellstand", str, true);
  if (fuellstand > 80) {setLED(1, 1, 1, 1);}
  else if (fuellstand > 50) {setLED(1, 1, 1, 0);}
  else if (fuellstand > 25) {setLED(1, 1, 0, 0);}
  else {setLED(1, 0, 0, 0);}
}

void sendTemp() {
  float temp = dht.readTemperature();
  float hum = dht.readHumidity();
  char str[6];
  //Serial.print("Temperatur und Luftfeuchte: ");
  //Serial.print(temp);
  //Serial.print(" °C, ");
  //Serial.print(hum);
  //Serial.println(" %");
  sprintf(str, "%f", temp);
  mqtt_client.publish("wetter/sensor/schopf/temp", str, true);
  sprintf(str, "%f", hum);
  mqtt_client.publish("wetter/sensor/schopf/hum", str, true);
}
void setLED(int led1, int led2, int led3, int led4) {
  digitalWrite(LED_D5, led1);
  digitalWrite(LED_D6, led2);
  digitalWrite(LED_D7, led3);
  digitalWrite(LED_D8, led4);
}

void loop() {
  if (!mqtt_client.connected()) {
    connectToMQTTBroker();
    }
  sendTimestamp();
  sendFuellstand();
  sendTemp();
  delay(60000);
}  

Stromversorgung: Photovoltaik

Immer einen Akku wechseln zu müssen fand' ich blöd, extra irgendwoher eine Stromleitung legen ebenfalls und da mein Projekt nicht 24/7 einen Wasserstand melden muss, der sich nur selten ändert, fand' ich eine Photovoltaik-Panele eine gute Idee.

4.5 Watt wie etwa hier (mit 6 Watt angepriesen, aber liefert laut genauerer Beschreibung nur max. 4.5) sollten eigentlich ausreichen.
Taten sie aber nicht.

Selbst bei maximaler Sonneneinstrahlung lief der ESP zwar und ich konnte an den LEDs den Wasserstand ablesen; eine WLAN-Verbindung stellte er aber nicht her. Dachte erst, ich hätte ihn ungünstig angebracht, aber mit angeschlossener PowerBank funktionierte die ganze Geschichte.

Hatte ich am Ende halt doch nen Stromkabel gezogen.