/*
Forum: https://forum.arduino.cc/t/gleicher-code-uno-nano/1414771/38
Wokwi: https://wokwi.com/projects/448227458899372033
Vorläufer Wokwi: https://wokwi.com/projects/448048248154617857
Der Sketch entspricht dem Wokwi-Vorläuferstand mit folgenden Anpassungen:
Die DHT22 wurden durch drei DS18B20 Sensoren ersetzt.
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 }; // .......................
// 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 tempVorlauf; // 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
// tempVorlauf = sensors.getTempCByIndex(1);
// gegen tempVorlauf = sensors.getTempC(sensorVlauf); ersetzen
//
// DeviceAddress sensorHzg = { 0x28, 0x55, 0x37, 0xB4, 0x00, 0x00, 0x00, 0x91 }; // Adresse Sensor 1
// DeviceAddress sensorVlauf = { 0x28, 0x4B, 0xCA, 0xB2, 0x00, 0x00, 0x00, 0x9A }; // Adresse Sensor 2
// DeviceAddress sensorWw = { 0x28, 0xEF, 0xD3, 0xB1, 0x00, 0x00, 0x00, 0x1D }; // Adresse Sensor 3
/* ----------------------------------------------------------
Starten der Seriellen Kommunikation
Initialisieren
der Ein- und Ausgabegeräte
des SDCard-Readers und der SD-Karte
der Echtzeituhr
des Alarmzählers
der Variablen für die Intervall-Steuerung
----------------------------------------------------------*/
void setup() {
Serial.begin(115200);
Serial.println(F("=== Heizungsüberwachung startet ==="));
initInputDevices();
initOutputDevices();
initSDCard();
initRTC();
initAlarmCounter();
lastCheckTime = millis();
}
/* ----------------------------------------------------------
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)
}
}
/* ----------------------------------------------------------
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);
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 (tempVorlauf > ALARM_ON_TEMP) {
AlarmCounter++;
alarmState = alarmOn;
data2LogFile("Alarm");
EEPROM.write(EEPROM_ADDR, AlarmCounter);
digitalWrite(ALARM_PIN, alarmOn);
}
} else {
if (tempVorlauf < 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(tempVorlauf, 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
---------------------------------------------------------*/
void readSensorData() {
tempVorlauf = sensors.getTempCByIndex(1);
tempVorlauf = temperatureOffset + tempVorlauf; // temperatureOffset zum Simulieren eingefügt
tempRueckl_HZG = sensors.getTempCByIndex(0);
tempRueckl_WW = sensors.getTempCByIndex(2);
sensors.requestTemperatures(); // löst neuen Lese-Zyklus aus! Braucht je nach Auflösung bis zu 750ms
}
/* --------------------------------------------------------
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();
}
}
}
Reset
BZ
Brenner
Ventil
Speichern
Alarm
Rücklauf
Heizung
Vorlauf
Heizung
Rücklauf
Warmwasser
RTC ein-/
ausschalten