init
This commit is contained in:
1 parent
82b19fe5dc
commit
6b79415427
11 files changed
+1787
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,250 @@
|
|||||||
|
================================================================================
|
||||||
|
HEIZUNGSSTEUERUNG – VERKABELUNG & AUFBAU TODO
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
HARDWARE-ÜBERSICHT
|
||||||
|
------------------
|
||||||
|
- Arduino Mega 2560
|
||||||
|
- Raspberry Pi (beliebiges Modell mit USB)
|
||||||
|
- 8-fach Relaismodul (ActiveLow, 5 V Steuerung)
|
||||||
|
- 5x DS18B20 Temperatursensor (OneWire, wasserfest)
|
||||||
|
- 1x MAX6675 Thermoelement-Modul (Abgastemperatur, Typ K)
|
||||||
|
- 1x RTC DS3231 (Echtzeituhr, I2C)
|
||||||
|
- 1x LCD Display 16x2 (HD44780, parallel)
|
||||||
|
- 1x OLED Display 128x64 (SSD1306, I2C) ← aktuell im Code vorhanden aber noch nicht genutzt
|
||||||
|
- 1x 4,7 kΩ Widerstand (Pullup für OneWire-Bus)
|
||||||
|
- Stromversorgung 5 V / mind. 2 A für Arduino + Relais
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
1. STROMVERSORGUNG
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
[ ] Arduino Mega über USB (vom Raspi) ODER extern 7-12V an Barrel-Jack
|
||||||
|
WICHTIG: Wenn Relaismodul separat versorgt wird → GND verbinden!
|
||||||
|
|
||||||
|
[ ] Relaismodul:
|
||||||
|
- VCC → 5 V Arduino (oder externe 5-V-Quelle bei großem Modul)
|
||||||
|
- GND → GND Arduino
|
||||||
|
- Eingangsseite (IN1-IN8) → Arduino-Pins direkt (5V-kompatibel)
|
||||||
|
|
||||||
|
[ ] Raspberry Pi separat mit offiziellen 5 V Netzteil versorgen
|
||||||
|
(NICHT über Arduino USB-Port – zu schwach)
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
2. RELAIS-MODUL → ARDUINO MEGA
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
Relais-Modul Arduino Mega Funktion
|
||||||
|
------------------------------------------------------------
|
||||||
|
IN1 → Pin 34 Mischer MINUS (kälter drehen)
|
||||||
|
IN2 → Pin 32 Mischer PLUS (wärmer drehen)
|
||||||
|
IN3 → Pin 30 Heizkreispumpe (Mischer-Pumpe)
|
||||||
|
IN4 → Pin 28 (reserviert / ggf. defekt – prüfen!)
|
||||||
|
IN5 → Pin 26 Kesselpumpe / Pufferbefüllung
|
||||||
|
IN6 → Pin 24 (frei)
|
||||||
|
IN7 → Pin 22 (frei)
|
||||||
|
IN8 → Pin 36 (frei)
|
||||||
|
VCC → 5V
|
||||||
|
GND → GND
|
||||||
|
|
||||||
|
ACHTUNG: Relais sind ActiveLow → HIGH = AUS, LOW = EIN
|
||||||
|
Arduino setzt beim Start alle Relais auf HIGH (sicher AUS)
|
||||||
|
|
||||||
|
[ ] Mischer MINUS (IN1/Pin 34) an Mischer-Motor Klemme "ZU" / "–"
|
||||||
|
[ ] Mischer PLUS (IN2/Pin 32) an Mischer-Motor Klemme "AUF" / "+"
|
||||||
|
[ ] Heizkreispumpe (IN3/Pin 30) an Pumpe (230 V Schaltung über Relais!)
|
||||||
|
[ ] Kesselpumpe (IN5/Pin 26) an Pumpenstromkreis
|
||||||
|
|
||||||
|
!! 230-V-SICHERHEIT !!
|
||||||
|
Netzspannung NUR über die Lastseite des Relais führen (COM + NO oder NC).
|
||||||
|
Steuerseite (IN-Pins, VCC, GND) IMMER galvanisch getrennt halten.
|
||||||
|
Sicherung und FI-Schutzschalter vorschalten!
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
3. DS18B20 TEMPERATURSENSOREN (OneWire-Bus)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
Alle 5 Sensoren hängen am selben Bus (Pin 50, Arduino Mega).
|
||||||
|
|
||||||
|
Verdrahtung je Sensor (3-Draht, gelbes Kabel = Daten):
|
||||||
|
Rotes Kabel → 5 V
|
||||||
|
Schwarzes Kabel → GND
|
||||||
|
Gelbes/weißes Kabel → OneWire-Bus (Pin 50)
|
||||||
|
|
||||||
|
[ ] 4,7 kΩ Pullup-Widerstand zwischen Pin 50 und 5 V einlöten
|
||||||
|
(einmal für den ganzen Bus, nicht pro Sensor!)
|
||||||
|
|
||||||
|
Sensor-Zuordnung (Adressen bereits im Code hinterlegt – bitte prüfen!):
|
||||||
|
[ ] wohnung – Raumtemperatur Wohnung
|
||||||
|
[ ] Kunten – Pufferspeicher UNTEN (Kessel unten)
|
||||||
|
[ ] Koben – Pufferspeicher OBEN (Kessel oben)
|
||||||
|
[ ] Vorlauf – Vorlaufrohr Heizkreis (außen am Rohr angebracht!)
|
||||||
|
[ ] Aussen – Außentemperatur (wetterfest montieren, beschatten!)
|
||||||
|
|
||||||
|
HINWEIS Vorlauf-Sensor:
|
||||||
|
Der Sensor sitzt außen am Vorlaufrohr → er reagiert mit 2-5 Minuten
|
||||||
|
Verzögerung auf Wassertemperaturänderungen. Das ist im Sketch durch
|
||||||
|
die Zonalregelung und Anlaufphase berücksichtigt.
|
||||||
|
Besser wäre ein Tauchhülsen-Sensor im Rohr – falls nachrüsten möglich.
|
||||||
|
|
||||||
|
Sensor-Adressen auslesen (falls Sensoren getauscht wurden):
|
||||||
|
→ OneWire-Scanner Sketch hochladen, Adressen aus Serial Monitor kopieren
|
||||||
|
→ Neue Adressen in heizung.ino eintragen (DeviceAddress Zeilen)
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
4. MAX6675 – ABGASTEMPERATUR (Thermoelement Typ K)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
MAX6675 Modul Arduino Mega
|
||||||
|
------------------------------------
|
||||||
|
VCC → 5 V
|
||||||
|
GND → GND
|
||||||
|
SO (MISO) → Pin 8
|
||||||
|
CS → Pin 9
|
||||||
|
SCK → Pin 10
|
||||||
|
|
||||||
|
[ ] Thermoelement Typ K an MAX6675-Klemmen anschließen (Polarität beachten!)
|
||||||
|
[ ] Thermoelement im Abgaskanal / Rauchrohr befestigen (max. ~1200°C Typ K)
|
||||||
|
[ ] Kabel hitzebeständig verlegen (mind. 5 cm Abstand zu heißen Flächen)
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
5. RTC DS3231 – ECHTZEITUHR (I2C)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
DS3231 Modul Arduino Mega
|
||||||
|
------------------------------------
|
||||||
|
VCC → 3,3 V oder 5 V (DS3231 ist beides kompatibel)
|
||||||
|
GND → GND
|
||||||
|
SDA → Pin 20 (SDA, Mega)
|
||||||
|
SCL → Pin 21 (SCL, Mega)
|
||||||
|
|
||||||
|
[ ] Knopfzelle (CR2032) einlegen damit Uhrzeit bei Stromausfall erhalten bleibt
|
||||||
|
[ ] Uhrzeit einmalig stellen: Zeile in setup() einkommentieren:
|
||||||
|
rtc.adjust(DateTime(2026, 3, 18, 12, 0, 0));
|
||||||
|
→ nach Upload wieder auskommentieren und neu flashen!
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
6. LCD 16x2 (HD44780, parallel)
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
LCD Pin Arduino Mega Hinweis
|
||||||
|
--------------------------------------------
|
||||||
|
VSS (1) → GND
|
||||||
|
VDD (2) → 5 V
|
||||||
|
V0 (3) → Schleifer 10kΩ Poti (Kontrast)
|
||||||
|
RS (4) → Pin 12
|
||||||
|
RW (5) → GND (immer schreiben)
|
||||||
|
EN (6) → Pin 11
|
||||||
|
D4 (11) → Pin 5
|
||||||
|
D5 (12) → Pin 4
|
||||||
|
D6 (13) → Pin 3
|
||||||
|
D7 (14) → Pin 2
|
||||||
|
A (15) → 5 V über 220 Ω (Hintergrundbeleuchtung +)
|
||||||
|
K (16) → GND (Hintergrundbeleuchtung –)
|
||||||
|
|
||||||
|
[ ] 10 kΩ Poti für Kontrast einbauen (V0)
|
||||||
|
[ ] 220 Ω Widerstand für Backlight
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
7. OLED 128x64 SSD1306 (I2C) – IM CODE VORBEREITET, NOCH NICHT GENUTZT
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
OLED Pin Arduino Mega
|
||||||
|
------------------------------------
|
||||||
|
VCC → 3,3 V
|
||||||
|
GND → GND
|
||||||
|
SDA → Pin 20 (gemeinsam mit DS3231)
|
||||||
|
SCL → Pin 21 (gemeinsam mit DS3231)
|
||||||
|
|
||||||
|
[ ] I2C-Adresse prüfen (Standard: 0x3C oder 0x3D)
|
||||||
|
Im Code: Adafruit_SSD1306 display(...) – ggf. Adresse im Konstruktor angeben
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
8. RASPBERRY PI ↔ ARDUINO VERBINDUNG
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
[ ] Arduino Mega per USB-Kabel an Raspberry Pi anschließen
|
||||||
|
(USB-B auf Arduino-Seite, USB-A auf Raspi-Seite)
|
||||||
|
|
||||||
|
[ ] Serial-Port auf Raspi prüfen:
|
||||||
|
ls /dev/ttyUSB* /dev/ttyACM*
|
||||||
|
→ typisch /dev/ttyUSB0 oder /dev/ttyACM0
|
||||||
|
|
||||||
|
[ ] Falls nötig: Benutzer zur dialout-Gruppe hinzufügen
|
||||||
|
sudo usermod -a -G dialout pi
|
||||||
|
→ danach einmal neu anmelden!
|
||||||
|
|
||||||
|
[ ] Python-Pakete installieren:
|
||||||
|
cd /pfad/zum/projekt/raspi
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
[ ] Serial-Port in server.py anpassen (Zeile SERIAL_PORT):
|
||||||
|
SERIAL_PORT = "/dev/ttyACM0" ← falls ttyUSB0 nicht gefunden wird
|
||||||
|
|
||||||
|
[ ] Webserver starten:
|
||||||
|
python3 server.py
|
||||||
|
→ Weboberfläche erreichbar unter http://<raspi-IP>:5000
|
||||||
|
|
||||||
|
[ ] Optional: Autostart beim Booten einrichten
|
||||||
|
sudo nano /etc/systemd/system/heizung.service
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=Heizungssteuerung Webserver
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/python3 /home/pi/heizung/raspi/server.py
|
||||||
|
WorkingDirectory=/home/pi/heizung/raspi
|
||||||
|
Restart=always
|
||||||
|
User=pi
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
|
sudo systemctl enable heizung
|
||||||
|
sudo systemctl start heizung
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
9. INBETRIEBNAHME – CHECKLISTE
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
[ ] Vor erstem Start: Sketch hochladen, Serial Monitor öffnen (9600 Baud)
|
||||||
|
→ JSON-Ausgabe sichtbar? Dann Arduino-Seite OK.
|
||||||
|
|
||||||
|
[ ] Relais-Test: Jedes Relais kurz manuell schalten (LOW), prüfen ob
|
||||||
|
Summer/LED auf Modul reagiert, BEVOR 230V angeschlossen wird!
|
||||||
|
|
||||||
|
[ ] Mischer-Test:
|
||||||
|
- Mischer manuell auf Mitte stellen
|
||||||
|
- Relais1 (Pin 34) kurz LOW → dreht in Kälter-Richtung?
|
||||||
|
- Relais2 (Pin 32) kurz LOW → dreht in Wärmer-Richtung?
|
||||||
|
- Bei verkehrter Drehrichtung: Motoranschlüsse tauschen ODER
|
||||||
|
relais1/relais2 im Sketch tauschen
|
||||||
|
|
||||||
|
[ ] Sensor-Test: Serial Monitor → alle 5 DS18B20 zeigen plausible Werte?
|
||||||
|
Typische Fehler: -127°C (nicht angeschlossen), 85°C (Initialisierungswert)
|
||||||
|
|
||||||
|
[ ] time_adjust kalibrieren:
|
||||||
|
Mischer von ganz-kalt bis ganz-warm messen → Sekunden in time_adjust eintragen
|
||||||
|
(Standard: 140 Sekunden – je nach Mischer-Motor variiert das!)
|
||||||
|
|
||||||
|
[ ] Webserver starten, Webseite öffnen → alle Werte sichtbar?
|
||||||
|
|
||||||
|
[ ] Parameter über Webseite anpassen und prüfen ob Arduino bestätigt
|
||||||
|
(JSON-Antwort: {"ok":true,...})
|
||||||
|
|
||||||
|
================================================================================
|
||||||
|
BEKANNTE EINSCHRÄNKUNGEN / MÖGLICHE VERBESSERUNGEN
|
||||||
|
================================================================================
|
||||||
|
|
||||||
|
- Vorlauf-Sensor außen am Rohr: Tauchhülsen-Sensor direkt im Wasser wäre
|
||||||
|
deutlich reaktionsschneller und genauer
|
||||||
|
|
||||||
|
- Keine Datenpersistenz auf Arduino: Einstellungen (neigung, rtsoll usw.)
|
||||||
|
gehen bei Neustart verloren → Raspi muss nach Arduino-Neustart Werte
|
||||||
|
erneut senden (oder im Sketch als Default-Werte hinterlegen)
|
||||||
|
|
||||||
|
- Relais4 (Pin 28): laut ursprünglichem Code "wahrscheinlich defekt" –
|
||||||
|
vor Verwendung prüfen!
|
||||||
|
|
||||||
|
================================================================================
|
||||||
+588
@@ -0,0 +1,588 @@
|
|||||||
|
// 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("\"}"));
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
# Serial-Port zum Arduino
|
||||||
|
SERIAL_PORT=/dev/ttyUSB0
|
||||||
|
|
||||||
|
# Externer MySQL-Server
|
||||||
|
DB_HOST=192.168.1.100
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USER=heizung
|
||||||
|
DB_PASSWORD=heizung_pw
|
||||||
|
DB_NAME=heizung
|
||||||
|
|
||||||
|
# Wie oft Messwerte in die DB geschrieben werden (Sekunden)
|
||||||
|
DB_WRITE_INTERVAL=60
|
||||||
|
|
||||||
|
# Flask
|
||||||
|
FLASK_HOST=0.0.0.0
|
||||||
|
FLASK_PORT=5000
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# System-Abhängigkeiten für pyserial auf ARM
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
gcc \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
CMD ["python3", "server.py"]
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
services:
|
||||||
|
heizung-web:
|
||||||
|
build: .
|
||||||
|
container_name: heizung-web
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
|
||||||
|
# Serial-Port zum Arduino durchreichen
|
||||||
|
# Falls /dev/ttyACM0 genutzt wird: beide Zeilen anpassen
|
||||||
|
devices:
|
||||||
|
- /dev/ttyUSB0:/dev/ttyUSB0
|
||||||
|
|
||||||
|
# Privileged-Modus damit der Container auf das Serial-Device zugreifen kann
|
||||||
|
# Alternativ: group_add mit der dialout-GID (auf Raspi OS meist 20)
|
||||||
|
privileged: true
|
||||||
|
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
# Logs persistent speichern (optional)
|
||||||
|
- heizung-logs:/app/logs
|
||||||
|
|
||||||
|
# Neustart erst nach Netzwerk (wichtig für DB-Verbindung)
|
||||||
|
# Die App funktioniert auch ohne DB, daher kein hard depends_on
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
heizung-logs:
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
flask>=3.0
|
||||||
|
pyserial>=3.5
|
||||||
|
# PyMySQL ist optional – nur benötigt wenn DB_HOST gesetzt ist
|
||||||
|
# PyMySQL>=1.1
|
||||||
+334
@@ -0,0 +1,334 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Heizungssteuerung – Raspberry Pi Webserver
|
||||||
|
Liest JSON-Daten vom Arduino über Serial, stellt Weboberfläche bereit
|
||||||
|
und schreibt Messwerte in eine externe MySQL-Datenbank.
|
||||||
|
|
||||||
|
Verwendung:
|
||||||
|
cp .env.example .env # und Werte anpassen
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
Ohne Docker:
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python3 server.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pymysql
|
||||||
|
import pymysql.cursors
|
||||||
|
_PYMYSQL_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
_PYMYSQL_AVAILABLE = False
|
||||||
|
|
||||||
|
import readSerial
|
||||||
|
from flask import Flask, jsonify, render_template, request
|
||||||
|
|
||||||
|
# ── Konfiguration aus Umgebungsvariablen ───────────────────────────────────────
|
||||||
|
SERIAL_PORT = "/dev/ttyACM0"
|
||||||
|
BAUD_RATE = 9600
|
||||||
|
HOST = "0.0.0.0"
|
||||||
|
PORT = "5000"
|
||||||
|
|
||||||
|
DB_HOST = "10.233.1.244"
|
||||||
|
DB_PORT = "3306"
|
||||||
|
DB_USER = "worker"
|
||||||
|
DB_PASSWORD = "bBHofurUJWKzPE8Z"
|
||||||
|
DB_NAME = "HEIZUNG"
|
||||||
|
DB_WRITE_INTERVAL = "60"
|
||||||
|
|
||||||
|
HISTORY_LEN = 120 # letzte 120 Datenpunkte im Live-Chart
|
||||||
|
|
||||||
|
ALLOWED_KEYS = {
|
||||||
|
"neigung": {"type": float, "min": 0.1, "max": 4.0},
|
||||||
|
"rtsoll": {"type": int, "min": 10, "max": 30},
|
||||||
|
"nachtabs": {"type": int, "min": 0, "max": 10},
|
||||||
|
"misch_begin": {"type": int, "min": -15, "max": 25},
|
||||||
|
"tag_anfang": {"type": int, "min": 0, "max": 12},
|
||||||
|
"tag_ende": {"type": int, "min": 12, "max": 23},
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Globaler Zustand ───────────────────────────────────────────────────────────
|
||||||
|
latest_data: dict = {}
|
||||||
|
history: deque = deque(maxlen=HISTORY_LEN)
|
||||||
|
data_lock = threading.Lock()
|
||||||
|
serial_lock = threading.Lock()
|
||||||
|
ser: readSerial.Serial | None = None
|
||||||
|
serial_connected = False
|
||||||
|
|
||||||
|
db_connected = False
|
||||||
|
db_error: str = "" # letzte Fehlermeldung, leer wenn OK
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s %(levelname)-8s %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
|
)
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# ── MySQL-Hilfsfunktionen ──────────────────────────────────────────────────────
|
||||||
|
def _db_connect():
|
||||||
|
"""Baut eine neue DB-Verbindung auf. Gibt None zurück bei Fehler."""
|
||||||
|
if not DB_HOST or not _PYMYSQL_AVAILABLE:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
conn = pymysql.connect(
|
||||||
|
host=DB_HOST,
|
||||||
|
port=DB_PORT,
|
||||||
|
user=DB_USER,
|
||||||
|
password=DB_PASSWORD,
|
||||||
|
database=DB_NAME,
|
||||||
|
connect_timeout=5,
|
||||||
|
autocommit=True,
|
||||||
|
charset="utf8mb4",
|
||||||
|
cursorclass=pymysql.cursors.DictCursor,
|
||||||
|
)
|
||||||
|
return conn
|
||||||
|
except pymysql.Error as e:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
def write_measurement(data: dict) -> None:
|
||||||
|
"""Schreibt einen Messdatensatz in die DB. Setzt db_connected/db_error."""
|
||||||
|
global db_connected, db_error
|
||||||
|
|
||||||
|
if not DB_HOST or not _PYMYSQL_AVAILABLE:
|
||||||
|
return # keine DB konfiguriert oder PyMySQL nicht installiert – still ignorieren
|
||||||
|
|
||||||
|
time_adjust = data.get("time_adjust", 1) or 1
|
||||||
|
stellung = data.get("stellung", 0)
|
||||||
|
stellung_proz = round(stellung / time_adjust * 100, 1) if time_adjust else 0
|
||||||
|
|
||||||
|
sql = """
|
||||||
|
INSERT INTO messungen
|
||||||
|
(ts, aussentemp, vorlauf_ist, vorlauf_soll,
|
||||||
|
kessel_oben, kessel_unten, wohnung, abgas,
|
||||||
|
stellung, stellung_proz, mischeron, abweichung,
|
||||||
|
neigung, rtsoll, misch_begin, nachtabs)
|
||||||
|
VALUES
|
||||||
|
(NOW(), %s, %s, %s,
|
||||||
|
%s, %s, %s, %s,
|
||||||
|
%s, %s, %s, %s,
|
||||||
|
%s, %s, %s, %s)
|
||||||
|
"""
|
||||||
|
values = (
|
||||||
|
data.get("aussentemp"), data.get("vorlauf_ist"), data.get("vorlauf_soll"),
|
||||||
|
data.get("kessel_oben"), data.get("kessel_unten"), data.get("wohnung"),
|
||||||
|
data.get("abgas"),
|
||||||
|
stellung, stellung_proz, data.get("mischeron"), data.get("abweichung"),
|
||||||
|
data.get("neigung"), data.get("rtsoll"),
|
||||||
|
data.get("misch_begin"), data.get("nachtabs"),
|
||||||
|
)
|
||||||
|
|
||||||
|
conn = None
|
||||||
|
try:
|
||||||
|
conn = _db_connect()
|
||||||
|
if conn is None:
|
||||||
|
return
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(sql, values)
|
||||||
|
if not db_connected:
|
||||||
|
log.info("Datenbankverbindung hergestellt (%s:%d/%s)", DB_HOST, DB_PORT, DB_NAME)
|
||||||
|
db_connected = True
|
||||||
|
db_error = ""
|
||||||
|
except Exception as e:
|
||||||
|
db_connected = False
|
||||||
|
db_error = str(e)
|
||||||
|
log.warning("DB-Schreibfehler: %s", e)
|
||||||
|
finally:
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def log_parameter_change(key: str, old_value, new_value) -> None:
|
||||||
|
"""Schreibt eine Parameteränderung in die DB."""
|
||||||
|
if not DB_HOST:
|
||||||
|
return
|
||||||
|
sql = """
|
||||||
|
INSERT INTO parameter_aenderungen (ts, parameter, alter_wert, neuer_wert)
|
||||||
|
VALUES (NOW(), %s, %s, %s)
|
||||||
|
"""
|
||||||
|
conn = None
|
||||||
|
try:
|
||||||
|
conn = _db_connect()
|
||||||
|
if conn is None:
|
||||||
|
return
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(sql, (key, str(old_value) if old_value is not None else None, str(new_value)))
|
||||||
|
except Exception as e:
|
||||||
|
log.warning("DB-Fehler beim Protokollieren der Änderung: %s", e)
|
||||||
|
finally:
|
||||||
|
if conn:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ── DB-Writer-Thread ───────────────────────────────────────────────────────────
|
||||||
|
def db_writer():
|
||||||
|
"""Schreibt alle DB_WRITE_INTERVAL Sekunden den aktuellen Zustand in die DB."""
|
||||||
|
# Kurz warten damit der Serial-Reader erst Daten sammeln kann
|
||||||
|
time.sleep(10)
|
||||||
|
while True:
|
||||||
|
time.sleep(DB_WRITE_INTERVAL)
|
||||||
|
with data_lock:
|
||||||
|
data = dict(latest_data)
|
||||||
|
if not data:
|
||||||
|
log.debug("Noch keine Daten vom Arduino – DB-Schreiben übersprungen")
|
||||||
|
continue
|
||||||
|
write_measurement(data)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Serial-Lese-Thread ─────────────────────────────────────────────────────────
|
||||||
|
def serial_reader():
|
||||||
|
global ser, serial_connected
|
||||||
|
while True:
|
||||||
|
if ser is None or not ser.is_open:
|
||||||
|
try:
|
||||||
|
ser = readSerial.Serial(SERIAL_PORT, BAUD_RATE, timeout=2)
|
||||||
|
serial_connected = True
|
||||||
|
log.info("Serial verbunden: %s", SERIAL_PORT)
|
||||||
|
except readSerial.SerialException as e:
|
||||||
|
serial_connected = False
|
||||||
|
log.warning("Serial nicht verfügbar: %s – retry in 5s", e)
|
||||||
|
time.sleep(5)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
with serial_lock:
|
||||||
|
raw = ser.readline().decode("utf-8", errors="replace").strip()
|
||||||
|
except readSerial.SerialException as e:
|
||||||
|
log.warning("Serial-Lesefehler: %s", e)
|
||||||
|
serial_connected = False
|
||||||
|
ser = None
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not raw.startswith("{"):
|
||||||
|
if raw:
|
||||||
|
log.debug("Arduino: %s", raw)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(raw)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
log.debug("JSON-Fehler: %s", raw)
|
||||||
|
continue
|
||||||
|
|
||||||
|
data["ts"] = time.time()
|
||||||
|
|
||||||
|
with data_lock:
|
||||||
|
latest_data.update(data)
|
||||||
|
history.append({
|
||||||
|
"ts": data["ts"],
|
||||||
|
"vorlauf_ist": data.get("vorlauf_ist"),
|
||||||
|
"vorlauf_soll": data.get("vorlauf_soll"),
|
||||||
|
"aussentemp": data.get("aussentemp"),
|
||||||
|
"wohnung": data.get("wohnung"),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def send_command(key: str, value) -> bool:
|
||||||
|
"""Sendet einen SET-Befehl an den Arduino."""
|
||||||
|
cmd = f"SET {key} {value}\n"
|
||||||
|
# Lock vor der ser-Prüfung holen: verhindert TOCTOU-Race zwischen
|
||||||
|
# dieser Prüfung und dem serial_reader-Thread der ser=None setzen kann.
|
||||||
|
try:
|
||||||
|
with serial_lock:
|
||||||
|
if ser is None or not ser.is_open:
|
||||||
|
return False
|
||||||
|
ser.write(cmd.encode())
|
||||||
|
return True
|
||||||
|
except (readSerial.SerialException, AttributeError) as e:
|
||||||
|
log.error("Sendefehler: %s", e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# ── Flask-Routen ───────────────────────────────────────────────────────────────
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
with data_lock:
|
||||||
|
data = dict(latest_data)
|
||||||
|
return render_template("index.html", data=data, connected=serial_connected)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/data")
|
||||||
|
def api_data():
|
||||||
|
with data_lock:
|
||||||
|
data = dict(latest_data)
|
||||||
|
data["serial_connected"] = serial_connected
|
||||||
|
ts = data.get("ts", 0)
|
||||||
|
data["data_stale"] = (time.time() - ts) > 30 if ts else True
|
||||||
|
data["db_connected"] = db_connected
|
||||||
|
data["db_error"] = db_error
|
||||||
|
data["db_configured"] = bool(DB_HOST)
|
||||||
|
return jsonify(data)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/history")
|
||||||
|
def api_history():
|
||||||
|
with data_lock:
|
||||||
|
hist = list(history)
|
||||||
|
return jsonify(hist)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/set", methods=["POST"])
|
||||||
|
def api_set():
|
||||||
|
body = request.get_json(silent=True) or {}
|
||||||
|
key = body.get("key", "").strip()
|
||||||
|
value = body.get("value")
|
||||||
|
|
||||||
|
if key not in ALLOWED_KEYS:
|
||||||
|
return jsonify({"ok": False, "error": f"Unbekannter Schlüssel: {key}"}), 400
|
||||||
|
|
||||||
|
spec = ALLOWED_KEYS[key]
|
||||||
|
try:
|
||||||
|
value = spec["type"](value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return jsonify({"ok": False, "error": "Ungültiger Wert"}), 400
|
||||||
|
|
||||||
|
if not (spec["min"] <= value <= spec["max"]):
|
||||||
|
return jsonify({
|
||||||
|
"ok": False,
|
||||||
|
"error": f"Wert außerhalb des Bereichs [{spec['min']}, {spec['max']}]"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
ok = send_command(key, value)
|
||||||
|
if not ok:
|
||||||
|
return jsonify({"ok": False, "error": "Arduino nicht erreichbar"}), 503
|
||||||
|
|
||||||
|
# Alten Wert für DB-Protokoll merken
|
||||||
|
with data_lock:
|
||||||
|
old_value = latest_data.get(key)
|
||||||
|
latest_data[key] = value
|
||||||
|
|
||||||
|
# Änderung in DB protokollieren (non-blocking, im Hintergrund)
|
||||||
|
threading.Thread(
|
||||||
|
target=log_parameter_change,
|
||||||
|
args=(key, old_value, value),
|
||||||
|
daemon=True,
|
||||||
|
).start()
|
||||||
|
|
||||||
|
return jsonify({"ok": True, "key": key, "value": value})
|
||||||
|
|
||||||
|
|
||||||
|
# ── Start ──────────────────────────────────────────────────────────────────────
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if not _PYMYSQL_AVAILABLE:
|
||||||
|
log.warning("PyMySQL nicht installiert – Datenbankanbindung deaktiviert")
|
||||||
|
elif not DB_HOST:
|
||||||
|
log.warning("DB_HOST nicht gesetzt – Datenbankanbindung deaktiviert")
|
||||||
|
|
||||||
|
threading.Thread(target=serial_reader, daemon=True, name="serial-reader").start()
|
||||||
|
threading.Thread(target=db_writer, daemon=True, name="db-writer").start()
|
||||||
|
|
||||||
|
log.info("Webserver gestartet auf http://%s:%d", HOST, PORT)
|
||||||
|
log.info("Datenbank: %s@%s:%d/%s", DB_USER, DB_HOST or "(nicht konfiguriert)", DB_PORT, DB_NAME)
|
||||||
|
log.info("DB-Schreibintervall: %d Sekunden", DB_WRITE_INTERVAL)
|
||||||
|
app.run(host=HOST, port=PORT, debug=False)
|
||||||
@@ -0,0 +1,412 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Heizungssteuerung</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
||||||
|
<style>
|
||||||
|
body { font-family: system-ui, sans-serif; }
|
||||||
|
.card { @apply bg-white rounded-2xl shadow p-4; }
|
||||||
|
.val-big { @apply text-3xl font-bold; }
|
||||||
|
.label { @apply text-xs text-gray-500 uppercase tracking-wide; }
|
||||||
|
.badge-on { @apply inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-green-100 text-green-800; }
|
||||||
|
.badge-off { @apply inline-block px-2 py-0.5 rounded-full text-xs font-semibold bg-gray-100 text-gray-500; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-100 min-h-screen p-4">
|
||||||
|
|
||||||
|
<!-- DB-Fehler Banner (nur sichtbar wenn DB konfiguriert aber nicht erreichbar) -->
|
||||||
|
<div id="db-banner" class="hidden max-w-5xl mx-auto mb-4 bg-red-50 border border-red-200 rounded-xl px-4 py-3 flex items-start gap-3">
|
||||||
|
<svg class="w-5 h-5 text-red-500 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3m0 3h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p class="font-semibold text-red-700 text-sm">Datenbankverbindung fehlgeschlagen</p>
|
||||||
|
<p class="text-red-600 text-xs mt-0.5" id="db-error-msg">–</p>
|
||||||
|
<p class="text-red-500 text-xs mt-1">Messwerte werden nicht gespeichert. Alle anderen Funktionen laufen normal weiter.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="max-w-5xl mx-auto">
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-800">🔥 Heizungssteuerung</h1>
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-500">
|
||||||
|
<span id="serial-dot" class="w-2 h-2 rounded-full bg-gray-400"></span>
|
||||||
|
<span id="serial-label">Verbinde…</span>
|
||||||
|
<span class="ml-4 text-gray-400" id="last-update"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sensor-Karten -->
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4 mb-6">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">Aussentemperatur</div>
|
||||||
|
<div class="val-big text-blue-600" id="aussentemp">–</div>
|
||||||
|
<div class="text-sm text-gray-400">°C</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">Wohnungstemperatur</div>
|
||||||
|
<div class="val-big text-orange-500" id="wohnung">–</div>
|
||||||
|
<div class="text-sm text-gray-400">°C</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card col-span-2">
|
||||||
|
<div class="label">Vorlauf</div>
|
||||||
|
<div class="flex items-end gap-3">
|
||||||
|
<div>
|
||||||
|
<div class="text-xs text-gray-400">Ist</div>
|
||||||
|
<div class="val-big text-red-500" id="vorlauf_ist">–</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl text-gray-300 pb-1">→</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-xs text-gray-400">Soll</div>
|
||||||
|
<div class="val-big text-gray-700" id="vorlauf_soll">–</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-400 pb-1">°C</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">Kessel Oben</div>
|
||||||
|
<div class="val-big text-amber-600" id="kessel_oben">–</div>
|
||||||
|
<div class="text-sm text-gray-400">°C</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">Kessel Unten</div>
|
||||||
|
<div class="val-big text-amber-500" id="kessel_unten">–</div>
|
||||||
|
<div class="text-sm text-gray-400">°C</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">Abgastemperatur</div>
|
||||||
|
<div class="val-big text-rose-700" id="abgas">–</div>
|
||||||
|
<div class="text-sm text-gray-400">°C</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">Mischer</div>
|
||||||
|
<div class="val-big text-indigo-600" id="stellung">–</div>
|
||||||
|
<div class="text-sm text-gray-400">% geöffnet</div>
|
||||||
|
<div class="mt-1" id="mischeron-badge"><span class="badge-off">aus</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Regelungs-Diagnose -->
|
||||||
|
<div class="card mb-6">
|
||||||
|
<h2 class="font-semibold text-gray-700 mb-3">Regelungsdiagnose</h2>
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<div class="label">Abweichung</div>
|
||||||
|
<div class="text-xl font-bold" id="abweichung">–</div>
|
||||||
|
<div class="text-xs text-gray-400">°C (Soll − Ist)</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="label">Regelzone / Puls</div>
|
||||||
|
<div class="text-xl font-bold" id="regelzone">–</div>
|
||||||
|
<div class="text-xs text-gray-400" id="puls-info">–</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="label">Check-Intervall</div>
|
||||||
|
<div class="text-xl font-bold" id="check-intervall">–</div>
|
||||||
|
<div class="text-xs text-gray-400">Sekunden</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="label">Status</div>
|
||||||
|
<div class="mt-1 flex flex-col gap-1">
|
||||||
|
<span id="puls-aktiv-badge" class="badge-off">kein Puls</span>
|
||||||
|
<span id="warmup-badge" class="badge-off">normal</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Verlaufs-Chart -->
|
||||||
|
<div class="card mb-6">
|
||||||
|
<div class="label mb-2">Temperaturverlauf</div>
|
||||||
|
<canvas id="tempChart" height="120"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Einstellungen -->
|
||||||
|
<div class="card mb-6">
|
||||||
|
<h2 class="font-semibold text-gray-700 mb-4">Einstellungen</h2>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6" id="settings-grid">
|
||||||
|
|
||||||
|
<div class="setting-block" data-key="neigung" data-type="float" data-min="0.1" data-max="4.0" data-step="0.1">
|
||||||
|
<label class="label">Neigung (Heizkurve)</label>
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<input type="range" class="flex-1 accent-red-500" id="input-neigung">
|
||||||
|
<span class="w-12 text-right font-mono" id="display-neigung">–</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-400">Bereich 0.1 – 4.0</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-block" data-key="rtsoll" data-type="int" data-min="10" data-max="30" data-step="1">
|
||||||
|
<label class="label">Raumsoll-Temperatur (°C)</label>
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<input type="range" class="flex-1 accent-orange-400" id="input-rtsoll">
|
||||||
|
<span class="w-12 text-right font-mono" id="display-rtsoll">–</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-400">Bereich 10 – 30 °C</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-block" data-key="nachtabs" data-type="int" data-min="0" data-max="10" data-step="1">
|
||||||
|
<label class="label">Nachtabsenkung (°C)</label>
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<input type="range" class="flex-1 accent-blue-400" id="input-nachtabs">
|
||||||
|
<span class="w-12 text-right font-mono" id="display-nachtabs">–</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-400">Bereich 0 – 10 °C</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-block" data-key="misch_begin" data-type="int" data-min="-15" data-max="25" data-step="1">
|
||||||
|
<label class="label">Heizung aus ab Außentemp (°C)</label>
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<input type="range" class="flex-1 accent-green-500" id="input-misch_begin">
|
||||||
|
<span class="w-12 text-right font-mono" id="display-misch_begin">–</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-400">Heizung läuft wenn Außentemp < diesem Wert</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-block" data-key="tag_anfang" data-type="int" data-min="0" data-max="12" data-step="1">
|
||||||
|
<label class="label">Tag beginnt (Uhr)</label>
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<input type="range" class="flex-1 accent-yellow-400" id="input-tag_anfang">
|
||||||
|
<span class="w-12 text-right font-mono" id="display-tag_anfang">–</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-400">Bereich 0 – 12 Uhr</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-block" data-key="tag_ende" data-type="int" data-min="12" data-max="23" data-step="1">
|
||||||
|
<label class="label">Tag endet (Uhr)</label>
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
<input type="range" class="flex-1 accent-yellow-600" id="input-tag_ende">
|
||||||
|
<span class="w-12 text-right font-mono" id="display-tag_ende">–</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-400">Bereich 12 – 23 Uhr</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status-Meldung -->
|
||||||
|
<div id="save-status" class="mt-4 text-sm text-gray-500 hidden"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- /max-w -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// ── Chart ─────────────────────────────────────────────────────────────────────
|
||||||
|
const ctx = document.getElementById('tempChart').getContext('2d');
|
||||||
|
const chart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: [],
|
||||||
|
datasets: [
|
||||||
|
{ label: 'Vorlauf Ist', data: [], borderColor: '#ef4444', tension: 0.3, pointRadius: 0 },
|
||||||
|
{ label: 'Vorlauf Soll', data: [], borderColor: '#6b7280', tension: 0.3, pointRadius: 0, borderDash: [5,3] },
|
||||||
|
{ label: 'Aussen', data: [], borderColor: '#3b82f6', tension: 0.3, pointRadius: 0 },
|
||||||
|
{ label: 'Wohnung', data: [], borderColor: '#f97316', tension: 0.3, pointRadius: 0 },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
animation: false,
|
||||||
|
scales: {
|
||||||
|
x: { display: false },
|
||||||
|
y: { grid: { color: '#f3f4f6' } }
|
||||||
|
},
|
||||||
|
plugins: { legend: { position: 'bottom', labels: { boxWidth: 12 } } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateChart(hist) {
|
||||||
|
chart.data.labels = hist.map(h => new Date(h.ts * 1000).toLocaleTimeString('de'));
|
||||||
|
chart.data.datasets[0].data = hist.map(h => h.vorlauf_ist);
|
||||||
|
chart.data.datasets[1].data = hist.map(h => h.vorlauf_soll);
|
||||||
|
chart.data.datasets[2].data = hist.map(h => h.aussentemp);
|
||||||
|
chart.data.datasets[3].data = hist.map(h => h.wohnung);
|
||||||
|
chart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Einstellungen ─────────────────────────────────────────────────────────────
|
||||||
|
// Für jeden Slider: beim Loslassen Wert an Server senden
|
||||||
|
let sendTimer = null;
|
||||||
|
|
||||||
|
function setupSliders() {
|
||||||
|
document.querySelectorAll('.setting-block').forEach(block => {
|
||||||
|
const key = block.dataset.key;
|
||||||
|
const type = block.dataset.type;
|
||||||
|
const min = parseFloat(block.dataset.min);
|
||||||
|
const max = parseFloat(block.dataset.max);
|
||||||
|
const step = parseFloat(block.dataset.step);
|
||||||
|
const input = block.querySelector('input');
|
||||||
|
const disp = block.querySelector(`#display-${key}`);
|
||||||
|
|
||||||
|
input.min = min;
|
||||||
|
input.max = max;
|
||||||
|
input.step = step;
|
||||||
|
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
disp.textContent = formatVal(input.value, type);
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('change', () => {
|
||||||
|
const val = type === 'float' ? parseFloat(input.value) : parseInt(input.value);
|
||||||
|
sendSet(key, val);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatVal(v, type) {
|
||||||
|
return type === 'float' ? parseFloat(v).toFixed(1) : parseInt(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySlider(key, value) {
|
||||||
|
const input = document.getElementById(`input-${key}`);
|
||||||
|
const disp = document.getElementById(`display-${key}`);
|
||||||
|
if (!input) return;
|
||||||
|
const block = input.closest('.setting-block');
|
||||||
|
const type = block.dataset.type;
|
||||||
|
input.value = value;
|
||||||
|
disp.textContent = formatVal(value, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendSet(key, value) {
|
||||||
|
const status = document.getElementById('save-status');
|
||||||
|
status.textContent = `Sende ${key} = ${value}…`;
|
||||||
|
status.className = 'mt-4 text-sm text-blue-500';
|
||||||
|
status.classList.remove('hidden');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/set', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ key, value })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.ok) {
|
||||||
|
status.textContent = `✓ ${key} = ${value} gespeichert`;
|
||||||
|
status.className = 'mt-4 text-sm text-green-600';
|
||||||
|
} else {
|
||||||
|
status.textContent = `✗ Fehler: ${data.error}`;
|
||||||
|
status.className = 'mt-4 text-sm text-red-500';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
status.textContent = `✗ Netzwerkfehler`;
|
||||||
|
status.className = 'mt-4 text-sm text-red-500';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Daten polling ─────────────────────────────────────────────────────────────
|
||||||
|
function fmt(v, dec = 1) {
|
||||||
|
if (v === null || v === undefined) return '–';
|
||||||
|
return typeof v === 'number' ? v.toFixed(dec) : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshData() {
|
||||||
|
try {
|
||||||
|
const [dataRes, histRes] = await Promise.all([
|
||||||
|
fetch('/api/data'),
|
||||||
|
fetch('/api/history')
|
||||||
|
]);
|
||||||
|
const data = await dataRes.json();
|
||||||
|
const hist = await histRes.json();
|
||||||
|
|
||||||
|
// Verbindungsstatus
|
||||||
|
const dot = document.getElementById('serial-dot');
|
||||||
|
const label = document.getElementById('serial-label');
|
||||||
|
if (data.serial_connected && !data.data_stale) {
|
||||||
|
dot.className = 'w-2 h-2 rounded-full bg-green-500';
|
||||||
|
label.textContent = 'Arduino verbunden';
|
||||||
|
} else if (data.serial_connected && data.data_stale) {
|
||||||
|
dot.className = 'w-2 h-2 rounded-full bg-yellow-400';
|
||||||
|
label.textContent = 'Keine Daten (> 30 s)';
|
||||||
|
} else {
|
||||||
|
dot.className = 'w-2 h-2 rounded-full bg-red-500';
|
||||||
|
label.textContent = 'Arduino getrennt';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('last-update').textContent =
|
||||||
|
'Zuletzt: ' + new Date().toLocaleTimeString('de');
|
||||||
|
|
||||||
|
// DB-Status Banner
|
||||||
|
const dbBanner = document.getElementById('db-banner');
|
||||||
|
if (data.db_configured && !data.db_connected) {
|
||||||
|
dbBanner.classList.remove('hidden');
|
||||||
|
document.getElementById('db-error-msg').textContent = data.db_error || 'Unbekannter Fehler';
|
||||||
|
} else {
|
||||||
|
dbBanner.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sensor-Werte
|
||||||
|
document.getElementById('aussentemp').textContent = fmt(data.aussentemp);
|
||||||
|
document.getElementById('wohnung').textContent = fmt(data.wohnung);
|
||||||
|
document.getElementById('vorlauf_ist').textContent = fmt(data.vorlauf_ist);
|
||||||
|
document.getElementById('vorlauf_soll').textContent = fmt(data.vorlauf_soll);
|
||||||
|
document.getElementById('kessel_oben').textContent = fmt(data.kessel_oben);
|
||||||
|
document.getElementById('kessel_unten').textContent = fmt(data.kessel_unten);
|
||||||
|
document.getElementById('abgas').textContent = fmt(data.abgas, 0);
|
||||||
|
|
||||||
|
// Mischerstellung in Prozent
|
||||||
|
if (data.stellung !== undefined && data.time_adjust) {
|
||||||
|
const proz = Math.round(data.stellung / data.time_adjust * 100);
|
||||||
|
document.getElementById('stellung').textContent = proz;
|
||||||
|
}
|
||||||
|
const badge = document.getElementById('mischeron-badge');
|
||||||
|
badge.innerHTML = data.mischeron
|
||||||
|
? '<span class="badge-on">Mischer an</span>'
|
||||||
|
: '<span class="badge-off">Mischer aus</span>';
|
||||||
|
|
||||||
|
// Regelungsdiagnose
|
||||||
|
if (data.abweichung !== undefined) {
|
||||||
|
const abw = data.abweichung;
|
||||||
|
const abwEl = document.getElementById('abweichung');
|
||||||
|
abwEl.textContent = (abw >= 0 ? '+' : '') + abw.toFixed(1);
|
||||||
|
abwEl.className = 'text-xl font-bold ' + (Math.abs(abw) > 8 ? 'text-red-600' :
|
||||||
|
Math.abs(abw) > 4 ? 'text-orange-500' :
|
||||||
|
Math.abs(abw) > 1.5 ? 'text-yellow-600' : 'text-green-600');
|
||||||
|
}
|
||||||
|
// Regelzone bestimmen (clientseitig aus Abweichung ableiten)
|
||||||
|
if (data.abweichung !== undefined && data.check_intervall !== undefined) {
|
||||||
|
const abwAbs = Math.abs(data.abweichung);
|
||||||
|
const zone = abwAbs > 8 ? 'Sprint' : abwAbs > 4 ? 'Jogging' : abwAbs > 1.5 ? 'Normal' : 'Totband';
|
||||||
|
document.getElementById('regelzone').textContent = zone;
|
||||||
|
document.getElementById('puls-info').textContent =
|
||||||
|
(data.puls_zeit || '–') + ' s Puls';
|
||||||
|
document.getElementById('check-intervall').textContent = data.check_intervall || '–';
|
||||||
|
}
|
||||||
|
document.getElementById('puls-aktiv-badge').innerHTML =
|
||||||
|
data.puls_aktiv ? '<span class="badge-on">Puls aktiv</span>' :
|
||||||
|
'<span class="badge-off">kein Puls</span>';
|
||||||
|
document.getElementById('warmup-badge').innerHTML =
|
||||||
|
data.warmup ? '<span class="badge-on">Anlaufphase</span>' :
|
||||||
|
'<span class="badge-off">Normalbetrieb</span>';
|
||||||
|
|
||||||
|
// Slider nur beim ersten Laden setzen (danach steuert der User)
|
||||||
|
const keys = ['neigung','rtsoll','nachtabs','misch_begin','tag_anfang','tag_ende'];
|
||||||
|
keys.forEach(k => {
|
||||||
|
const input = document.getElementById(`input-${k}`);
|
||||||
|
if (input && input.dataset.loaded !== 'true' && data[k] !== undefined) {
|
||||||
|
applySlider(k, data[k]);
|
||||||
|
input.dataset.loaded = 'true';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateChart(hist);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Refresh-Fehler:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||||
|
setupSliders();
|
||||||
|
refreshData();
|
||||||
|
setInterval(refreshData, 5000); // alle 5 Sekunden aktualisieren
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Heizungssteuerung – MySQL Datenbankschema
|
||||||
|
-- Auf dem externen MySQL-Server ausführen:
|
||||||
|
-- mysql -u root -p < schema.sql
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE DATABASE IF NOT EXISTS heizung
|
||||||
|
CHARACTER SET utf8mb4
|
||||||
|
COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
USE heizung;
|
||||||
|
|
||||||
|
-- ── Messwerte ────────────────────────────────────────────────
|
||||||
|
-- Wird alle 60 Sekunden vom Raspberry Pi befüllt
|
||||||
|
CREATE TABLE IF NOT EXISTS messungen (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
ts DATETIME NOT NULL COMMENT 'Zeitstempel der Messung',
|
||||||
|
|
||||||
|
-- Temperaturen
|
||||||
|
aussentemp DECIMAL(5,1) COMMENT 'Aussentemperatur °C',
|
||||||
|
vorlauf_ist DECIMAL(5,1) COMMENT 'Vorlauftemperatur gemessen °C',
|
||||||
|
vorlauf_soll DECIMAL(5,1) COMMENT 'Vorlauftemperatur Soll °C',
|
||||||
|
kessel_oben DECIMAL(5,1) COMMENT 'Pufferspeicher oben °C',
|
||||||
|
kessel_unten DECIMAL(5,1) COMMENT 'Pufferspeicher unten °C',
|
||||||
|
wohnung DECIMAL(5,1) COMMENT 'Raumtemperatur Wohnung °C',
|
||||||
|
abgas DECIMAL(5,1) COMMENT 'Abgastemperatur °C',
|
||||||
|
|
||||||
|
-- Regelung
|
||||||
|
stellung SMALLINT COMMENT 'Mischerstellung in Sekunden',
|
||||||
|
stellung_proz DECIMAL(5,1) COMMENT 'Mischerstellung in Prozent',
|
||||||
|
mischeron TINYINT(1) COMMENT '1 = Mischer/Heizkreis aktiv',
|
||||||
|
abweichung DECIMAL(5,1) COMMENT 'Vorlauf Soll-Ist Abweichung °C',
|
||||||
|
|
||||||
|
-- Einstellungen (Snapshot zum Zeitpunkt der Messung)
|
||||||
|
neigung DECIMAL(4,2) COMMENT 'Heizkurven-Neigung',
|
||||||
|
rtsoll TINYINT COMMENT 'Raumsoll-Temperatur °C',
|
||||||
|
misch_begin TINYINT COMMENT 'Einschalt-Schwelle Aussentemperatur °C',
|
||||||
|
nachtabs TINYINT COMMENT 'Nachtabsenkung °C',
|
||||||
|
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
INDEX idx_ts (ts),
|
||||||
|
INDEX idx_ts_vorlauf (ts, vorlauf_ist, vorlauf_soll)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||||
|
COMMENT='Periodische Messwerte der Heizungssteuerung';
|
||||||
|
|
||||||
|
-- ── Parameteränderungen ──────────────────────────────────────
|
||||||
|
-- Wird bei jeder Änderung über die Weboberfläche befüllt
|
||||||
|
CREATE TABLE IF NOT EXISTS parameter_aenderungen (
|
||||||
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
|
ts DATETIME NOT NULL COMMENT 'Zeitstempel der Änderung',
|
||||||
|
parameter VARCHAR(50) NOT NULL COMMENT 'Name des Parameters',
|
||||||
|
alter_wert VARCHAR(50) COMMENT 'Alter Wert',
|
||||||
|
neuer_wert VARCHAR(50) NOT NULL COMMENT 'Neuer Wert',
|
||||||
|
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
INDEX idx_ts (ts),
|
||||||
|
INDEX idx_parameter (parameter)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||||
|
COMMENT='Protokoll aller Parameteränderungen über die Weboberfläche';
|
||||||
|
|
||||||
|
-- ── Datenbank-Benutzer anlegen ───────────────────────────────
|
||||||
|
-- Anpassen: 'heizung_pw' durch ein sicheres Passwort ersetzen
|
||||||
|
-- '192.168.%.%' auf die eigene Netzwerkadresse einschränken
|
||||||
|
CREATE USER IF NOT EXISTS 'heizung'@'192.168.%.%'
|
||||||
|
IDENTIFIED BY 'heizung_pw';
|
||||||
|
|
||||||
|
GRANT SELECT, INSERT ON heizung.messungen
|
||||||
|
TO 'heizung'@'192.168.%.%';
|
||||||
|
|
||||||
|
GRANT SELECT, INSERT ON heizung.parameter_aenderungen
|
||||||
|
TO 'heizung'@'192.168.%.%';
|
||||||
|
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
|
||||||
|
-- ── Nützliche Beispiel-Abfragen ──────────────────────────────
|
||||||
|
/*
|
||||||
|
-- Letzten 24 Stunden anzeigen (1-Minuten-Auflösung)
|
||||||
|
SELECT ts, aussentemp, vorlauf_ist, vorlauf_soll, stellung_proz
|
||||||
|
FROM messungen
|
||||||
|
WHERE ts >= NOW() - INTERVAL 24 HOUR
|
||||||
|
ORDER BY ts DESC;
|
||||||
|
|
||||||
|
-- Tagesstatistik
|
||||||
|
SELECT
|
||||||
|
DATE(ts) AS tag,
|
||||||
|
AVG(aussentemp) AS aussen_mittel,
|
||||||
|
MIN(aussentemp) AS aussen_min,
|
||||||
|
MAX(vorlauf_ist) AS vorlauf_max,
|
||||||
|
SUM(mischeron) / COUNT(*) * 100 AS heizung_aktiv_prozent
|
||||||
|
FROM messungen
|
||||||
|
GROUP BY DATE(ts)
|
||||||
|
ORDER BY tag DESC;
|
||||||
|
|
||||||
|
-- Letzte Parameteränderungen
|
||||||
|
SELECT ts, parameter, alter_wert, neuer_wert
|
||||||
|
FROM parameter_aenderungen
|
||||||
|
ORDER BY ts DESC
|
||||||
|
LIMIT 20;
|
||||||
|
*/
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e # Script bricht bei Fehlern sofort ab
|
||||||
|
|
||||||
|
PORT="/dev/ttyACM0"
|
||||||
|
FQBN="arduino:avr:mega"
|
||||||
|
|
||||||
|
echo "==> Prüfe angeschlossene Boards"
|
||||||
|
arduino-cli board list
|
||||||
|
|
||||||
|
echo "==> Installiere benötigte Libraries"
|
||||||
|
LIBS=(
|
||||||
|
"OneWire"
|
||||||
|
"DallasTemperature"
|
||||||
|
"RTClib"
|
||||||
|
"LiquidCrystal"
|
||||||
|
"max6675"
|
||||||
|
"Wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
for LIB in "${LIBS[@]}"; do
|
||||||
|
echo "Installiere $LIB..."
|
||||||
|
arduino-cli lib install "$LIB"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "==> Installierte Libraries:"
|
||||||
|
arduino-cli lib list
|
||||||
|
|
||||||
|
echo "==> Kompiliere Sketch"
|
||||||
|
arduino-cli compile --fqbn "$FQBN" . --warnings all
|
||||||
|
|
||||||
|
echo "==> Lade Sketch hoch auf $PORT"
|
||||||
|
arduino-cli upload -p "$PORT" --fqbn "$FQBN" .
|
||||||
|
|
||||||
|
echo "==> Fertig!"
|
||||||
Reference in new issue
Block a user