Files
Heizung/heizung.ino
T
2026-04-15 17:05:07 +02:00

589 lines
19 KiB
Arduino
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Heizungssteuerung – millis()-basiert, kein blockierendes delay()
// Kommunikation mit Raspberry Pi über Serial (JSON)
// Befehle vom Raspi: SET <key> <value>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <RTClib.h>
#include <LiquidCrystal.h>
#include <max6675.h>
#include <Wire.h>
// Adafruit_GFX und Adafruit_SSD1306 entfernt – OLED aktuell nicht genutzt,
// das Objekt würde ~1 KB RAM als Frame-Buffer belegen.;
#define ONE_WIRE_BUS 31
RTC_DS3231 rtc;
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
// Relais Pins
const int relais1 = 34; // Mischer –
const int relais2 = 32; // Mischer +
const int relais3 = 30; // Heizkreispumpe
const int relais4 = 28;
const int relais5 = 26; // Kesselpumpe / Pufferbefüllung
const int relais6 = 24;
const int relais7 = 22;
const int relais8 = 36;
// LCD
const int rs = 12, en = 11, d4 = 5, d5 = 4, d6 = 3, d7 = 2;
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);
// MAX6675 Abgastemperatur
const int max6675SO = 8;
const int max6675CS = 9;
const int max6675CLK = 10;
MAX6675 ktc(max6675CLK, max6675CS, max6675SO);
// Sensor-Adressen
DeviceAddress wohnung = {0x28, 0xA3, 0x9A, 0x61, 0x2F, 0x21, 0x01, 0x9E};
DeviceAddress Kunten = {0x28, 0x51, 0x4D, 0x56, 0x2F, 0x21, 0x01, 0x43};
DeviceAddress Koben = {0x28, 0x81, 0x87, 0x64, 0x2F, 0x21, 0x01, 0x57};
DeviceAddress Vorlauf = {0x28, 0x22, 0xF7, 0x60, 0x2F, 0x21, 0x01, 0x31};
DeviceAddress Aussen = {0x28, 0xE8, 0x19, 0x60, 0x2F, 0x21, 0x01, 0x19};
// ============================================================
// Einstellbare Parameter (änderbar per Serial-Befehl vom Raspi)
// ============================================================
float time_adjust = 140.0; // Sekunden – Mischer-Laufzeit von ganz-zu bis ganz-auf
float neigung = 1.2; // Heizkurven-Neigung
int rtsoll = 20; // Raumtemperatur Soll (°C)
int nachtabs = 3; // Nachtabsenkung (Grad)
int tag_anfang = 4; // Stunde: Tagesbetrieb beginnt
int tag_ende = 20; // Stunde: Tagesbetrieb endet
int misch_begin = 12; // Heizung läuft wenn Aussentemperatur <= diesem Wert
// ============================================================
// Interne Zustandsvariablen
// ============================================================
float temp_aussen = 0;
float temp_vorlauf = 0;
float temp_koben = 0;
float temp_kunten = 0;
float temp_wohnung = 0;
float temp_abgas = 0;
float vtsoll = 0;
float niveau = 0;
float darw = 0;
int stellung = 0; // aktuelle Mischerstellung in Sekunden (0 = kalt, time_adjust = warm)
int mischeron = 0;
const int arranz = 7; // Anzahl Anzeige-Einträge
// ============================================================
// System-Zustandsautomat
// ============================================================
enum SystemState { STARTUP, RUNNING };
SystemState systemState = STARTUP;
unsigned long startupStart = 0;
// ============================================================
// Sensor-Timing
// ============================================================
unsigned long sensorTimer = 0;
const unsigned long SENSOR_INTERVAL = 10000UL; // alle 10 Sekunden Temp lesen
bool sensorsRequested = false;
unsigned long sensorRequestTime = 0;
const unsigned long SENSOR_WAIT = 800UL; // DS18B20 Konvertierungszeit
// ============================================================
// Mischer-Puls (non-blocking, abweichungsabhängige Pulszeit)
// ============================================================
enum MischerPulse { PULSE_NONE, PULSE_PLUS, PULSE_MINUS };
MischerPulse currentPulse = PULSE_NONE;
unsigned long mischerPulseStart = 0;
unsigned long activePulseTime = 4000UL; // wird vor jedem Puls gesetzt
// Zonalregelung: Schwellwerte der Vorlauf-Abweichung (°C)
// Der Sensor sitzt außen am Rohr → Totzeit ca. 2-5 Min.
// Bei großer Abweichung aggressiv regeln, bei kleiner vorsichtig (kein Überschwingen)
const float ZONE_SPRINT = 8.0; // >8°C Abweichung → maximale Energie
const float ZONE_JOGGING = 4.0; // 4-8°C → zügig
const float ZONE_NORMAL = 1.5; // 1.5-4°C → fein / Totband-Grenze
// Pulszeiten pro Zone (öffnen / Richtung +)
const unsigned long PULS_SPRINT = 12000UL; // 12 Sekunden
const unsigned long PULS_JOGGING = 6000UL; // 6 Sekunden
const unsigned long PULS_NORMAL = 3000UL; // 3 Sekunden
// Schließen (Richtung −): zonenabhängig, aber etwas kürzer als Plus (Sensor-Trägheit)
const unsigned long PULS_MINUS_SPRINT = 8000UL; // 8 Sekunden
const unsigned long PULS_MINUS_JOGGING = 4000UL; // 4 Sekunden
const unsigned long PULS_MINUS_NORMAL = 2000UL; // 2 Sekunden
// Check-Intervalle pro Zone
const unsigned long CHECK_SPRINT = 25000UL; // 25 Sekunden
const unsigned long CHECK_JOGGING = 45000UL; // 45 Sekunden
const unsigned long CHECK_NORMAL = 70000UL; // 70 Sekunden
const unsigned long CHECK_TOTBAND = 90000UL; // 90 Sekunden (nur Überwachung)
// Wartezeit zwischen Mischer-Korrekturen (wird dynamisch aus Zone gesetzt)
unsigned long mischerCheckTimer = 0;
unsigned long dynamicCheckInterval = CHECK_TOTBAND; // aktuell gültiges Intervall
// Anlaufphase: nach mischeron 0→1 weiß der Regler dass das Rohr kalt ist.
// → erste Prüfung nach 20 s, danach 5 Min. lang aggressiverer Modus.
unsigned long mischeronStart = 0;
bool mischeronWarmup = false; // true = Anlaufphase aktiv
const unsigned long WARMUP_DAUER = 300000UL; // 5 Minuten Anlaufphase
// ============================================================
// Display-Zyklus (ersetzt delay(3000) im Display-Loop)
// ============================================================
unsigned long displayTimer = 0;
const unsigned long DISPLAY_INTERVAL = 3000UL; // 3 Sekunden pro Sensor
int displayIndex = 0;
bool showProzent = false;
unsigned long prozentTimer = 0;
const unsigned long PROZENT_DURATION = 3000UL;
// ============================================================
// Serial-Ausgabe an Raspberry Pi
// ============================================================
unsigned long serialOutTimer = 0;
const unsigned long SERIAL_OUT_INTERVAL = 2000UL; // alle 2 Sekunden JSON senden
// ============================================================
// SETUP
// ============================================================
void setup() {
Serial.begin(9600);
Serial.setTimeout(100); // readStringUntil() bricht nach 100ms ab statt 1s
//rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
sensors.begin();
rtc.begin();
const int relaisPins[] = {
relais1,
relais2,
relais3,
relais4,
relais5,
relais6,
relais7,
relais8
};
for (int i = 0; i < 8; i++) {
pinMode(relaisPins[i], OUTPUT);
digitalWrite(relaisPins[i], HIGH); // HIGH = Relais aus (ActiveLow)
}
// Kesselpumpe sofort an wenn Abgas heiß
if (ktc.readCelsius() >= 40) {
digitalWrite(relais5, LOW);
}
lcd.begin(16, 2); // einmalige Initialisierung – danach NICHT mehr aufrufen!
lcd.print(" Initialisiere");
lcd.setCursor(0, 1);
lcd.print("Mischer -> kalt");
// Mischer auf ganz-kalt fahren (non-blocking: Relais an, Zeit merken)
digitalWrite(relais1, LOW);
startupStart = millis();
systemState = STARTUP;
}
// ============================================================
// LOOP
// ============================================================
void loop() {
unsigned long now = millis();
// --- STARTUP: Mischer vollständig zurückdrehen ---
if (systemState == STARTUP) {
if (now - startupStart >= (unsigned long)(time_adjust * 1000UL)) {
digitalWrite(relais1, HIGH); // Relais aus – Mischer stoppt
stellung = 0;
systemState = RUNNING;
// Timer initialisieren
sensorTimer = now;
displayTimer = now;
serialOutTimer = now;
mischerCheckTimer = now;
// Erste Sensorabfrage starten
sensors.requestTemperatures();
sensorsRequested = true;
sensorRequestTime = now;
lcd.clear();
lcd.print("Bereit");
}
// Während Startup: nichts weiter tun
return;
}
// --- RUNNING ---
handleSerialInput();
// Sensorabfrage periodisch starten
if (!sensorsRequested && (now - sensorTimer >= SENSOR_INTERVAL)) {
sensorTimer = now;
sensors.requestTemperatures();
sensorsRequested = true;
sensorRequestTime = now;
}
// Sensorwerte auslesen nach Konvertierungszeit
if (sensorsRequested && (now - sensorRequestTime >= SENSOR_WAIT)) {
sensorsRequested = false;
readSensors();
updateLogic(now);
}
// Mischer-Relais-Puls überwachen
handleMischerPulse(now);
// Display-Zyklus
updateDisplay(now);
// JSON an Raspberry Pi senden
if (now - serialOutTimer >= SERIAL_OUT_INTERVAL) {
serialOutTimer = now;
sendSerialJSON();
}
}
// ============================================================
// Sensor-Werte auslesen
// ============================================================
// DS18B20 Fehlerwerte: -127°C = Kabelbruch/nicht angeschlossen, 85°C = Initialisierungswert
// Bei Fehler: letzten gültigen Wert behalten (wird nicht überschrieben)
#define SENSOR_ERR_LOW -100.0
#define SENSOR_ERR_HIGH 84.0
static void readSensor(float &dest, float raw) {
if (raw > SENSOR_ERR_LOW && raw < SENSOR_ERR_HIGH) {
dest = raw;
} else {
Serial.print(F("# Sensorfehler: "));
Serial.println(raw, 1);
// dest bleibt unverändert (letzter gültiger Wert)
}
}
void readSensors() {
readSensor(temp_aussen, sensors.getTempC(Aussen));
readSensor(temp_vorlauf, sensors.getTempC(Vorlauf));
readSensor(temp_koben, sensors.getTempC(Koben));
readSensor(temp_kunten, sensors.getTempC(Kunten));
readSensor(temp_wohnung, sensors.getTempC(wohnung));
temp_abgas = ktc.readCelsius(); // MAX6675 hat eigenes Fehlerprotokoll
}
// ============================================================
// Heizungslogik
// ============================================================
void updateLogic(unsigned long now) {
DateTime aktuell = rtc.now();
int jetzt = aktuell.hour();
// Kesselpumpe: an wenn Abgas >= 40°C
if (temp_abgas >= 40) {
digitalWrite(relais5, LOW);
} else {
digitalWrite(relais5, HIGH);
}
// Heizkurve: Vorlaufsoll berechnen
darw = temp_aussen - (float)rtsoll;
float rtsoll_eff = (float)rtsoll;
if (jetzt < tag_anfang || jetzt >= tag_ende) {
rtsoll_eff -= (float)nachtabs; // Nachtabsenkung
}
vtsoll = rtsoll_eff + niveau
- neigung * darw * (1.4347 + 0.021 * darw + 247.9e-6 * darw * darw);
// Sicherheitsbegrenzung: vtsoll physikalisch sinnvoll halten
// Schützt vor Extremwerten bei Sensorfehler (z.B. -127°C Kabelbruch)
if (vtsoll < 10.0) vtsoll = 10.0;
if (vtsoll > 75.0) vtsoll = 75.0;
// Heizkreispumpe / Mischer-Freigabe
bool pufferOK = (temp_koben >= vtsoll);
bool aussenKalt = (temp_aussen <= (float)misch_begin);
bool abgasHot = (temp_abgas >= 40);
int mischeronAlt = mischeron;
if (aussenKalt && (pufferOK || abgasHot)) {
digitalWrite(relais3, LOW);
mischeron = 1;
} else {
digitalWrite(relais3, HIGH);
mischeron = 0;
}
// Anlaufphase erkennen: wenn Mischer gerade eingeschaltet wurde
if (mischeron == 1 && mischeronAlt == 0) {
// Übergang 0→1: Rohr ist kalt, sofort erste Prüfung in 20 Sekunden
mischeronWarmup = true;
mischeronStart = now;
mischerCheckTimer = now - CHECK_SPRINT + 20000UL; // erste Prüfung nach 20s
Serial.println(F("# Anlaufphase gestartet"));
}
if (mischeron == 0) {
mischeronWarmup = false; // Anlaufphase beenden wenn Heizung aus
}
bool inWarmup = mischeronWarmup && (now - mischeronStart < WARMUP_DAUER);
// ── Zonalregelung ──────────────────────────────────────────────────────────
// Abweichung positiv = Vorlauf zu kalt, negativ = zu warm
float abweichung = vtsoll - temp_vorlauf;
float abweichungAbs = fabs(abweichung);
// Regelzone bestimmen → gibt Pulszeit und Check-Intervall vor.
// WICHTIG: activePulseTime wird NUR aktualisiert wenn kein Puls läuft.
// Würden wir es mid-flight ändern, würde handleMischerPulse() den Puls
// zum falschen Zeitpunkt beenden und stellung falsch berechnen.
// Während Anlaufphase: eine Zone aggressiver, da das Rohr erst aufgeheizt werden muss.
if (abweichungAbs > ZONE_SPRINT || (inWarmup && abweichungAbs > ZONE_JOGGING)) {
dynamicCheckInterval = CHECK_SPRINT;
if (currentPulse == PULSE_NONE) activePulseTime = PULS_SPRINT;
} else if (abweichungAbs > ZONE_JOGGING || (inWarmup && abweichungAbs > ZONE_NORMAL)) {
dynamicCheckInterval = CHECK_JOGGING;
if (currentPulse == PULSE_NONE) activePulseTime = PULS_JOGGING;
} else if (abweichungAbs > ZONE_NORMAL) {
dynamicCheckInterval = CHECK_NORMAL;
if (currentPulse == PULSE_NONE) activePulseTime = PULS_NORMAL;
} else {
// Totband: Mischer sitzt richtig, nur überwachen
dynamicCheckInterval = CHECK_TOTBAND;
if (currentPulse == PULSE_NONE) activePulseTime = PULS_NORMAL;
}
// Mischer regeln – nur wenn Heizkreis läuft, kein Puls aktiv
// und das zonenabhängige Check-Intervall abgelaufen ist
if (mischeron == 1 && currentPulse == PULSE_NONE &&
(now - mischerCheckTimer >= dynamicCheckInterval)) {
if (abweichung > ZONE_NORMAL && stellung < (int)time_adjust) {
// Mischer weiter öffnen (wärmer)
// activePulseTime wurde oben zonenabhängig gesetzt
currentPulse = PULSE_PLUS;
mischerPulseStart = now;
digitalWrite(relais2, LOW);
Serial.print(F("# Mische + Puls="));
Serial.print(activePulseTime / 1000);
Serial.print(F("s Abw="));
Serial.println(abweichung, 1);
} else if (abweichung < -ZONE_NORMAL && stellung > 0) {
// Mischer schließen (kälter) – zonenabhängig, etwas kürzer als Plus
if (abweichungAbs > ZONE_SPRINT) {
activePulseTime = PULS_MINUS_SPRINT;
} else if (abweichungAbs > ZONE_JOGGING) {
activePulseTime = PULS_MINUS_JOGGING;
} else {
activePulseTime = PULS_MINUS_NORMAL;
}
currentPulse = PULSE_MINUS;
mischerPulseStart = now;
digitalWrite(relais1, LOW);
Serial.print(F("# Mische - Abw="));
Serial.println(abweichung, 1);
}
}
}
// ============================================================
// Mischer-Puls beenden wenn Zeit abgelaufen
// ============================================================
void handleMischerPulse(unsigned long now) {
if (currentPulse == PULSE_NONE) return;
if (now - mischerPulseStart >= activePulseTime) {
// Stellung um die tatsächliche Pulszeit (in Sekunden) aktualisieren
int pulsSekunden = (int)(activePulseTime / 1000UL);
if (currentPulse == PULSE_PLUS) {
digitalWrite(relais2, HIGH);
stellung += pulsSekunden;
if (stellung > (int)time_adjust) stellung = (int)time_adjust;
} else {
digitalWrite(relais1, HIGH);
stellung -= pulsSekunden;
if (stellung < 0) stellung = 0;
}
currentPulse = PULSE_NONE;
mischerCheckTimer = now; // Check-Timer neu starten (nächstes Intervall läuft ab jetzt)
// Prozentanzeige für 3 Sekunden einblenden
showProzent = true;
prozentTimer = now;
//showProzentDisplay();
}
}
// ============================================================
// Display-Zyklus (non-blocking)
// ============================================================
void updateDisplay(unsigned long now) {
// Prozentanzeige: solange aktiv, nichts anderes zeigen
if (showProzent) {
if (now - prozentTimer >= PROZENT_DURATION) {
showProzent = false;
displayTimer = now; // Zyklus-Timer zurücksetzen
}
return;
}
if (now - displayTimer >= DISPLAY_INTERVAL) {
displayTimer = now;
displayIndex = (displayIndex + 1) % arranz;
showSensor();
}
}
//void showProzentDisplay() {
// float proz = (stellung * 100.0) / time_adjust;
// lcd.clear();
// lcd.print("Mischer ");
// lcd.print(proz, 0);
// lcd.print("%");
// lcd.setCursor(0, 1);
// lcd.print("geoeffnet");
//}
void showSensor() {
char Uhr[] = "hh:mm";
DateTime aktuell = rtc.now();
float sensorVal[7] = {
temp_aussen, temp_vorlauf, vtsoll,
temp_koben, temp_kunten, temp_wohnung, temp_abgas
};
const char* ort[7] = {
"Aussentemp", "Vorlauf ist", "Vorlauf soll",
"Puffer Oben", "Puffer unt.", "Heizung", "Abgas"
};
lcd.clear();
lcd.begin(16, 2);
lcd.print(ort[displayIndex]);
lcd.setCursor(0, 1);
lcd.print(sensorVal[displayIndex], 1);
lcd.print("\337C ");
lcd.setCursor(11, 1);
lcd.print(aktuell.toString(Uhr));
// showProzentDisplay();
// Sonderanzeige Mischer-Grenzwerte – nur wenn Mischer aktiv läuft
//if (mischeron) {
// if (stellung >= (int)time_adjust) {
// lcd.clear();
// lcd.print("Mischer max!");
// lcd.setCursor(0, 1);
// lcd.print("voll geoeffnet");
// } else if (stellung <= 0) {
// lcd.clear();
// lcd.print("Mischer min");
// lcd.setCursor(0, 1);
// lcd.print("sehr gut !");
// }
//}
}
// ============================================================
// JSON-Ausgabe an Raspberry Pi
// ============================================================
void sendSerialJSON() {
char buffer[512];
float abw = vtsoll - temp_vorlauf;
snprintf(buffer, sizeof(buffer),
"{"
"\"aussentemp\":%.1f,"
"\"vorlauf_ist\":%.1f,"
"\"vorlauf_soll\":%.1f,"
"\"kessel_oben\":%.1f,"
"\"kessel_unten\":%.1f,"
"\"wohnung\":%.1f,"
"\"abgas\":%.1f,"
"\"stellung\":%d,"
"\"mischeron\":%d,"
"\"neigung\":%.2f,"
"\"rtsoll\":%d,"
"\"misch_begin\":%d,"
"\"nachtabs\":%d,"
"\"tag_anfang\":%d,"
"\"tag_ende\":%d,"
"\"time_adjust\":%d,"
"\"abweichung\":%.1f,"
"\"check_intervall\":%ld,"
"\"puls_zeit\":%ld,"
"\"puls_aktiv\":%d,"
"\"warmup\":%d"
"}",
temp_aussen,
temp_vorlauf,
vtsoll,
temp_koben,
temp_kunten,
temp_wohnung,
temp_abgas,
stellung,
mischeron,
neigung,
rtsoll,
misch_begin,
nachtabs,
tag_anfang,
tag_ende,
time_adjust,
abw,
dynamicCheckInterval / 1000,
activePulseTime / 1000,
(currentPulse != PULSE_NONE) ? 1 : 0,
mischeronWarmup ? 1 : 0
);
Serial.println(buffer);
}
// ============================================================
// Serielle Befehle vom Raspberry Pi verarbeiten
// Format: SET <key> <value>\n
// ============================================================
void handleSerialInput() {
if (!Serial.available()) return;
String line = Serial.readStringUntil('\n');
line.trim();
if (!line.startsWith(F("SET "))) return;
// "SET key value" parsen
int sp1 = line.indexOf(' '); // nach "SET"
int sp2 = line.indexOf(' ', sp1 + 1);
if (sp1 < 0 || sp2 < 0) return;
String key = line.substring(sp1 + 1, sp2);
String val = line.substring(sp2 + 1);
val.trim();
bool ok = true;
if (key == F("neigung")) neigung = val.toFloat();
else if (key == F("rtsoll")) rtsoll = val.toInt();
else if (key == F("nachtabs")) nachtabs = val.toInt();
else if (key == F("misch_begin")) misch_begin = val.toInt();
else if (key == F("tag_anfang")) tag_anfang = val.toInt();
else if (key == F("tag_ende")) tag_ende = val.toInt();
else ok = false;
// Bestätigung zurücksenden
Serial.print(F("{\"ok\":"));
Serial.print(ok ? F("true") : F("false"));
Serial.print(F(",\"key\":\""));
Serial.print(key);
Serial.print(F("\",\"value\":\""));
Serial.print(val);
Serial.println(F("\"}"));
}