/*
Forum: https://forum.arduino.cc/t/gleicher-code-uno-nano/1414771/38
Wokwi: https://wokwi.com/projects/448048248154617857
Vorläufer Wokwi: https://wokwi.com/projects/447981734015225857
Der Sketch entspricht dem Wokwi-Vorläuferstand mit folgenden Anpassungen:
Änderungen entsprechend Post 40 (wwerner) eingebracht
Konstante, Variable und Funktionen wurden jedoch in weiten Teilen
(aber nicht komplett) umgestellt, so dass sich kürzere Funktionen
ergeben und die Programmlogik etwas einfacher nachvollziehbar sein
sollte.
Die RTC kann bei Bedarf vor einem Neustart in Wokwi mittels Schiebeschalter
vom Controller getrennt werden, so dass das Einschaltverhalten des Sketches
geprüft werden kann: Sollte sich die RTC beim Start nicht initialisieren lassen,
so wird der weitere Sketch nicht ausgeführt; stattdessen blinkt die Speicher-Led
schnell.
Sollte die maximale Alarmanzahl beim Start des Sketches bereits ereicht sein
(aus dem EEPROM ausgelesener Zähler), dann wird der Alarm-Pin auf alarmOn gesetzt
und die Speicher-Led blinkt schnell. Der weitere Ablauf des Sketches wird gestoppt.
In Mod 2 vom 2025/11/20 wurden folgende Änderungen eingebracht:
Die Länge der char-Arrays in data2LogFile() wurde auf 8 Character erhöht.
Das Schreiben des logFiles wird nur dann versucht, wenn
a) die Initialisierung der SDCard erfolgreich war und
b) das File noch nicht existiert (um erneutes Einfügen des Headers bei Restart zu vermeiden)
2025/11/20
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
----------------------------------------------------------*/
// HIer folgen die WOKWI-spezifischen Anteile
#ifdef WOKWI
#include "DHTSensors.h"
DHTSensors sensors;
RTC_DS1307 rtc;
#else
// Hier die Anteile aus der Originalanwendung
constexpr uint8_t ONE_WIRE_BUS { 2 };
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
RTC_DS3231 rtc;
#endif
/* ----------------------------------------------------------
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