Projekt: Solar-Poolheizung im Eigenbau und Smarthome-Integration

Poolheizungssteuerung auf Basis Arduino, gekoppelt mit Smarthome-Platform FHEM

In diesem Beitrag beschreibe ich wie ich die Poolsteuerung über die Solarheizung (auf Basis schwarzer PET Schläuche am Dach) gebaut habe.

Es besteht kein Anspruch auf Vollständigkeit, sollte aber alles ausreichend dokumentiert sein um mit etwas Nachforschung die Lücken leicht füllen zu können.

Funktionalität:

Automatisches heizen des Pools über die Heizelemente solange die Rücklauftemperatur ausreichend hoch ist (in meinem Fall +5°C über der Wassertemperatur des Pools). Sobald die Rücklauftemperatur unter den Schwellwert fällt (+3° über Wassertemperatur des Pools) soll die Arbeit der Pumpe eingestellt werden. Die Schwellwerte wurden gewählt um ein schnelles Ein/Aus Pendeln zu verhindern wenn die Kraft der Sonne nicht ausreicht um Rücklauftemperatur dauerhaft über den Schwellwert zu halten. Der Pool wird auf maximal 34 ° geheizt (const PoolMaxTemp), dies ist aber normalerweise in FHEM auf einen niedrigeren Wert eingestellt.

Sollte für einen Tag die minimale Laufzeit für den Filter nicht erreicht werden, wird automatisch die Pumpe in der Nacht für den kleinen Kreislauf aktiviert und somit die verbleibende Laufzeit aufgefüllt.

Der Smarthome Server FHEM dient nur zur Visualisierung und liefert ein paar optionale Soll-Werte um die Steuerung zu beeinflussen. zB Soll-Temperatur, Berechnung der Zirkulationspumpe in der Nacht

Verwendetes Material / Software

  • Arduino Mega + Ethernet Shield (Alternative für WLAN: ESP32)
  • 2er Relais zur Ansteuerung der Pumpen
  • 20×4 Display (I²C)
  • Stecker (in meinem Fall physisch USB, logisch 1Wire Bus mit Datenleitung auf Pin 3) für Anschluss der 1-Wire DS18B20 Temepratursensoren
  • viel Kabel für:
    • 1-wire Temperatursensoren
    • 230V für Pumpen an Relais
  • Gehäuse in dem alles Platz hat
  • Patchkabel
  • Software: ArduinoIDE

Warum Arduino Mega und kein Uno?
Wenn man das Ethernet Shield nutzt hat der Arduino Uno zu wenig Anschlussmöglichkeiten um sinnvoll die beiden Relais, I²C für das Display und die 2 1-Wire-Datenbus zu betreiben. Die Datenverbindung sollte per LAN sein – wenn ein Kabel schon dort liegt und gepatcht ist… why not!

Kommentar zu ESP32: hier fällt das Ethernet Shield weg und somit die exzessive Nutzung der Digitalpins durch das Ethernet Shield.

Hardware die bereits vorhanden war:

  • Smarthome Server (auf Basis Udoo x86) mit Ubuntu Server 18.04 LTS auf 240 GB SSD
  • FHEM als Smarthome Platform (und PostgreSQL als Datenbank)
  • Netzwerk

Voraussetzung

Der Pumpkreislauf des Pools wurde auf folgendes Schema umgebaut:

Warum dieser Kreislauf und kein 3-Wege-Ventil?

  • 3-Wege-Ventile sind teuer (300+ €) und haben lange Umschaltzeiten -> teuer wenn es zu Defekten kommt
  • 2 Pumpen sind ausfallssicherer und der grüne Schrecken grüßt hoffentlich nicht
  • Implementierung über 2 Pumpen verbraucht etwa gleichviel physischer Platz
  • Pumpen sind leicht und in großer Zahl verfügbar und relativ preiswert
  • für die Umwälzung reicht eine kleinere Pumpe -> Energieeinsparung wenn kleiner Kreislauf läuft

Umsetzung

Phase 1: Hardware Prototyp auf Breadboard

Diese Phase lief fast problemlos ab:

  1. Arduino mit I²C Bus auf den Pins 20+21 mit SCA+SCL des Displays verbinden
  2. 1-Wire Bus für Temperatursensor am Pool: Datenleitung an Pin 26 + Pullup (4,7 kOhm) auf +5V
  3. 1-Wire Bus für Temperatursensor an Dach-Rücklauf: Datenleitung an Pin 28 + Pullup (4,7 kOhm) auf +5V
  4. Relais für Zirkulationspumpe (kleiner Kreislauf) an Pin 40
  5. Relais für Zirkulationspumpe (Dach) an Pin 38
  6. +5V und Ground an 1-Wire, Relais, Display
  7. Stromversorgung an einen der +5V und GND Pins

Fazit: nichts raucht, nichts wird heiß -> passt.

Aufgetretene Probleme: 2 Jumperkabel defekt 🙂

Warum 2 1-Wire Datenbus und nicht nur einer?

1 Wire ist für limitiert für die Abzweigung vom Hauptbus auf 1,5 m. Da ausreichend Pins am Arduino Mega vorhanden sind wird für die zusätzliche Stabilität durch den kürzeren Datenbus (und damit weniger Störsignale) jeder Sensor an einen eigenen Bus gehängt. Hätte ich alles an einen 1-Wire-Bus gehängt wäre ich auf über 50 m gekommen! Dies ist zwar weit weg von der maximalen Länge, hätte aber dann sicher genauer auf die Busspezifikation eingehen müssen und 1-Wire Bustreiber integrieren müssen. So hat der Digital-Pin des Arduino mit 4,7 kOhm Pullup ohne zusätzliche Beschaltung ausgereicht.

Phase 2: Software

In dieser Phase ging einiges an Zeit (und Nerven) drauf.

Arduino IDE öffnen, programmieren, debuggen (haha), Googln, programmieren, Pause, goto Anfang

Probleme: „Speichermanagement“ das ja defakto nicht existiert und bei einem strcpy durchaus zu interessanten Effekten führen kann (vor allem in Kombination mit lesen von Strings über das Netzwerk! -> liest mehr als char* Variable vorsieht -> Überschreibt Nachbarvariablen -> Chaos bricht aus) -> Abhilfe strncpy!!!

Ablaufdiagramm zur Ansteuerung der Pumpen

siehe Sourcecode: funktion „recalcAktoren()“

Sourcecode für Arduino Mega

//TODO 
/*    done Arduino als Trusted in FHEM anlegen - eigene IP 10.0.0.250
      done dummy in FHEM anlegen
      done 1W Adressen hinterlegen
      done fhemServerIP und ip ändern
      done tempDachDelta = 5 setzen
*/

/* Features: 
      Steuerung Pumpe (Manuell ein, 2 Pumpen für Zirkulation/Dach)
      Displayausgabe (Dach RL Temperaturen, Pooltemperatur, Pumpe an/aus, letzter Sync mit Fhem  etc)
      Watchdog (Reboot des Microcontrollers wenn er sich aufhängen sollte)

V1.1  Hysterese ausBei=+3 anBei=+5
 */

#include <SPI.h>
#include <Ethernet.h>
#include <Wire.h> 
#include <LiquidCrystal_I2C.h>
#include <OneWire.h>

#include <avr/wdt.h>


//##################################################
//     Pins
#define relaisPumpDachPin 38
#define relaisPumpZirkPin 40

//1w Dach
#define oneWireDachPin  28

//1w Pool
#define oneWirePoolPin 26

//###########################################################################################################
//#              Type Definitions
//###########################################################################################################

struct fhemReadings_t {
  char date[19];  // "29.08.2019 18:01:39"

  //Aktoren
  boolean PumpActiveDachSoll;
  boolean PumpActiveDachIst;
  boolean PumpActiveZirkSoll;
  boolean PumpActiveZirkIst;
  boolean PumpONRequested;
  boolean PumpMasterON;

  //Temperaturen
  //double dachVLTemp;
  double dachRLTemp;
  double poolTemp;
  int PoolTempRequested;
};
  #define VentilPositionDach 0
  #define VentilPositionZirk 1
  
struct statistics_t {
  long ok;
  long nok;
};

struct tempsens_t {
  byte addr[8];
};

//###########################################################################################################
//#              Global Variables
//###########################################################################################################

//this is my MAC-Adress
byte mac[] = {
  0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xEA
};
//my own IP
IPAddress ip(10, 0, 0, 250);
// Enter the IP address of the FHEM server you're connecting to:
IPAddress fhemServerIP(10, 0, 0, 2 );

EthernetClient fhemTelnetClient;
OneWire dsDach(oneWireDachPin);  // One-Wire on pin x
OneWire dsPool(oneWirePoolPin);  // One-Wire on pin x
LiquidCrystal_I2C lcd(0x27,20,4); // set the LCD address to 0x27 for a 20 chars and 2 line display
int lcdCurrentPage = 0;
const int lcdPageCount = 1;

unsigned long lastButtonpress = 0;
unsigned long loopStarted = 0;
unsigned long loopTime = 0;
unsigned long pumpeStartZeit = 0;
unsigned long pumpeZeit = 0;
const unsigned long fhemUpdateInterval = 60000;
unsigned long fhemLastUpdateMillis = 0;
boolean newFhemData = false;
const int NUM_FHEM_READING_CHARS = 300;
char fhemReadingsChar[NUM_FHEM_READING_CHARS];
fhemReadings_t fhemReadings;
statistics_t stats;

int currentState = 1;
unsigned long waitStateStartedMillis = 0;
unsigned long otherPageTime = 0;


//###########################################################################################################
//#              Wassertemperatur Einstellungen
//###########################################################################################################

const float PoolMinTemp = 10; 
const float PoolMaxTemp = 34; 
const float TempDachDeltaAus = 3;
const float TempDachDeltaAn = 5;


//###########################################################################################################
//#              OneWire Sensoren Adressen - müssen mit i2c Scanner ausgelesen werden
//###########################################################################################################

/*tempsens_t DachVLTemp =   { 
                              .addr = { 40, 255, 135, 43, 181, 22, 3, 27 }
                            };*/
                            
tempsens_t DachRLTemp =   { 
                              .addr = { 0x28, 0xFF, 0x9D, 0x3, 0xB5, 0x16, 0x3, 0xB6 }
                            };

tempsens_t PoolTemp =   { 
                              .addr = { 0x28, 0xFF, 0x72, 0xD9, 0xB4, 0x16, 0x5, 0x6F }
                            };
                          
//###########################################################################################################
//#              Function Prototypes
//###########################################################################################################

boolean fhemSync(void);
void readall( char*);
void discardall( void);
boolean fhemLogin(void);
boolean fhemLoadReadings(void);
void fhemShowReadings(void);
boolean fhemUpdateReadings(void);
void recvWithStartEndMarkers(char *result);
boolean convertReadings(void);
void clearFhemReadings(void);
int availableMemory();
void updatePins();
void updateDisplay();
void buttonHandler();
int readTempSensorsPool();
int readTempSensorsDach();
int recalcAktoren();


char *ftoa(char *a, double f, int precision)
{
 long p[] = {0,10,100,1000,10000,100000,1000000,10000000,100000000};
 
 char *ret = a;
 long heiltal = (long)f;
 itoa(heiltal, a, 10);
 while (*a != '\0') a++;
 *a++ = '.';
 long desimal = abs((long)((f - heiltal) * p[precision]));
 itoa(desimal, a, 10);
 return ret;
}


//###########################################################################################################
//#              S E T U P
//###########################################################################################################
void setup() {
  // start the Ethernet connection:
  Ethernet.begin(mac, ip);

  // Open serial communications and wait for port to open:
  Serial.begin(9600);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }

  Serial.print("My IP address: ");
  Serial.println(Ethernet.localIP());

  //Setup Pins und gleich ausschalten
  pinMode(relaisPumpDachPin, OUTPUT);
  digitalWrite(relaisPumpDachPin, LOW);
  pinMode(relaisPumpZirkPin, OUTPUT);
  digitalWrite(relaisPumpZirkPin, LOW);

  clearFhemReadings();

  lcd.init(); //initialize the lcd
  lcd.clear();
  lcd.backlight(); //open the backlight 

  //Watchdog aktivieren
  wdt_enable(WDTO_8S);

  lcdCurrentPage = 0;
}



//###########################################################################################################
//#              L O O P
//###########################################################################################################
void loop() {
  boolean rc = true;

  Serial.println("");
  Serial.println("");
  Serial.println("");
  Serial.println("");
  Serial.println("");
  Serial.println("");
  Serial.println("loop START");

  loopStarted = millis();
  
  if (Ethernet.maintain())
    Serial.println("Ethernet.maintain = done");
  else
    Serial.println("Ethernet.maintain = 0");

  //Serial.print("free RAM "); Serial.println(freeRam());
  Serial.print("free RAM "); Serial.println(availableMemory());

  //FHEM Daten holen
  boolean doFhem;
  if ((unsigned long) millis() >= fhemLastUpdateMillis + fhemUpdateInterval )
    doFhem = true;
  else
    doFhem = false;
    
  if ( doFhem )
  {
    fhemSyncRead();
  }

  //Temperatursensoren neu lesen
  readTempSensorsPool();
  readTempSensorsDach();

  //neuen Aktorenzustand berechnen
  recalcAktoren();

  fhemShowReadings();
  
  //update Output Pins / Aktoren
  updatePins();

  //FHEM Daten senden
  if ( doFhem  )
  {
    fhemSyncWrite();
    fhemLastUpdateMillis = millis();
  }

  updateDisplay();


  Serial.println("");
  Serial.println("");
  if ( rc == true )
  {
    Serial.println("     OOO     K   K");
    Serial.println("    O   O    K  K");
    Serial.println("    O   O    K K");
    Serial.println("    O   O    K  K");
    Serial.println("     OOO     K   K");
    stats.ok++;
  }
  else
  {
    Serial.println("   X     X        X     X        X     X     ");
    Serial.println("    X   X          X   X          X   X      ");
    Serial.println("     X X            X X            X X       ");
    Serial.println("      X              X              X        ");
    Serial.println("     X X            X X            X X       ");
    Serial.println("    X   X          X   X          X   X      ");
    Serial.println("   X     X        X     X        X     X     ");
    stats.nok++;
  }
  Serial.println("");
  Serial.print("ok = ");
  Serial.print(stats.ok);
  Serial.print("    nok = ");
  Serial.println(stats.nok);
  Serial.println("loop END");

  //Waldi beruhigen....
  wdt_reset();

  loopTime = millis() - loopStarted;

  while( ( (unsigned long) loopStarted + 300 >= millis()))
    true;
}


//###########################################################################################################
//#              fhemSyncRead
//###########################################################################################################
boolean fhemSyncRead() {
  boolean rc = false;
  fhemTelnetClient.stop();

  lcd.setCursor(19,3);
  lcd.print("r");

  Serial.println("connecting to fhem...");
  // if you get a connection, report back via serial:

  if (fhemTelnetClient.connect(fhemServerIP, 7073)) {
    rc = true;
    for (int i = 0; i < 100; i++) {
      if ( fhemTelnetClient.connected() )
      {
        Serial.println("Telnet to FHEM connected");
        break;
      }
      else
        delay(100);
    }

  } else {
    // if you didn't get a connection to the server:
    Serial.println("Telnet to FHEM connection failed -> nur WW-Aufbereitung; Heizung Notbetrieb");
    return false;
  }

  if ( fhemLogin( ) == true )
  {
    rc = fhemLoadReadings();
    if ( rc == true )
    {
      rc = convertReadings();
    }
  }
  else
  {
    Serial.println("FHEM authorization failed -> cancelling");
    rc = false;
  }

  Serial.println();
  Serial.println("disconnecting.");
  fhemTelnetClient.stop();

  lcd.setCursor(19,3);
  lcd.print(" ");

  return rc;
}

void fhemShowReadings(void){
  Serial.println("-- Current FHEM Readings in RAM");
  Serial.print ("Date: "); Serial.println (fhemReadings.date);
    
  if (fhemReadings.PumpActiveDachSoll)
    Serial.println("PumpActiveDachSoll=true");
  else
    Serial.println("PumpActiveDachSoll=false"); ;
    
  if (fhemReadings.PumpActiveDachIst)
    Serial.println("PumpActiveDachIst=true");
  else
    Serial.println("PumpActiveDachIst=false"); ;

  if (fhemReadings.PumpActiveZirkSoll)
    Serial.println("PumpActiveZirkSoll=true");
  else
    Serial.println("PumpActiveZirkSoll=false"); ;
    
  if (fhemReadings.PumpActiveZirkIst)
    Serial.println("PumpActiveZirkIst=true");
  else
    Serial.println("PumpActiveZirkIst=false"); ;

  if (fhemReadings.PumpONRequested)
    Serial.println("PumpONRequested=true");
  else
    Serial.println("PumpONRequested=false"); ;

    
//  Serial.print("dachVLTemp="); Serial.println(fhemReadings.dachVLTemp);
  Serial.print("dachRLTemp="); Serial.println(fhemReadings.dachRLTemp);
  Serial.print("poolTemp="); Serial.println(fhemReadings.poolTemp);  
}

//###########################################################################################################
//#              fhemSyncWrite
//###########################################################################################################
boolean fhemSyncWrite() {
  boolean rc = false;
  fhemTelnetClient.stop();

  lcd.setCursor(19,3);
  lcd.print("w");

  Serial.println("connecting to fhem...");
  // if you get a connection, report back via serial:

  if (fhemTelnetClient.connect(fhemServerIP, 7073)) {
    rc = true;
    for (int i = 0; i < 100; i++) {
      if ( fhemTelnetClient.connected() )
      {
        Serial.println("Telnet to FHEM connected");
        break;
      }
      else
        delay(100);
    }

  } else {
    // if you didn't get a connection to the server:
    Serial.println("Telnet to FHEM connection failed -> nur WW-Aufbereitung; Heizung Notbetrieb");
    return false;
  }

  if ( fhemLogin( ) == true )
  {
    rc = fhemUpdateReadings();
  }
  else
  {
    Serial.println("FHEM authorization failed -> cancelling");
    rc = false;
  }


  Serial.println();
  Serial.println("disconnecting.");
  fhemTelnetClient.stop();

  lcd.setCursor(19,3);
  lcd.print(" ");
  
  return rc;
}
//###########################################################################################################
void readall(char* clientCommand) {
  /*String text;
    for (int i = 0; i<1000; i++) {
    delay(20);
    if(client.available()) {
      text+= (char)client.read();
      if(client.available() == 0);
        break;
    }
    }
    return text;*/



  unsigned long start = 0;
  int pos = 0;
  clientCommand[0] = '\0';

  delay(100);
  start = millis();

  while ( start + 500 <= millis() ) {
    while (fhemTelnetClient.available() > 0) {

      clientCommand[pos] = (char)fhemTelnetClient.read();
      Serial.println(clientCommand);
      pos++;
    }
    delay(100);
  }
}

//###########################################################################################################
void discardall( void) {
  unsigned long start = 0;
  delay(100);
  start = millis();

  while ( start + 100 <= millis() ) {
    while (fhemTelnetClient.available() > 0) {

      fhemTelnetClient.read();
    }
    delay(100);
  }
}

//###########################################################################################################
boolean fhemLogin()  {
  //String text;

  delay(500);
  /*Serial.println("try to read for password prompt");
    text = readall(client);

    Serial.println(text);

    if (text.indexOf("Password: ")>-1){
      Serial.println("Telnet send: sending pass");
      client.println("YOUR_VERY_SECRET_PASSWORD_FOR_TELNET_IN_FHEM");


      //send enter to get "fhem>"
      client.println("");
      client.flush();
      Serial.println("wait 50");
      delay(50);

      for(int i=0;i<1000;i++) {
        delay(100);
        if(client.available() > 0)
          client.read();
      }
      return true;
    }
    else {
    return false;
    }*/


  //Arduino ist trusted IP -> no login required
  return true;
}

//###########################################################################################################
boolean fhemLoadReadings()  {
  String text = "";
  fhemReadingsChar[0] = '\0';
  boolean rc = false;
  unsigned long start = millis();

  //neuen Befehl senden
  char befehl[300] = "{sprintf(\"[result/%s/%s/%s/%s]\",strftime('%d.%m.%Y %H:%M:%S',localtime),ReadingsVal(\"Pool_PumpONRequested\",\"state\",0),ReadingsVal(\"Pool_TempRequested\",\"state\",25),ReadingsVal(\"Pool_PumpMasterON\",\"state\",0))}";
  Serial.print("Telnet send: "); Serial.println(befehl);
  fhemTelnetClient.println(befehl);

  Serial.println("wait 500");
  delay(500);

  char charResult[500];

  recvWithStartEndMarkers(charResult);
  if (newFhemData == true)
  {
    Serial.println("entering if newFhemData");
    text = String(charResult);
    Serial.println(newFhemData);

    text.remove(text.lastIndexOf("]"));
    text = text.substring( text.indexOf("[") + 1 );
    Serial.println(text);
    newFhemData = false;
    strncpy(fhemReadingsChar, text.c_str(), NUM_FHEM_READING_CHARS);
    rc = true;
  }
  else
    rc = false;

  Serial.println("-------------------------------");

  Serial.print("runtime: ");
  Serial.println(millis() - start);

  return rc;
}

//###########################################################################################################
boolean fhemUpdateReadings()  {
  String text = "";
  boolean rc = false;
  unsigned long start = millis();
  char *befehl;
  char befehlchar[100];
  char wert[50];
  char *befehlx = NULL;

  
  //PumpActiveDachSoll
  wert[0] = '\0';
  befehlchar[0] = '\0';

  if (fhemReadings.PumpActiveDachSoll)
    befehl = "set Pool_PumpActiveDachSoll on";
  else
    befehl = "set Pool_PumpActiveDachSoll off";
  Serial.print("Telnet send: "); Serial.println(befehl);
  fhemTelnetClient.println(befehl);

  Serial.println("wait 50");
  delay(20);
  discardall();

  //PumpActiveDachIst
  wert[0] = '\0';
  befehlchar[0] = '\0';

  if (fhemReadings.PumpActiveDachIst)
    befehl = "set Pool_PumpActiveDachIst on";
  else
    befehl = "set Pool_PumpActiveDachIst off";
  Serial.print("Telnet send: "); Serial.println(befehl);
  fhemTelnetClient.println(befehl);

  Serial.println("wait 50");
  delay(20);
  discardall();


  //PumpActiveZirkSoll
  wert[0] = '\0';
  befehlchar[0] = '\0';

  if (fhemReadings.PumpActiveZirkSoll)
    befehl = "set Pool_PumpActiveZirkSoll on";
  else
    befehl = "set Pool_PumpActiveZirkSoll off";
  Serial.print("Telnet send: "); Serial.println(befehl);
  fhemTelnetClient.println(befehl);

  Serial.println("wait 50");
  delay(20);
  discardall();

  //PumpActiveZirkIst
  wert[0] = '\0';
  befehlchar[0] = '\0';

  if (fhemReadings.PumpActiveZirkIst)
    befehl = "set Pool_PumpActiveZirkIst on";
  else
    befehl = "set Pool_PumpActiveZirkIst off";
  Serial.print("Telnet send: "); Serial.println(befehl);
  fhemTelnetClient.println(befehl);

  Serial.println("wait 50");
  delay(20);
  discardall();



/*  //dachVLTemp
  wert[0] = '\0';
  befehlchar[0] = '\0';

  dtostrf(fhemReadings.dachVLTemp,2,1,wert);
  Serial.println(wert);
  strncpy(befehlchar, "set Pool_dachVLTemp ", 100);
  befehlx = strcat ( befehlchar , wert);
  Serial.print("Telnet send: "); Serial.println(befehlx);
  fhemTelnetClient.println(befehlx);

  Serial.println("wait 50");
  delay(20);
  discardall();*/

  //dachRLTemp
  wert[0] = '\0';
  befehlchar[0] = '\0';

  dtostrf(fhemReadings.dachRLTemp,2,1,wert);
  Serial.println(wert);
  strncpy(befehlchar, "set Pool_dachRLTemp ",100);
  befehlx = strcat ( befehlchar , wert);
  Serial.print("Telnet send: "); Serial.println(befehlx);
  fhemTelnetClient.println(befehlx);

  Serial.println("wait 50");
  delay(20);
  discardall();

  //poolTemp
  wert[0] = '\0';
  befehlchar[0] = '\0';

  dtostrf(fhemReadings.poolTemp,2,1,wert);
  Serial.println(wert);
  strncpy(befehlchar, "set Pool_poolTemp ", 100);
  befehlx = strcat ( befehlchar , wert);
  Serial.print("Telnet send: "); Serial.println(befehlx);
  fhemTelnetClient.println(befehlx);

  Serial.println("wait 50");
  delay(20);
  discardall();

  rc = true;
  Serial.print("runtime: ");
  Serial.println(millis() - start);

  return rc;
}

//###########################################################################################################
void recvWithStartEndMarkers(char *result) {
  static boolean recvInProgress = false;
  static byte ndx = 0;
  char startMarker = '[';
  char endMarker = ']';
  char rc;
  const int numChars = 500;
  char receivedChars[numChars];

  Serial.println("recvWithStartEndMarkers START");
  Serial.println(ndx);
  Serial.println(recvInProgress);


  while (fhemTelnetClient.available() > 0 && newFhemData == false) {
    rc = fhemTelnetClient.read();
    Serial.print(rc);

    if (recvInProgress == true) {
      if (rc != endMarker) {
        receivedChars[ndx] = rc;
        ndx++;
        if (ndx >= numChars) {
          ndx = numChars - 1;
        }
      }
      else {
        receivedChars[ndx] = '\0'; // terminate the string
        recvInProgress = false;
        ndx = 0;
        newFhemData = true;
      }
    }

    else if (rc == startMarker) {
      recvInProgress = true;
    }
  }
  strncpy(result, receivedChars, 500);
  Serial.println("");
  Serial.println("recvWithStartEndMarkers END");
}

//###########################################################################################################
boolean convertReadings() {
  /*
       char date[19];  // "29.08.2017 18:01:39"
  //Aktoren
  
  boolean VentilPositionSoll;
  boolean VentilPositionIst;
  boolean PumpActiveSoll;
  boolean PumpActiveIst;

  //Temperaturen
  double dachVLTemp;
  double dachRLTemp;
  double poolTemp;
  */
  char *p;
  char *str;
  char **end;
  Serial.println("convert fhem readings");

  if (strlen(fhemReadingsChar) > 0)
  {
    p = fhemReadingsChar;

    //Parse result keyword
    Serial.println("parsing result");
    str = strtok_r(p, "/", &p);
    if (str != NULL)
    {
      Serial.println(str);
    }
    else
    {
      Serial.println("Error parsing result");
      return false;
    }

    //Parse date
    Serial.println("parsing date");
    str = strtok_r(p, "/", &p);
    if (str != NULL)
    {
      Serial.println(str);
      strncpy(fhemReadings.date, str, 19);
    }
    else
    {
      Serial.println("Error parsing result");
      return false;
    }
	
	//Parse Pool_PumpONRequested (boolean)
    Serial.println("parsing Pool_PumpONRequested");
    str = strtok_r(p, "/", &p);
    if (str != NULL)
    {
      Serial.println(str);
      if (strcmp(str, "on") == 0)
        fhemReadings.PumpONRequested = true;
      else if (strcmp(str, "off") == 0)
        fhemReadings.PumpONRequested = false;
      else
      {
        Serial.println("ERROR: Pool_PumpONRequested: String to Bool Conversion failed");
        return false;
      }
    }
    else
    {
      Serial.println("Error parsing result");
      return false;
    }


      //Parse Pool_TempRequested (int)
    Serial.println("parsing Pool_PumpONRequested");
    str = strtok_r(p, "/", &p);
    if (str != NULL)
    {
      Serial.println(str);
      fhemReadings.PoolTempRequested = atoi(str);
    }
    else
    {
      Serial.println("Error parsing result");
      return false;
    }

    //Parse Pool_PumpMasterON (boolean)
    Serial.println("parsing Pool_PumpMasterON");
    str = strtok_r(p, "/", &p);
    if (str != NULL)
    {
      Serial.println(str);
      if (strcmp(str, "on") == 0)
        fhemReadings.PumpMasterON = true;
      else if (strcmp(str, "off") == 0)
        fhemReadings.PumpMasterON = false;
      else
      {
        Serial.println("ERROR: Pool_PumpMasterON: String to Bool Conversion failed");
        return false;
      }
    }
    else
    {
      Serial.println("Error parsing result");
      return false;
    }

    return true;
  }
  else
  {
    return false;
  }
}

void clearFhemReadings()
{
  //clear fhemReadings - ohne Aktoren
  fhemReadings.PumpActiveDachSoll = false;
  fhemReadings.PumpActiveDachIst = false;
  fhemReadings.PumpActiveZirkSoll = false;
  fhemReadings.PumpActiveZirkIst = false;
  fhemReadings.PumpONRequested = false;
  fhemReadings.PumpMasterON = true;
//  fhemReadings.dachVLTemp = 0.0;
  fhemReadings.dachRLTemp = 0.0;
  fhemReadings.poolTemp = 0.0;
  fhemReadings.PoolTempRequested = 25;
  strncpy(fhemReadings.date, "keine Verb. zu FHEM", 19);
}

/*int freeRam () {
  extern int _heap_start, *_brkval;
  int v;
  return (int) &v - (_brkval == 0 ? (int) &_heap_start : (int) __brkval);
}*/

int availableMemory() {
  int size = 8192; // Use 2048 with ATmega328
  byte *buf;

  while ((buf = (byte *) malloc(--size)) == NULL)
    ;

  free(buf);

  return size;
}

void updatePins() {

  
  
	//normales Prozedere - aktivieren einer Pumpe
  	
    if(fhemReadings.PumpActiveDachSoll)
  	{
  	  digitalWrite(relaisPumpDachPin, LOW);
      fhemReadings.PumpActiveDachIst = true;
  	  Serial.println("Pumpe Pool Dach ein");
	  
  	}
  	else {
  	  digitalWrite(relaisPumpDachPin, HIGH);
      fhemReadings.PumpActiveDachIst = false;
  	  Serial.println("Pumpe Pool Dachaus");
  	}

    if(fhemReadings.PumpActiveZirkSoll)
    {
      digitalWrite(relaisPumpZirkPin, LOW);
      fhemReadings.PumpActiveZirkIst = true;
      Serial.println("Pumpe Pool Zirkulation ein");
    
    }
    else {
      digitalWrite(relaisPumpZirkPin, HIGH);
      fhemReadings.PumpActiveZirkIst = false;
      Serial.println("Pumpe Pool Zirkulatio aus");
    }  
}

void buttonHandler() {
  if( millis() >= lastButtonpress + 350)
  {
    lastButtonpress = millis();
    //Serial.println("pin2 interrupt");
    //Serial.println("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
    
    lcdCurrentPage++;
    
    if( lcdCurrentPage >= lcdPageCount)
      lcdCurrentPage = 0;
  }

  if (lcdCurrentPage != 0)
     otherPageTime = 1;
}

void updateDisplay(){

  char array1[]="Zeile 1            1 "; //the string to print on the LCD
  char array2[]="zEile 2            2 "; //the string to print on the LCD
  char array3[]="zeIle 3            3 "; //the string to print on the LCD
  char array4[]="zeiLe 4            4 "; //the string to print on the LCD
  char temp[] = "                     ";
  char temp2[]= "                     ";
  char temp3[]= "                     ";
  //lcd.clear();

  if( lcdCurrentPage != 0)
  {
    if( otherPageTime > 120)
      lcdCurrentPage = 0;
    else
      otherPageTime++;
  }
  Serial.print("otherPageTime="); Serial.println(otherPageTime);


  //Inhalt des Displays in den 4 Arrays aufbauen
  switch(lcdCurrentPage)
  {
    case 0:
      //Seite 1:  xxxxxxxxxxxxxxxxxxxx
      // Zeile 1: PoolTemp = 32 ° C  i
      // Zeile 2: Dach=aus Zirk=aus   
      // Zeile 3: Dach RL=45.1
      // Zeile 4: 01.09.2017 13:20:51
      dtostrf(fhemReadings.poolTemp,2,1,temp);
      sprintf(array1, "PoolTemp = %s C", temp );
      
      if (fhemReadings.PumpActiveDachIst)
        strncpy(temp, "an", 5);
      else
        strncpy(temp, "aus", 5);
        
      if (fhemReadings.PumpActiveZirkIst)
        strncpy(temp2, "an", 5);
      else
        strncpy(temp2, "aus", 5);
	  
        
      sprintf(array2, "Dach=%3s Zirk=%3s   %1d", temp,temp2, currentState );

      if(fhemReadings.PumpMasterON == false )
      {
        sprintf(array2, "MASTER OFF" );    
      }
	  
      dtostrf(fhemReadings.dachRLTemp,2,1,temp2);
      sprintf(array3, "Dach RL=%4s", temp2 );
  
      sprintf(array4, "%s",fhemReadings.date);
      array4[19] = ' ';
      break;

  for(int i=strlen(array1); i<20; i++)
    array1[i] = ' ';
  
  for(int i=strlen(array2); i<20; i++)
    array2[i] = ' ';

  for(int i=strlen(array3); i<20; i++)
    array3[i] = ' ';

  for(int i=strlen(array4); i<20; i++)
    array4[i] = ' ';
    

  Serial.println("LCD Output:");
  Serial.println("----------------------");
  Serial.print("|");
  Serial.print(array1);
  Serial.println("|");
  Serial.print("|");
  Serial.print(array2);
  Serial.println("|");
  Serial.print("|");
  Serial.print(array3);
  Serial.println("|");
  Serial.print("|");
  Serial.print(array4);
  Serial.println("|");
  Serial.println("----------------------");
  
  //Arrays auf LCD schreiben
  lcd.setCursor(0,0); // set the cursor to column 15, line 0
  for (int positionCounter1 = 0; positionCounter1 < 19; positionCounter1++) {
    lcd.print(array1[positionCounter1]);
  }
  lcd.setCursor(0,1);
  for (int positionCounter1 = 0; positionCounter1 < 20; positionCounter1++) {
    lcd.print(array2[positionCounter1]);
  }
  
  lcd.setCursor(0,2);
  for (int positionCounter1 = 0; positionCounter1 < 20; positionCounter1++) {
    lcd.print(array3[positionCounter1]);
  }

  lcd.setCursor(0,3);
  for (int positionCounter1 = 0; positionCounter1 < 20; positionCounter1++) {
    lcd.print(array4[positionCounter1]);
  }

}

int readTempSensorsDach() {
  byte data[12];
  byte addr[8];

  Serial.println("readTempSensorsDach      START\n");
  while( dsDach.search(addr)) {

    Serial.print("1 Wire current addr: ");
    Serial.print(addr[0]);Serial.print(",");
    Serial.print(addr[1]);Serial.print(",");
    Serial.print(addr[2]);Serial.print(",");
    Serial.print(addr[3]);Serial.print(",");
    Serial.print(addr[4]);Serial.print(",");
    Serial.print(addr[5]);Serial.print(",");
    Serial.print(addr[6]);Serial.print(",");
    Serial.print(addr[7]);Serial.println("");
  
    if ( OneWire::crc8( addr, 7) != addr[7]) {
        Serial.println("CRC is not valid!");
        return -1000;
    }
  
    if ( addr[0] != 0x10 && addr[0] != 0x28) {
        Serial.print("Device is not recognized");
        return -1000;
    }
  
    dsDach.reset();
    dsDach.select(addr);
    dsDach.write(0x44,1); // start conversion, with parasite power on at the end
  
    byte present = dsDach.reset();
    dsDach.select(addr);    
    dsDach.write(0xBE); // Read Scratchpad
  
    
    for (int i = 0; i < 9; i++) { // we need 9 bytes
      data[i] = dsDach.read();
    }
    
    byte MSB = data[1];
    byte LSB = data[0];
  
    float tempRead = ((MSB << 8) | LSB); //using two's compliment
    float TemperatureSum = tempRead / 16;
  
    Serial.print("tempsens=");
    Serial.println(TemperatureSum);

    /*if( ! memcmp(addr, DachVLTemp.addr, 8))
    { 
      fhemReadings.dachVLTemp = TemperatureSum;
      Serial.print("Dach VL Temp = ");
      Serial.println(fhemReadings.dachVLTemp);
    }
    else */
    if ( ! memcmp(addr, DachRLTemp.addr, 8))
    { 
      fhemReadings.dachRLTemp = TemperatureSum;
      Serial.print("Rauchfang 1 Temp = ");
      Serial.println(fhemReadings.dachRLTemp);
    }
    

    Serial.println("");    
  } // while
  dsPool.reset_search();

  Serial.println("readTempSensorsDach      END\n");

  return true;
}

int readTempSensorsPool() {
  byte data[12];
  byte addr[8];

  Serial.println("readTempSensorsPool      START\n");
  while( dsPool.search(addr)) {

    Serial.print("1 Wire current addr: ");
    Serial.print(addr[0]);Serial.print(",");
    Serial.print(addr[1]);Serial.print(",");
    Serial.print(addr[2]);Serial.print(",");
    Serial.print(addr[3]);Serial.print(",");
    Serial.print(addr[4]);Serial.print(",");
    Serial.print(addr[5]);Serial.print(",");
    Serial.print(addr[6]);Serial.print(",");
    Serial.print(addr[7]);Serial.println("");
  
    if ( OneWire::crc8( addr, 7) != addr[7]) {
        Serial.println("CRC is not valid!");
        return -1000;
    }
  
    if ( addr[0] != 0x10 && addr[0] != 0x28) {
        Serial.print("Device is not recognized");
        return -1000;
    }
  
    dsPool.reset();
    dsPool.select(addr);
    dsPool.write(0x44,1); // start conversion, with parasite power on at the end
  
    byte present = dsPool.reset();
    dsPool.select(addr);    
    dsPool.write(0xBE); // Read Scratchpad
  
    
    for (int i = 0; i < 9; i++) { // we need 9 bytes
      data[i] = dsPool.read();
    }
    
    byte MSB = data[1];
    byte LSB = data[0];
  
    float tempRead = ((MSB << 8) | LSB); //using two's compliment
    float TemperatureSum = tempRead / 16;
  
    Serial.print("tempsens=");
    Serial.println(TemperatureSum);


    if ( ! memcmp(addr, PoolTemp.addr, 8))
    {
      fhemReadings.poolTemp = TemperatureSum;
      Serial.print("Pool Temp = ");
      Serial.println(fhemReadings.poolTemp);
    }

    Serial.println("");    
  } // while
  dsPool.reset_search();

  Serial.println("readTempSensorsPool      END\n");

  return true;
}

int recalcAktoren() {

  if(fhemReadings.PumpMasterON == false )
  {
    fhemReadings.PumpActiveZirkSoll = false;
    fhemReadings.PumpActiveDachSoll = false;
    return true;
  }
  
  Serial.print("CurrentState="); Serial.println(currentState);
#define VentilUmschaltzeit 0
   
  switch(currentState) 
  {
    case 1: //Initialzustand
      //
      fhemReadings.PumpActiveZirkSoll = false;
      fhemReadings.PumpActiveDachSoll = false;
	  
	    //Dach Rücklauf ist wärmer als Pool (und noch  nicht MaximalTemperatur) -> Zirkulation übers Dach aktivieren
      if( fhemReadings.dachRLTemp > fhemReadings.poolTemp + TempDachDeltaAn  && fhemReadings.poolTemp <= PoolMaxTemp && fhemReadings.poolTemp <= fhemReadings.PoolTempRequested )
		  {
		    currentState = 2;
        break;
		  }
        
	  
	    //Pumpanforderung vorhanden - Dach Rücklauf ist nicht warm genug -> Zirkulation starten
      if( fhemReadings.PumpONRequested == true ) 
      {   
         if ( fhemReadings.dachRLTemp < fhemReadings.poolTemp + 2 ) 
            currentState = 4; // Zirkulation starten
         else
            currentState = 4; // Zirkulation starten 
      }
      else
        currentState = 1;
        
      break; // case 1

    case 2:  // Wechsel auf Dach - 10 s warten
      fhemReadings.PumpActiveZirkSoll = false;
      fhemReadings.PumpActiveDachSoll = false;

      if (waitStateStartedMillis == 0)
      {
        //initialzustand des Wartens -> millis abspeichern 
        waitStateStartedMillis = millis();
      }

      if( waitStateStartedMillis + VentilUmschaltzeit <= millis() )
      {
        //Zeit ist vorbei -> weiter gehts
        waitStateStartedMillis = 0;
        currentState = 3;
      }
      break;  // case 2
      
    case 3:
	  //Zirkulation übers Dach laufen lassen
      fhemReadings.PumpActiveZirkSoll = false;
      fhemReadings.PumpActiveDachSoll = true;
	  
	  //Pool geht über Maximaltemperatur -> Pumpe aus bzw umschalten auf Zirkulation
	  if( fhemReadings.poolTemp > PoolMaxTemp || fhemReadings.poolTemp > fhemReadings.PoolTempRequested)
		{
      if (fhemReadings.PumpONRequested == false )
		  {
		    currentState = 1;
        break;
		  }
      else 
      {
        currentState = 4;
        break;	  
      }
		}
    
	  //Dach-Rücklauf ist niedriger als Pool -> Umschalten auf Zirkulation
	  if( fhemReadings.dachRLTemp - TempDachDeltaAus < fhemReadings.poolTemp )
	  {
	    if( fhemReadings.PumpONRequested == true )
	      currentState = 4; //Weiter mit Zirkulation
      else 
        currentState = 1; //Pumpe aus
	  }
		 	  
     break; // case 3

    case 4:
      //umschalten auf Zirkulation und 10 s warten
	    fhemReadings.PumpActiveZirkSoll = false;
      fhemReadings.PumpActiveDachSoll = false;
	  
	  if (waitStateStartedMillis == 0)
      {
        //initialzustand des Wartens -> millis abspeichern 
        waitStateStartedMillis = millis();
      }

      if( waitStateStartedMillis + VentilUmschaltzeit <= millis() )
      {
        //Zeit ist vorbei -> weiter gehts
        waitStateStartedMillis = 0;
        currentState = 5;
      }

      break;  //case 4

    case 5:
      fhemReadings.PumpActiveZirkSoll = true;
      fhemReadings.PumpActiveDachSoll = false;
	  
	    //Pumpe soll aus sein -> zurück zum start
	    if( fhemReadings.PumpONRequested == false)
		  {
		    currentState = 1;
        break;
	  	}
	  
	    //Pool könnte geheizt werden -> umschalten auf Dachzirkulation
	    if( fhemReadings.dachRLTemp > fhemReadings.poolTemp + TempDachDeltaAn && fhemReadings.poolTemp < PoolMaxTemp  && fhemReadings.poolTemp <= fhemReadings.PoolTempRequested)
		    currentState = 2;
      
      break;  //  case 5

  }

  return 1;
}

Phase 4: Integration/Konfiguration FHEM

Hier gehe ich davon aus dass eine funktionierende FHEM Installation vorliegt und eine Netzwerkverbindung besteht.

FHEM Objekte anlegen:

#Telnet Port für Kommunikation zu Poolsteuerung 10.0.0.23 und 10.0.0.250 können sich verbinden (Whitelisted) - andere IPs nicht (Grund: Telnet ist nicht passwortgeschützt):
define telnetPortArduino telnet 7073 global
attr telnetPortArduino allowfrom 10.0.0.23|10.0.0.250
attr telnetPortArduino room 9.05 fhem

#Dummy für Werte aus Poolsteuerung
define Pool_PumpActiveDachSoll dummy
attr Pool_PumpActiveDachSoll group Pool-Ist-Werte
attr Pool_PumpActiveDachSoll room 2.10 Pool

define Pool_PumpActiveDachIst dummy
attr Pool_PumpActiveDachIst group Pool-Ist-Werte
attr Pool_PumpActiveDachIst room 2.10 Pool

define Pool_dachRLTemp dummy
attr Pool_dachRLTemp group Pool-Ist-Werte
attr Pool_dachRLTemp room 2.10 Pool

define Pool_poolTemp dummy
attr Pool_poolTemp group Pool-Ist-Werte
attr Pool_poolTemp room 2.10 Pool

define Pool_PumpActiveZirkIst dummy
attr Pool_PumpActiveZirkIst group Pool-Ist-Werte
attr Pool_PumpActiveZirkIst room 2.10 Pool

define Pool_PumpActiveZirkSoll dummy
attr Pool_PumpActiveZirkSoll group Pool-Ist-Werte
attr Pool_PumpActiveZirkSoll room 2.10 Pool


#Zeitsteuerung für Zirkulationspumpe
define PumpeVMStart at *03:00:00 set Pool_PumpONRequested on
attr PumpeVMStart group PoolZeitschaltuhr
attr PumpeVMStart room 2.10 Pool

define PumpeVMStop at *06:30:00 set Pool_PumpONRequested off
attr PumpeVMStop group PoolZeitschaltuhr
attr PumpeVMStop room 2.10 Pool

define Pool_PumpONRequested dummy
attr Pool_PumpONRequested event-min-interval tick.*:0,.*:3600
attr Pool_PumpONRequested group Pool-Einstellungen
attr Pool_PumpONRequested room 2.10 Pool
attr Pool_PumpONRequested setList :on,off
attr Pool_PumpONRequested webCmd on:off


#Vorgabe der Maximaltemperatur (Slider)
define Pool_TempRequested dummy
attr Pool_TempRequested event-min-interval .*:3600
attr Pool_TempRequested group Pool-Einstellungen
attr Pool_TempRequested room 2.10 Pool
attr Pool_TempRequested setList state:slider,20,1,36
attr Pool_TempRequested webCmd state

#Manuelles DEaktivieren der Pumpe - wenn dieser Wert auf OFF geht wird keine Pumpe aktiviert -> Winterbetrieb!
define Pool_PumpMasterON dummy
attr Pool_PumpMasterON event-min-interval tick.*:0,.*:3600
attr Pool_PumpMasterON group Pool-Einstellungen
attr Pool_PumpMasterON room 2.10 Pool
attr Pool_PumpMasterON setList :on,off
attr Pool_PumpMasterON webCmd on:off

FHEM Diagramme anlegen:

define SVG_PoolTemps SVG myDbLog:SVG_PoolTemps:HISTORY
attr SVG_PoolTemps captionPos left
attr SVG_PoolTemps endPlotNow 1
attr SVG_PoolTemps room 2.10 Pool

gplot-Dateien:

# Created by FHEM/98_SVG.pm, 2019-09-11 13:04:50
set terminal png transparent size <SIZE> crop
set output '<OUT>.png'
set xdata time
set timefmt "%Y-%m-%d_%H:%M:%S"
set xlabel " "
set title 'Pooltemperaturen'
set ytics ("on" 95,"off" 85)
set y2tics 
set grid
set ylabel "Pumpe"
set y2label "Temperatur"
set yrange [0:100]
set y2range [0:50]

#myDbLog Pool_PumpActiveDachIst:state
#myDbLog Pool_PumpActiveZirkIst:state
#myDbLog Pool_TempRequested:state
#myDbLog Pool_dachRLTemp:state
#myDbLog Pool_poolTemp:state


plot "<IN>" using 1:2 axes x1y1 title 'Pumpe Dach IST' ls l7 lw 1 with steps,\
     "<IN>" using 1:2 axes x1y1 title 'Pumpe Zirkulation IST' ls l2 lw 0.5 with steps,\
     "<IN>" using 1:2 axes x1y2 title 'Pool Temperatur MAX' ls l1 lw 0.5 with steps,\
     "<IN>" using 1:2 axes x1y2 title 'Dach RL Temperatur' ls l3 lw 1 with lines,\
     "<IN>" using 1:2 axes x1y2 title 'Pool Temperatur IST' ls l0 lw 1 with lines

Phase 4: Hardware final zusammenbauen und in Gehäuse integrieren

Fazit: Zeit und Geduld beim Einbau und dann nochmals alles im „trockenen“ testen obs eh noch geht

Phase 5: Aufbau

Diese Phase hat einen ganzen Tag gekostet.

Relais mit Pumpen verkabeln, 1-Wire Sensoren an Datenbusleitungen (bis zu 30 m!) anlöten, Stecker drauf, Netzwerkkabel legen, Temperatursensoren aufs Dach zum Rücklauf der Heizungselemente legen + mit Silikon anlegen (um Auflagefläche des runden Sensors zu erhöhen) gegen Wind isolieren (Filz, Isomatten etc), Temepratursensor zum Pool legen, zwischen Isolierung und Poolfolie einpassen (Pool ist natürlich befüllt und der Sensor sollte mind. 30 cm unter der Wasseroberfläche sein).

Zusätzlich wurde parallel zum Relais der Umwälzpumpe1 (kleiner Kreislauf) ein Aufputz-Lichtschalter verbaut um die Pumpe jederzeit (zB zum Rückspülen des Filters) aktivieren zu können.

Winterbetrieb gibt es keinen – Pool wird bis auf ca 30 % entleert und es gibt keine laufenden Pumpen außerhalb des Sommers und damit keinen Steuerungsbedarf. Im Winter wird die ganze Steuerung ausgesteckt.

Das Ergebnis

Anmerkungen:

Dach-Vorlauftemperatur ist praktisch immer gleich der Pooltemperatur – daher kein Temperatursensor.

Darstellung in FHEM:

Darstellung der Werte in FHEM
Temperaturverlauf über einen Tag

Im Diagramm sieht man schön wie gegen 10 Uhr Vormittag die Temperatur am Dach-Rücklauf (magenta – Pumpe ist noch aus) über die Pool Temperatur steigt, und um ca 10:15 die Pumpe aktiviert wird (kleiner Einbruch der Rücklauftemperatur. Es wird der Pool geheizt, die Temperatur im Pool (rot) steigt, bis Abends gegen 18:00 die Temperatur am Dach-Rücklauf nicht mehr ausreicht und die Pumpe ausgeschalten wird. Die Maximaltemperatur (grün) wird hier nicht erreicht.

Bei Fragen gerne jederzeit melden bei Alex