/*
Forum: https://forum.arduino.cc/t/gleicher-code-uno-nano/1414771/38
Wokwi: https://wokwi.com/projects/448234802474619905
Vorläufer Wokwi: https://wokwi.com/projects/448227458899372033
Der Sketch entspricht dem Wokwi-Vorläuferstand mit folgenden Anpassungen:
Die DHT22 wurden durch drei DS18B20 ersetzt, deren individuelle Adressen
im Sketch eintragen sind, so dass die einzelnen Sensoren direkt adressiert und
abgefragt werden können.
Die Position der Sensoren von links nach rechts in der Simulation entspricht jetzt
der Datenreihenfolge in der CSV-Ausgabe (Vorlauf Hzg, Rücklauf Hzg, Rücklauf Warmwasser).
Die Adressen in der Wokwi-Simulation und der echten Anwendung unterscheiden sich,
diesem Zustand wird mittels #define WOKWI Rechnung getragen.
Auf Tastendruck (DS-Adressen) werden die Adressen der über OneWire angeschlossenen
Sensoren auf der seriellen Schnittstelle ausgegeben.
Stand Mod 4
2025/11/21
ec2021
===========================================================
Heizungsüberwachung – Arduino UNO R3
-----------------------------------------------------------
Funktionen:
- Messung Vorlauf/Rücklauf mit 3 x DS18B20 (OneWire-Bus)
- Erfassung der Zustände:
Brenner EIN/AUS / BZ
Ventil Heizung
Ventil Warmwasser
- Logging auf SD-Karte (nur bei Zustandsänderung)
- erweitert wenn Brenner Ein & BZ
loggen in sekundentakt
- Echtzeit über DS3231 RTC
- Alarm-Ausgang bei Vorlauf > 75 °C (mit Hysterese)
- zum Testen serielle Eingabe der Vorlauftemperatur 14.11.25
- Zähler der Alarme im EEPROM
Reset über Pin 5
-----------------------------------------------------------
Hardware:
- DS18B20 Data → Pin 2, Pullup 4,7kΩ nach 5V
- IO PINS
- Brenner
- BZ
- Ventil Warmwasser
- Alarm-LED/Relais
SD-Karte wie folgt an SPI-Bus angeschlossen:
- MOSI - Pin 11 auf Arduino Uno/Nano/Duemilanove/Diecimila
- MISO - Pin 12 auf Arduino Uno/Nano/Duemilanove/Diecimila
- CLK - Pin 13 auf Arduino Uno/Nano/Duemilanove/Diecimila
- SD-CS -Pin 10
- RTC DS3231 via I2C (A4 = SDA, A5 = SCL)
===========================================================
*/
#define WOKWI
#include <OneWire.h>
#include <DallasTemperature.h>
#include <Wire.h>
#include <RTClib.h>
#include <SPI.h>
#include <SD.h>
#include <EEPROM.h>
// Input Pins
constexpr uint8_t VENTIL_WW_PIN{ 4 }; // Warmwasser-Ventil
constexpr uint8_t RESET_PIN{ 5 }; // zum Reset des Alarmzählers beim Start
constexpr uint8_t BRENNER_PIN{ 6 }; // Brenner Ein/Aus-Signal
constexpr uint8_t BZ_PIN{ 7 }; // .......................
constexpr uint8_t ADDR_PIN{ 3 }; // Taste zur Ausgabe der DS18B20 Adressen
// Output Pins
constexpr uint8_t ALARM_PIN{ 8 }; // Alarm-Ausgang hier Led, später Relais
constexpr uint8_t SP_PIN{ 9 }; // SDCard-Led: blinkt, wenn das Speichern auf SDCard erfolgreich, sonst aus
constexpr uint8_t SD_CS_PIN{ 10 }; // Chip-Select des SDCard-Readers
// // Globale Konstanten für:
constexpr uint8_t EEPROM_ADDR{ 0 }; // Adresse im EEPROM, an der der Zähler gespeichert wird
constexpr uint32_t INTERVAL { 1000 }; // Update-Intervall für Temperatur und Abfrage der Input-Pins
constexpr char heizungFile[] { "heizung.csv" }; // Logfile-Name
// Alarm
constexpr uint8_t ALARM_ON_TEMP{ 75 }; // Schwellwert für Alarm EIN über 75°C
constexpr uint8_t ALARM_OFF_TEMP{ 68 }; // Schwellwert für Alarm AUS unter 68°C
constexpr uint8_t MAX_ALARM_ANZAHL {4}; // Maximal zulässige Anzahl für automatisches Rücksetzen des Alarms
// Alarmstatus-Konstanten
constexpr bool alarmOn { LOW }; // Status, wenn Led/Relais eingeschaltet
constexpr bool alarmOff{ !alarmOn }; // Status, wenn led/Relais ausgeschaltet
/* ----------------------------------------------------------
Globale Objekte
----------------------------------------------------------*/
constexpr uint8_t ONE_WIRE_BUS { 2 };
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
RTC_DS3231 rtc;
/* ----------------------------------------------------------
Globale Variablen
----------------------------------------------------------*/
// Alarm
uint8_t AlarmCounter = 0; // Zählt, wie häufig der Alarmzustand eingetreten ist
bool alarmState = alarmOff; // Start mit Alarmstatus Off
// SDCardReader
boolean SDCardAvailable = false; // Ist true, wenn der SDCard-Reader sich initialisieren lässt
bool writeSuccess = false; // Flag, das den Erfolg des letzten SDCard-Schreibvorgangs speichert
// Status der Eingaben
bool brennerStatus;
bool BZStatus;
bool ventilWWStatus;
// Temperatur-Variable
float tempVorl_Hzg; // Vorlauftemperatur Heizung
float tempRueckl_HZG; // Rücklauftemperatur Heizung
float tempRueckl_WW; // Rücklauftemperatur Warmwasser
float temperatureOffset = 0.0; // Offset für den Heizungsvorlauf zum Testen der Alarmschwellen
// Variable für die Intervall-Steuerung
uint32_t lastCheckTime; // Merker, wann das letzte Mal die Sensoren abgefragt wurden
// ----------------------------------------------------------
// Adressen der Sensoren 3.11.25
// mit den Adressen könnte ich
// tempVorl_Hzg = sensors.getTempCByIndex(1);
// gegen tempVorl_Hzg = sensors.getTempC(sensorVlauf); ersetzen
#ifdef WOKWI // Hier folgen die simulationstypischen Adressen der DS18B20
DeviceAddress sensor_Rueckl_Hzg = { 0x10, 0x28, 0x55, 0x37, 0xB4, 0x00, 0x00, 0x47};
DeviceAddress sensor_Vorl_Hzg = { 0x10, 0x28, 0x4B, 0xCA, 0xB2, 0x00, 0x00, 0xA4};
DeviceAddress sensor_Rueckl_WW = { 0x10, 0x28, 0xEF, 0xD3, 0xB1, 0x00, 0x00, 0xC7};
#else // Dies sind die vom TO angegebenen Adressen seiner DS18B20
DeviceAddress sensor_Rueckl_Hzg = { 0x28, 0x55, 0x37, 0xB4, 0x00, 0x00, 0x00, 0x91 }; // Adresse Sensor 1
DeviceAddress sensor_Vorl_Hzg = { 0x28, 0x4B, 0xCA, 0xB2, 0x00, 0x00, 0x00, 0x9A }; // Adresse Sensor 2
DeviceAddress sensor_Rueckl_WW = { 0x28, 0xEF, 0xD3, 0xB1, 0x00, 0x00, 0x00, 0x1D }; // Adresse Sensor 3
#endif
/* ----------------------------------------------------------
Setup der Komponenten
----------------------------------------------------------*/
void setup() {
Serial.begin(115200); // Starten der Seriellen Kommunikation
Serial.println(F("=== Heizungsüberwachung startet ==="));
initInputDevices(); // Initialisierung der Eingabegeräte
initOutputDevices(); // der Ausgabegeräte
initSDCard(); // des SDCard-Readers und der SD-Karte
initRTC(); // des Echtzeituhr
initAlarmCounter(); // des Alarmzählers
lastCheckTime = millis(); // der Variablen für die Intervall-Steuerung
}
/* ----------------------------------------------------------
Hauptschleife
---------------------------------------------------------*/
void loop() {
if (millis() - lastCheckTime >= INTERVAL) { // Im Abstand von INTERVAL [ms] erfolgt in der loop():
lastCheckTime = millis(); // das Speichern der aktuellen millis()-Zeit für den nächsten Aufruf
readPins(); // das Einlesen der Signale BZ, Brenner und Ventil und das Loggen bei Änderung
readSensorData(); // das Einlesen der Temperaturdaten
checkAlarm(); // die Prüfung gegen die Alarmschwellen
logIfBrennerOn(); // bei eingeschaltetem Brenner das regelmäßige Loggen aller Daten
handleWriteLed(); // das Schalten der Speicher-Led (bei Erfolg Blinken, sonst Led aus)
simulateTemp(); // die Möglichkeit zur Eingabe einer OffsetTemperatur (Testmöglichkeit)
}
retrieveDSAddresses(); // die Adressen der angeschlossenen Ds18B20 auf Tastendruck ausgeben
}
/* ----------------------------------------------------------
Alarmzähler initialisieren
---------------------------------------------------------*/
void initAlarmCounter() {
if (digitalRead(RESET_PIN) == LOW) {
EEPROM.write(EEPROM_ADDR, 0);
AlarmCounter = 0;
Serial.println(F("Alarmzähler wurde genullt (Reset-Pin war LOW)."));
} else {
AlarmCounter = EEPROM.read(EEPROM_ADDR);
Serial.print(F("Alarmzähler aus EEPROM geladen: "));
Serial.println(AlarmCounter);
}
// Falls bereits MAX_ALARM_ANZAHL erreicht wurde,
// wird der ALARM_PIN auf alarmOn gesetzt und
// die Speicher-Led beginnt schnell zu blinken, ansonsten
// stoppt der Sketch an dieser Stelle die weiteren Schritte
if (AlarmCounter >= MAX_ALARM_ANZAHL) {
digitalWrite(ALARM_PIN, alarmOn);
waitForEver();
}
// Speicher-Led dem Zählerstand entsprechend blinken lassen
for (uint8_t i = 0; i < AlarmCounter; i++) {
digitalWrite(SP_PIN, HIGH);
delay(1000);
digitalWrite(SP_PIN, LOW);
delay(1000);
}
}
/* ----------------------------------------------------------
Echtzeituhr initialisieren
---------------------------------------------------------*/
void initRTC() {
// RTC starten
if (!rtc.begin()) {
Serial.println(F("RTC nicht gefunden!"));
Serial.println(F("Die Überwachung wird gestoppt"));
waitForEver();
}
#ifndef WOKWI
if (rtc.lostPower()) {
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
#endif
}
/* ----------------------------------------------------------
Falls die Echtzeituhr nicht gestartet werden kann,
blinkt die Speicher-Led im blinkInterval ms-Takt und
der weitere Sketch wird nicht ausgeführt!
---------------------------------------------------------*/
constexpr unsigned long blinkInterval {150};
void waitForEver() {
lastCheckTime = millis();
byte state = LOW;
while (1) {
if (millis() - lastCheckTime >= blinkInterval) { // Im Abstand von blinkInterval [ms] blinken
lastCheckTime = millis();
digitalWrite(SP_PIN, state);
state = !state;
}
}
}
/* ----------------------------------------------------------
SDCardReader initialisieren und Logfile anlegen
---------------------------------------------------------*/
void initSDCard() {
SDCardAvailable = SD.begin(SD_CS_PIN);
writeSuccess = false;
if (!SDCardAvailable) {
Serial.println(F("SD-Karte nicht initialisierbar oder nicht vorhanden."));
} else {
Serial.println(F("SD-Karte initialisiert."));
writeSuccess = SD.exists(heizungFile);
if (!writeSuccess) {
char buf[] = "Datum,Uhrzeit,Vorl [°C], HZG [°C], WW [°C] , WW , BR , BZ , Al , ,ALZä";
writeSuccess = printLineToLog(buf);
}
}
Serial.print(F("System "));
if (!writeSuccess) {
Serial.print(F("nicht "));
}
Serial.println(F("bereit"));
}
/* ----------------------------------------------------------
Eingabegeräte initialisieren
---------------------------------------------------------*/
void initInputDevices() {
pinMode(BRENNER_PIN, INPUT_PULLUP);
pinMode(RESET_PIN, INPUT_PULLUP);
pinMode(VENTIL_WW_PIN, INPUT_PULLUP);
pinMode(BZ_PIN, INPUT_PULLUP);
pinMode(ADDR_PIN, INPUT_PULLUP);
sensors.begin();
sensors.requestTemperatures(); // löst den ersten(!) Lese-Zyklus aus. Braucht je nach Auflösung bis zu 750ms
}
/* ----------------------------------------------------------
Ausgaben initialisieren
---------------------------------------------------------*/
void initOutputDevices() {
pinMode(ALARM_PIN, OUTPUT);
digitalWrite(ALARM_PIN, alarmOff); // Anfangszustand: aus
pinMode(SP_PIN, OUTPUT);
digitalWrite(SP_PIN, false); // Anfangszustand: aus
}
/* ----------------------------------------------------------
Loggen, wenn Brenner eingeschaltet ist
---------------------------------------------------------*/
void logIfBrennerOn() {
if (digitalRead(BRENNER_PIN) == LOW && digitalRead(BZ_PIN) == LOW) {
data2LogFile("BR");
}
}
/* ----------------------------------------------------------
Blinken wenn erfolgreich ins Logfile geschrieben wurde,
sonst LED ausschalten
---------------------------------------------------------*/
void handleWriteLed() {
digitalWrite(SP_PIN, (writeSuccess == true) ? !digitalRead(SP_PIN) : false);
}
/* --------------------------------------------------------
Prüfen, ob die Alarmbedingungen erreicht wurden mit Hysterese:
Nach dem Ereeichen von MAX_ALARM_ANZAHL wird die Led/das Relais
nicht mehr automatisch zurückgesetzt
-------------------------------------------------------- */
void checkAlarm() {
if (alarmState == alarmOff) {
if (tempVorl_Hzg > ALARM_ON_TEMP) {
AlarmCounter++;
alarmState = alarmOn;
data2LogFile("Alarm");
EEPROM.write(EEPROM_ADDR, AlarmCounter);
digitalWrite(ALARM_PIN, alarmOn);
}
} else {
if (tempVorl_Hzg < ALARM_OFF_TEMP && AlarmCounter < MAX_ALARM_ANZAHL) {
alarmState = alarmOff;
digitalWrite(ALARM_PIN, alarmOff);
}
}
}
/* --------------------------------------------------------
Loggen bei Änderung
---------------------------------------------------------*/
void data2LogFile(char * txt) {
DateTime now;
now = rtc.now();
char buf[120];
char vlT[8]; // Zur Sicherheit 8 Zeichen (für "-127.0" plus `\0` genügen eigentlich 7)
char rlH[8] ;
char rlW[8] ;
dtostrf(tempVorl_Hzg, 2, 1, vlT);
dtostrf(tempRueckl_HZG, 2, 1, rlH);
dtostrf(tempRueckl_WW, 2, 1, rlW);
snprintf(buf, sizeof(buf), "%02d.%02d.%d,%02d:%02d:%02d,%s,%s,%s,%d,%d,%d,%d,%s,%d",
now.day(), now.month(), now.year(), now.hour(), now.minute(), now.second(),
vlT, rlH, rlW, ventilWWStatus, brennerStatus, BZStatus, alarmState == alarmOff ? 0 : 1, txt, AlarmCounter);
writeSuccess = printLineToLog(buf);
}
/* --------------------------------------------------------
Die eigentliche Logfile-Schreibroutine
---------------------------------------------------------*/
boolean printLineToLog(char * txt) {
if (SDCardAvailable) {
File logfile = SD.open(heizungFile, FILE_WRITE);
if (logfile) {
logfile.println(txt);
logfile.close();
Serial.println(txt);
return true;
}
}
return false;
}
/* --------------------------------------------------------
Digitale Eingänge lesen (bei Optokopplern evtl. invertiert)
und Änderungen unmittelbar in das Log-File schreiben
---------------------------------------------------------*/
void readPins() {
if (!digitalRead(BRENNER_PIN) != brennerStatus) {
brennerStatus = !brennerStatus;
data2LogFile("LÜ");
}
if (!digitalRead(VENTIL_WW_PIN) != ventilWWStatus) {
ventilWWStatus = !ventilWWStatus;
data2LogFile( "WW");
}
if (!digitalRead(BZ_PIN) != BZStatus) {
BZStatus = !BZStatus;
data2LogFile("BZ");
}
}
/* --------------------------------------------------------
Temperaturen einlesen
sensor_Vorl_Hzg
sensor_Rueckl_Hzg
sensor_Rueckl_WW
---------------------------------------------------------*/
void readSensorData() {
tempRueckl_HZG = sensors.getTempC(sensor_Rueckl_Hzg);
tempVorl_Hzg = sensors.getTempC(sensor_Vorl_Hzg);
tempRueckl_WW = sensors.getTempC(sensor_Rueckl_WW);
tempVorl_Hzg = temperatureOffset + tempVorl_Hzg; // temperatureOffset zum Simulieren eingefügt
sensors.requestTemperatures(); // löst neuen Lese-Zyklus aus! Braucht je nach Auflösung bis zu 750ms
}
/* --------------------------------------------------------
Adressen der DS18b20 seriell ausgeben
---------------------------------------------------------*/
void retrieveDSAddresses() {
if (digitalRead(ADDR_PIN) == LOW) {
byte count = 0;
byte addr[8];
oneWire.reset_search();
Serial.println(F("Starte Adress-Suche ..."));
while (oneWire.search(addr)) {
count++;
Serial.print(F("DeviceAddress sensor"));
Serial.print(count);
Serial.print(F(" = { "));
for (int i = 0; i < 8; i++) {
Serial.print(F("0x"));
Serial.print(addr[i], HEX);
Serial.print((i == 7) ? "};\n" : ", " );
}
}
while (digitalRead(ADDR_PIN) == LOW);
Serial.println(F("Keine weiteren Adressen"));
}
}
/* --------------------------------------------------------
Offset-Temperatur zur Prüfung der Alarmschwellen
per Serial eingeben
---------------------------------------------------------*/
void simulateTemp() {
if (Serial.available() > 0) {
float eingabe = Serial.parseFloat();
if (eingabe != 0.0 || (eingabe == 0.0 && Serial.peek() == '0')) {
temperatureOffset = eingabe;
Serial.print(F("Temperatur auf manuellen Wert gesetzt: "));
Serial.println(temperatureOffset);
} else {
Serial.println(F("Ungueltige Eingabe."));
}
while (Serial.available() > 0) {
Serial.read();
}
}
}
DS-
Adressen
BZ
Brenner
Ventil
Speichern
Alarm
Rücklauf
Heizung
Vorlauf
Heizung
Rücklauf
Warmwasser
RTC ein-/
ausschalten