Zum Inhalt

heating_ds2484.tc

heating_ds2484.tc — Heizungssteuerung über DS2484-1-Wire (Beispiel)

Source on GitHub

// ============================================================================
// heating_ds2484.tc — Heizungssteuerung über DS2484-1-Wire (Beispiel)
// ============================================================================
// Vollständige, im Betrieb erprobte Heizungssteuerung (aus Rolfs Anlage),
// portiert von einem Tasmota-Scripter-.tas nach TinyC. Dient zugleich als
// Beispiel für eine komplette HAUPTSEITEN-Web-UI ganz ohne Untermenü:
//   * alle Schalter als Checkboxen, Solltemperatur als Zahlenfeld
//   * Vierwegehahn als Radio-Buttons (Zu=blau, Auf=rot, per CSS eingefärbt)
//   * Temperaturen trend-eingefärbt (steigend=orange, fallend=hellblau, gleich=weiss)
//   * Live-1-h-Verlaufschart des Heizkessels (WebChart) direkt auf der Hauptseite
// Web-Bausteine: webCheckbox / webNumber / webRadio / WebChart in WebCall()+WebPage().
//
// Selbstständig: DS2484 (I2C) 1-Wire + DS18x20-Temperaturen + DS2406-Schalter
// + die Heizautomatik (Pumpe / Vierwegehahn aus Vorlauf vs. Solltemperatur).
//
// Original-Scripter benutzte Tasmotas eingebautes ow() auf einem GPIO. Hier
// läuft der Bus über die DS2484-Bridge (wie onewire32.tc), die ow()-Aufrufe
// sind 1:1 auf die DS2484-Funktionen abgebildet:
//   ow(0)  Bus init+scan   -> ow_reinit()
//   ow(5/6/10)+get_owid    -> ow_scan() (Bitbang-ROM-Suche über die Bridge)
//   ow(8 id+par<<6)        -> set_act()/set6pin() (DS2406 PIO setzen)
//   ow(9 id[+0x80])        -> ow_read_temp() (DS18x20)
//
// WICHTIG: Die Geräte werden über ihre **ROM-Adresse** den Namen zugeordnet
// (Tabelle unten). Das sind Rolfs ECHTE Anlagen-ROMs. Auf einem Testaufbau mit
// Ersatz-Chips passt keine — dann bleiben die benannten Sensoren -1 / 0; der
// Bus-Scan und das Schalten funktionieren trotzdem.
//
// FAITHFUL: Das Pumpen-Verhalten ist bewusst wie im Original — die Automatik
// (Heizung an) besitzt die Pumpe; bei Heizung AUS wird pump=0 erzwungen. Siehe
// Notiz am Ende für die optionale Entkopplung.
// ============================================================================

// ---- DS2484 / 1-Wire ----
#define OW_MAX 24

#define OW_SEARCH_ROM  0xF0
#define OW_MATCH_ROM   0x55
#define DS_CONVERT      0x44
#define DS_READ_SCRATCH 0xBE
#define FAM_DS18B20  0x28
#define FAM_DS18S20  0x10
#define FAM_DS1822   0x22
#define FAM_DS2406       0x12
#define DS2406_CH_ACCESS 0xF5

#define DS84_ADDR     0x18
#define DS84_DRST     0xF0
#define DS84_SRP      0xE1
#define DS84_WCFG     0xD2
#define DS84_1WRS     0xB4
#define DS84_1WSB     0x87
#define DS84_1WRB     0x96
#define DS84_1WWB     0xA5
#define DS84_RP_RDATA 0xE1
#define DS84_ST_1WB   0x01
#define DS84_ST_PPD   0x02
#define DS84_ST_SBR   0x20

persist int ow_i2c_bus;
int ow_ok = 0;

// Ein gemeinsames Geräte-Array (Temperaturen + Schalter gemischt).
int   ow_cnt = 0;
char  ow_rom[24][8];
int   ow_fam[24];
float ow_t[24];          // letzte Temperatur (DS18x20)
int   ow_tvalid[24];
int   ow_trend[24];      // -1 fallend (hellblau) · 0 gleich (weiss) · +1 steigend (orange)
int   ow_conv[24];       // Konvertierung gestartet?
int   ow_swch[24];       // 1- oder 2-kanaliger DS2406
int   ow_swstate[24];    // Soll: bit0=A bit1=B (0=leitend/EIN, 1=AUS)
int   ow_swin[24];       // Istzustand (Rücklesung)
int   ow_swvalid[24];

char  ow_buf[16];
char  ow_84_buf[2];
char  fhex[20];          // ROM-Hex eines gefundenen Geräts (für den Abgleich)

// ---- Rollen -> Geräteindex (aus dem ROM-Abgleich) ----
int vl_id=-1; int hk_id=-1; int kv_id=-1; int sp1_id=-1; int sp2_id=-1;
int sp3_id=-1; int olks_id=-1; int sol_id=-1; int rm_id=-1; int atmp_id=-1; int rl_id=-1;
int led_id=-1; int pump_id=-1; int fan_id=-1; int olhz_id=-1; int pwstrip_id=-1; int mixer_id=-1;

// ---- Benannte Temperaturwerte ----
float vorlauf=0; float holzkessel=0; float kesselvorlauf=0;
float speicher1=0; float speicher2=0; float speicher3=0;
float olkessel=0; float solar=0; float raum=0; float aussen=0; float rucklauf=0;

// ---- Heizkessel-Verlauf fürs 1-h-Webchart (60 Slots × 1 Min) ----
#define HK_N 60
float hk_hist[HK_N];   // Ringpuffer der Holzkessel-Temperatur
int hk_pos=0;          // Schreibkopf (nächster Slot)
int hk_filled=0;       // gefüllte Slots (0..HK_N) -> x-Achse wächst bis 1 h
int hk_sec=0;          // Sekundenzähler fürs 1-Min-Sampling

// ---- UI / Steuerung (persistent) ----
persist int hzg=0; persist int solltmp=50;
persist int led=0; persist int pump=0; persist int fan=0;
persist int olhz=0; persist int pwstrip=0; persist int mixer=1;   // 0=Zu 1=Neutral 2=Auf
// Vorwerte für die Änderungserkennung (chg[])
int hzg_p=0; int led_p=0; int pump_p=0; int fan_p=0; int olhz_p=0; int pwstrip_p=0; int mixer_p=1;
int once=0;

// Nicht-blockierender Mischer-Puls (statt der delay()-Schleifen im Original)
int mix_active=0; int mix_until=0; int mix_led=0; int mix_pulsems=0; int mix_cool=0;

// Automatik-Trigger
float vl_prev=-999; int soll_prev=-999;

int ow_ti=0;            // Round-Robin-Index
int scnt=0;            // Temp-Lese-Index

// ============================================================================
// DS2484-Bridge
// ============================================================================
int ow_crc8(int len) {
    int crc=0; int i=0;
    while (i<len) {
        int b=ow_buf[i]&0xFF; int j=0;
        while (j<8) { int mix=(crc^b)&1; crc=crc>>1; if (mix) crc=crc^0x8C; b=b>>1; j=j+1; }
        i=i+1;
    }
    return crc;
}
int ow_84_cfg(int v) { return (((~v)&0x0F)<<4)|(v&0x0F); }
int ow_84_wait() {
    int i=0;
    while (i<50) {
        if (i2cRead0(DS84_ADDR, ow_84_buf, 1, ow_i2c_bus)>0) {
            int st=ow_84_buf[0]&0xFF; if (!(st&DS84_ST_1WB)) return st;
        }
        delay(1); i=i+1;
    }
    return 0xFF;
}
int ow_84_init() {
    int b=0;
    while (b<2) {
        if (i2cExists(DS84_ADDR, b)) {
            ow_i2c_bus=b;
            if (!i2cWrite0(DS84_ADDR, DS84_DRST, b)) return 0;
            delay(1);
            i2cWrite8(DS84_ADDR, DS84_WCFG, ow_84_cfg(0x01), b);   // APU an (Standard, Lüfter ok)
            return 1;
        }
        b=b+1;
    }
    return 0;
}
int ow_reset() {
    if (!i2cWrite0(DS84_ADDR, DS84_1WRS, ow_i2c_bus)) return 0;
    int st=ow_84_wait(); if (st==0xFF) return 0;
    return (st&DS84_ST_PPD)?1:0;
}
void ow_write(int v) { i2cWrite8(DS84_ADDR, DS84_1WWB, v&0xFF, ow_i2c_bus); ow_84_wait(); }
int ow_read() {
    i2cWrite0(DS84_ADDR, DS84_1WRB, ow_i2c_bus); ow_84_wait();
    i2cWrite8(DS84_ADDR, DS84_SRP, DS84_RP_RDATA, ow_i2c_bus);
    if (i2cRead0(DS84_ADDR, ow_84_buf, 1, ow_i2c_bus)>0) return ow_84_buf[0]&0xFF;
    return 0xFF;
}
void ow_wbit(int b) { i2cWrite8(DS84_ADDR, DS84_1WSB, b?0x80:0x00, ow_i2c_bus); ow_84_wait(); }
int ow_rbit() { i2cWrite8(DS84_ADDR, DS84_1WSB, 0x80, ow_i2c_bus); int st=ow_84_wait(); return (st&DS84_ST_SBR)?1:0; }

// ============================================================================
// ROM-Suche (Bitbang über die Bridge — DS2484 hat kein natives owSearch)
// ============================================================================
char ow_srom[8];

void ow_rom_hex(char dst[], int idx) {
    char hx[4]; int i=7; dst[0]=0;
    while (i>=0) { sprintf(hx, "%02X", ow_rom[idx][i]&0xFF); strcat(dst, hx); i=i-1; }
}

void ow_record_found() {
    int fam=ow_srom[0]&0xFF;
    if (ow_cnt>=OW_MAX) return;
    int i=0; while (i<8) { ow_rom[ow_cnt][i]=ow_srom[i]; i=i+1; }
    ow_fam[ow_cnt]=fam;
    ow_tvalid[ow_cnt]=0; ow_conv[ow_cnt]=0;
    ow_swstate[ow_cnt]=0xFF; ow_swin[ow_cnt]=0x05; ow_swvalid[ow_cnt]=0; ow_swch[ow_cnt]=2;
    ow_cnt=ow_cnt+1;
}

void ow_scan() {
    ow_cnt=0;
    int last_disc=0; int last_done=0; int i=0;
    while (i<8) { ow_srom[i]=0; i=i+1; }
    int total=0;
    while (total<OW_MAX) {
        if (last_done) break;
        if (!ow_reset()) break;
        ow_write(OW_SEARCH_ROM);
        int last_zero=0; int pos=1; int ok=1;
        while (pos<=64) {
            int id_bit=ow_rbit(); int cmp_bit=ow_rbit();
            if (id_bit==1 && cmp_bit==1) { ok=0; break; }
            int dir=0;
            if (id_bit==0 && cmp_bit==0) {
                if (pos==last_disc) dir=1;
                else if (pos>last_disc) dir=0;
                else { int bN=(pos-1)/8; int btN=(pos-1)%8; dir=(ow_srom[bN]>>btN)&1; }
                if (dir==0) last_zero=pos;
            } else dir=id_bit;
            int bN=(pos-1)/8; int btN=(pos-1)%8;
            if (dir) ow_srom[bN]=ow_srom[bN]|(1<<btN); else ow_srom[bN]=ow_srom[bN]&~(1<<btN);
            ow_wbit(dir); pos=pos+1;
        }
        if (!ok) break;
        last_disc=last_zero;
        if (last_disc==0) last_done=1;
        i=0; while (i<8) { ow_buf[i]=ow_srom[i]; i=i+1; }
        if (ow_crc8(8)!=0) { total=total+1; continue; }
        ow_record_found();
        total=total+1;
    }
}

void ow_select(int idx) {
    ow_write(OW_MATCH_ROM);
    int i=0; while (i<8) { ow_write(ow_rom[idx][i]&0xFF); i=i+1; }
}

// ============================================================================
// DS18x20-Temperatur
// ============================================================================
// HINWEIS Kalibrierung: das Original las ow(9 id+0x80)*6.25/100 (= roh/16,
// DS18B20-Standard) und multiplizierte die beiden DS18S20 (Rücklauf, Speicher3)
// zusätzlich mit 4.97. Hier wird der DS18S20-Standard (roh/2) verwendet. Stimmt
// ein Wert nicht mit der alten Anlage überein -> #define KORR_DS18S20 anpassen.
#define KORR_DS18S20 1.0

void ow_read_temp(int idx) {
    if (!ow_reset()) return;
    ow_select(idx);
    ow_write(DS_READ_SCRATCH);
    int i=0; while (i<9) { ow_buf[i]=ow_read(); i=i+1; }
    if (ow_crc8(9)!=0) return;
    int lsb=ow_buf[0]&0xFF; int msb=ow_buf[1]&0xFF;
    int raw=(msb<<8)|lsb; if (raw&0x8000) raw=raw|0xFFFF0000;
    if (ow_fam[idx]==FAM_DS18S20) ow_t[idx]=(raw/2.0)*KORR_DS18S20;
    else                         ow_t[idx]=raw/16.0;
    ow_tvalid[idx]=1;
}

// ============================================================================
// DS2406-Schalter  (PIO-Latch, aktiv-low wie in der Anlage)
// ============================================================================
// ow_swstate bit0=PIO-A bit1=PIO-B; 0 = Transistor leitend, 1 = AUS.
int ow_ds2406_write(int idx) {
    if (!ow_reset()) return 0;
    ow_select(idx);
    ow_write(DS2406_CH_ACCESS);
    ow_write(0x0C);                          // CCB1 = 0x0C IMMER (CHS=11) — wie der native ow(8)
    ow_write(0xFF);
    int info=ow_read();
    int a_on=((ow_swstate[idx]&0x01)==0);
    int b_on=((ow_swstate[idx]&0x02)==0);
    // Ausgabe-Byte exakt wie der native ow(8 sid+par<<6): PIO in Bit6/Bit7.
    // bit6=PIO-A, bit7=PIO-B; gesetzt=AUS, 0=EIN. (Das alte 0x55/0xAA/0xFF + CCB1=0x04
    // haben den Ausgang nie wirklich gesetzt — Rolfs "zeigt EIN, ist es aber nicht".)
    int data=0;
    if (!a_on) data=data|0x40;               // bit6 = PIO-A
    if (!b_on) data=data|0x80;               // bit7 = PIO-B
    ow_write(data);
    ow_reset();
    int mapped=0x05;
    if (a_on) mapped=mapped&0xFE;
    if (b_on) mapped=mapped&0xFB;
    ow_swin[idx]=mapped; ow_swvalid[idx]=1;
    return 1;
}
int ow_ds2406_read(int idx) {
    if (!ow_reset()) return 0;
    ow_select(idx);
    ow_write(DS2406_CH_ACCESS);
    if (ow_swch[idx]>=2) ow_write(0x4C); else ow_write(0x44);
    ow_write(0xFF);
    int info=ow_read();
    if (info&0x40) ow_swch[idx]=2; else ow_swch[idx]=1;
    int mapped=0x05;
    if ((info&0x01)==0) mapped=mapped&0xFE;
    if ((info&0x02)==0) mapped=mapped&0xFB;
    ow_swin[idx]=mapped; ow_swvalid[idx]=1;
    ow_reset();
    return 1;
}

// set_act(idx,par): einkanalig — PIO-A-Ausgabebit = par, wie nativ ow(8 sid+par<<6)
// (Aufruf wie Original mit led^1). ow_swstate bit0 wird DIREKT auf par gesetzt, weil
// ow_ds2406_write jetzt bit6 = bit0 schreibt (vorher invertiert -> es schaltete nichts).
void set_act(int idx, int par) {
    if (idx<0) return;
    if (par) ow_swstate[idx]=ow_swstate[idx]|0x01; else ow_swstate[idx]=ow_swstate[idx]&0xFE;
    ow_ds2406_write(idx);
}
// set6pin(idx,par): zweikanalig — PIO-A-Bit=par&1, PIO-B-Bit=par&2 (wie nativ par<<6)
void set6pin(int idx, int par) {
    if (idx<0) return;
    ow_swch[idx]=2;
    if (par&1) ow_swstate[idx]=ow_swstate[idx]|0x01; else ow_swstate[idx]=ow_swstate[idx]&0xFE;
    if (par&2) ow_swstate[idx]=ow_swstate[idx]|0x02; else ow_swstate[idx]=ow_swstate[idx]&0xFD;
    ow_ds2406_write(idx);
}

// ============================================================================
// ROM -> Rolle  (Rolfs echte Anlagen-Adressen)
// ============================================================================
void ow_map_roles() {
    vl_id=-1; hk_id=-1; kv_id=-1; sp1_id=-1; sp2_id=-1; sp3_id=-1;
    olks_id=-1; sol_id=-1; rm_id=-1; atmp_id=-1; rl_id=-1;
    led_id=-1; pump_id=-1; fan_id=-1; olhz_id=-1; pwstrip_id=-1; mixer_id=-1;
    int i=0;
    while (i<ow_cnt) {
        ow_rom_hex(fhex, i);
        // Temperaturen
        // Temperatur-ROMs aus Rolfs NEUEM Skript (2026-06-21). Die alten DS18S20
        // (Rücklauf/Speicher3, Familie 10) sind durch DS18B20 ersetzt; mehrere
        // Sensoren wurden neu zugeordnet.
        if      (strcmp(fhex,"A803168C0D00FF28")==0) vl_id=i;
        else if (strcmp(fhex,"9900000238A41528")==0) rl_id=i;    // war Vorlauf, jetzt Rücklauf
        else if (strcmp(fhex,"770000023881C528")==0) hk_id=i;
        else if (strcmp(fhex,"1C0516853013FF28")==0) kv_id=i;
        else if (strcmp(fhex,"130516941725FF28")==0) sp1_id=i;
        else if (strcmp(fhex,"2A03168C3086FF28")==0) sp2_id=i;   // Sensor getauscht (war 01000000BB3CE928)
        else if (strcmp(fhex,"57031683FA86FF28")==0) sp3_id=i;
        else if (strcmp(fhex,"44000002389B9128")==0) olks_id=i;
        else if (strcmp(fhex,"A000000238874D28")==0) sol_id=i;
        else if (strcmp(fhex,"7F000000BB9A0228")==0) rm_id=i;
        else if (strcmp(fhex,"AE051693DA31FF28")==0) atmp_id=i;
        // Schalter
        else if (strcmp(fhex,"2A0000007D299F12")==0) led_id=i;
        else if (strcmp(fhex,"9F00000036B09812")==0) pump_id=i;
        else if (strcmp(fhex,"A300000036A69C12")==0) fan_id=i;
        else if (strcmp(fhex,"EC0000007D323612")==0) olhz_id=i;
        else if (strcmp(fhex,"9E0000007D593812")==0) pwstrip_id=i;
        else if (strcmp(fhex,"0C00000072DB6A12")==0) mixer_id=i;
        i=i+1;
    }
}

// ============================================================================
// Initialisierung
// ============================================================================
void ow_reinit() {
    ow_ok=0; ow_cnt=0;
    if (!ow_84_init()) { addLog("Heizung26: kein DS2484 auf I2C"); return; }
    addLog("Heizung26: DS2484 auf I2C-Bus %d", ow_i2c_bus);
    if (!ow_reset()) { addLog("Heizung26: Bus leer"); return; }
    ow_scan();
    ow_map_roles();
    addLog("Heizung26: %d Geraete, Vorlauf=%d Pumpe=%d Mischer=%d", ow_cnt, vl_id, pump_id, mixer_id);
    ow_ok=1;
    once=0;
}

// ============================================================================
// Schalter anwenden (chg[]-Äquivalent) + Heizautomatik
// ============================================================================
void apply_switches() {
    if (hzg!=hzg_p)         hzg_p=hzg;            // hzg steuert nur die Logik
    if (led!=led_p)         { set_act(led_id, led^1);         led_p=led; }
    if (pump!=pump_p)       { set_act(pump_id, pump^1);       pump_p=pump; }
    if (fan!=fan_p)         { set_act(fan_id, fan^1);         fan_p=fan; }
    if (olhz!=olhz_p)       { set_act(olhz_id, olhz^1);       olhz_p=olhz; }
    if (pwstrip!=pwstrip_p) { set_act(pwstrip_id, pwstrip^1); pwstrip_p=pwstrip; }
    if (mixer!=mixer_p) {
        if      (mixer==0) set6pin(mixer_id, 2);   // Zu
        else if (mixer==1) set6pin(mixer_id, 3);   // Neutral
        else if (mixer==2) set6pin(mixer_id, 1);   // Auf
        mixer_p=mixer;
    }
}

// nicht-blockierender Mischer-Puls (ersetzt die delay()-Schleifen aus >t2).
// pulse_ms = Stellzeit; danach (in EverySecond) zurück auf neutral + Abkühlzeit.
void mixer_pulse(int dir, int pulse_ms, int withled) {
    mixer=dir;                 // 2=öffnen (Auf), 0=schließen (Zu)
    mix_until=millis()+pulse_ms;
    mix_pulsems=pulse_ms;
    mix_active=1; mix_led=withled;
    if (withled) led=1;
}

void heizautomatik() {
    if (hzg>0) {
        if (kesselvorlauf>=solltmp) {
            pump=1;
            float d=solltmp-vorlauf; if (d<0) d=-d;          // abs
            int pms=((int)d)*1800;                            // Stellzeit = Abweichung * 1800 ms (neues Skript)
            if (d>1 && !mix_active && millis()>=mix_cool) {   // nicht während Puls oder Abkühlzeit
                if (vorlauf<solltmp)           mixer_pulse(2, pms, 0);   // zu kalt -> öffnen (Auf)
                else if ((int)vorlauf>solltmp) mixer_pulse(0, pms, 1);    // zu warm -> schließen (Zu) (+LED)
            }
        }
    }
    // Heizung AUS: Pumpe NICHT mehr zwangsweise auf 0 — sonst überschrieb die Automatik
    // bei jeder Temp-Messung die Handschaltung (das war Rolfs "Pumpe schaltet nicht").
    // Bei Heizung-AUS ist die Pumpe jetzt manuell; bei Heizung-AN folgt sie weiter dem Kessel.
    // Mischer-Endlage erreicht?
    if (hzg>0 && vorlauf>solltmp && mixer==3) mixer=2;
    if (hzg>0 && vorlauf<solltmp && mixer==1) mixer=2;
}

// ============================================================================
// Callbacks
// ============================================================================
void EverySecond() {
    if (!ow_ok) return;

    // Erststart: alle Schalter auf den gespeicherten Zustand bringen
    if (!once) {
        set_act(led_id, led^1); set_act(pump_id, pump^1); set_act(fan_id, fan^1);
        set_act(olhz_id, olhz^1); set_act(pwstrip_id, pwstrip^1);
        set6pin(mixer_id, 3); mixer=1;   // Mischer-Start: neutral (Hardware + Variable)
        hzg_p=hzg; led_p=led; pump_p=pump; fan_p=fan; olhz_p=olhz; pwstrip_p=pwstrip; mixer_p=mixer;
        once=1;
    }

    // Mischer-Puls beenden -> neutral, dann Abkühlzeit (Original: pulse*4 + 10 s)
    if (mix_active && millis()>=mix_until) {
        mixer=1; if (mix_led) led=0;   // zurück auf Neutral
        mix_active=0;
        mix_cool=millis()+mix_pulsems*4+10000;
    }

    // Round-Robin: ein Gerät pro Tick lesen
    if (ow_cnt>0) {
        int i=ow_ti;
        if (ow_fam[i]==FAM_DS2406) {
            ow_ds2406_read(i);
        } else {
            if (ow_conv[i]) {
                float oldv=ow_t[i]; int wasv=ow_tvalid[i];
                ow_read_temp(i);
                if (!wasv)             ow_trend[i]=0;   // erste Messung: weiss
                else if (ow_t[i]>oldv) ow_trend[i]=1;   // steigend -> orange
                else if (ow_t[i]<oldv) ow_trend[i]=-1;  // fallend  -> hellblau
                else                   ow_trend[i]=0;   // unverändert -> weiss
            }
            if (ow_reset()) { ow_select(i); ow_write(DS_CONVERT); ow_conv[i]=1; }
        }
        ow_ti=ow_ti+1; if (ow_ti>=ow_cnt) ow_ti=0;
    }

    // benannte Temperaturen übernehmen
    if (vl_id>=0)   vorlauf=ow_t[vl_id];
    if (hk_id>=0)   holzkessel=ow_t[hk_id];
    if (kv_id>=0)   kesselvorlauf=ow_t[kv_id];
    if (sp1_id>=0)  speicher1=ow_t[sp1_id];
    if (sp2_id>=0)  speicher2=ow_t[sp2_id];
    if (sp3_id>=0)  speicher3=ow_t[sp3_id];
    if (olks_id>=0) olkessel=ow_t[olks_id];
    if (sol_id>=0)  solar=ow_t[sol_id];
    if (rm_id>=0)   raum=ow_t[rm_id];
    if (atmp_id>=0) aussen=ow_t[atmp_id];
    if (rl_id>=0)   rucklauf=ow_t[rl_id];

    // Holzkessel fürs 1-h-Webchart. Beim ERSTEN gültigen Wert den ganzen Ring
    // vorbelegen -> Chart sofort sichtbar, festes 1-h-Fenster (kein Anwachsen).
    if (hk_id>=0 && ow_tvalid[hk_id]) {
        if (hk_filled==0) {
            int k=0; while (k<HK_N) { hk_hist[k]=holzkessel; k=k+1; }
            hk_filled=HK_N; hk_pos=0;
        }
        hk_sec=hk_sec+1;
        if (hk_sec>=60) {
            hk_sec=0;
            hk_hist[hk_pos]=holzkessel;
            hk_pos=(hk_pos+1)%HK_N;
        }
    }

    // Automatik bei Änderung von Vorlauf oder Solltemperatur (chg[]-Gate)
    if (vorlauf!=vl_prev || solltmp!=soll_prev) {
        vl_prev=vorlauf; soll_prev=solltmp;
        heizautomatik();
    }

    apply_switches();
}

void Rescan() { ow_reinit(); }

// ---- MQTT ----
void JsonCall() {
    if (!ow_ok) return;
    char b[64];
    sprintf(b, ",\"Vorlauf\":%.1f", vorlauf); responseAppend(b);
    sprintf(b, ",\"Ruecklauf\":%.1f", rucklauf); responseAppend(b);
    sprintf(b, ",\"Holzkessel\":%.1f", holzkessel); responseAppend(b);
    sprintf(b, ",\"KesselVorlauf\":%.1f", kesselvorlauf); responseAppend(b);
    sprintf(b, ",\"Speicher1\":%.1f", speicher1); responseAppend(b);
    sprintf(b, ",\"Speicher2\":%.1f", speicher2); responseAppend(b);
    sprintf(b, ",\"Speicher3\":%.1f", speicher3); responseAppend(b);
    sprintf(b, ",\"Oelkessel\":%.1f", olkessel); responseAppend(b);
    sprintf(b, ",\"Solar\":%.1f", solar); responseAppend(b);
    sprintf(b, ",\"Raum\":%.1f", raum); responseAppend(b);
    sprintf(b, ",\"Aussen\":%.1f", aussen); responseAppend(b);
    sprintf(b, ",\"Heizung\":%d", hzg); responseAppend(b);
    sprintf(b, ",\"Pumpe\":%d", pump); responseAppend(b);
    sprintf(b, ",\"Mischer\":%d", mixer); responseAppend(b);
}

// Trend-Farbe der Temperaturen: steigend=orange, fallend=hellblau, gleich=weiss
void setcol(char dst[], int t) {
    if (t>0)      strcpy(dst, "orange");
    else if (t<0) strcpy(dst, "#5cf");
    else          strcpy(dst, "#fff");
}

void WebCall() {
    if (!ow_ok) { webSend("{s}Heizung26{m}kein Bus{e}"); return; }
    char vt[96]; char col[8];
    if (vl_id>=0)   { setcol(col, ow_trend[vl_id]);   sprintf(vt, "{s}Vorlauf{m}<span style='color:%s'>%.2f °C</span>{e}", col, vorlauf); webSend(vt); }
    if (rl_id>=0)   { setcol(col, ow_trend[rl_id]);   sprintf(vt, "{s}Rücklauf{m}<span style='color:%s'>%.2f °C</span>{e}", col, rucklauf); webSend(vt); }
    if (hk_id>=0)   { setcol(col, ow_trend[hk_id]);   sprintf(vt, "{s}Holzkessel{m}<span style='color:%s'>%.2f °C</span>{e}", col, holzkessel); webSend(vt); }
    if (kv_id>=0)   { setcol(col, ow_trend[kv_id]);   sprintf(vt, "{s}Kessel-Vorlauf{m}<span style='color:%s'>%.2f °C</span>{e}", col, kesselvorlauf); webSend(vt); }
    if (sp1_id>=0)  { setcol(col, ow_trend[sp1_id]);  sprintf(vt, "{s}Speicher 1{m}<span style='color:%s'>%.2f °C</span>{e}", col, speicher1); webSend(vt); }
    if (sp2_id>=0)  { setcol(col, ow_trend[sp2_id]);  sprintf(vt, "{s}Speicher 2{m}<span style='color:%s'>%.2f °C</span>{e}", col, speicher2); webSend(vt); }
    if (sp3_id>=0)  { setcol(col, ow_trend[sp3_id]);  sprintf(vt, "{s}Speicher 3{m}<span style='color:%s'>%.2f °C</span>{e}", col, speicher3); webSend(vt); }
    if (olks_id>=0) { setcol(col, ow_trend[olks_id]); sprintf(vt, "{s}Ölkessel{m}<span style='color:%s'>%.2f °C</span>{e}", col, olkessel); webSend(vt); }
    if (sol_id>=0)  { setcol(col, ow_trend[sol_id]);  sprintf(vt, "{s}Solar{m}<span style='color:%s'>%.2f °C</span>{e}", col, solar); webSend(vt); }
    if (rm_id>=0)   { setcol(col, ow_trend[rm_id]);   sprintf(vt, "{s}Raum{m}<span style='color:%s'>%.2f °C</span>{e}", col, raum); webSend(vt); }
    if (atmp_id>=0) { setcol(col, ow_trend[atmp_id]); sprintf(vt, "{s}Aussen{m}<span style='color:%s'>%.2f °C</span>{e}", col, aussen); webSend(vt); }
    sprintf(vt, "{s}Status{m}Heizung %d · Pumpe %d · Mischer %d{e}", hzg, pump, mixer); webSend(vt);
    sprintf(vt, "{s}Geräte am Bus{m}%d (Vorlauf-ID %d){e}", ow_cnt, vl_id); webSend(vt);

    // --- Steuerung direkt auf der Hauptseite (wie Scripter >W) ---
    // Alle Bedienelemente in einem gemeinsamen Rahmen (fieldset)
    webSend("<fieldset style='border:1px solid #888;border-radius:5px;padding:6px 12px;margin-top:8px'><legend><b>Steuerung</b></legend>");
    webCheckbox(hzg, "Heizung");
    webNumber(solltmp, 0, 100, "Soll-Temperatur °C");
    webCheckbox(led, "LED");
    webCheckbox(pump, "Pumpe");
    webCheckbox(fan, "Ventilator");
    webCheckbox(olhz, "Ölheizung");
    webCheckbox(pwstrip, "Steckerleiste");
    webSend("<b>Vierwegehahn</b>");
    webSend("<div id='hzgmix'>");
    webRadio(mixer, "Zu|Neutral|Auf");                    // Zu=Option1, Neutral=2, Auf=3
    webSend("</div><style>#hzgmix fieldset>div:nth-of-type(1) label{color:#48f}#hzgmix fieldset>div:nth-of-type(3) label{color:#f44}</style>");
    webSend("</fieldset>");
}

// (Die Steuerung liegt jetzt direkt in WebCall() auf der Hauptseite — kein Untermenü.)

// ---- 1-h-Verlaufschart des Heizkessels (Holzkessel) auf der Hauptseite ----
void WebPage() {
    if (hk_id<0 || hk_filled==0) return;   // ab dem ersten gültigen Wert (Ring vorbelegt)
    webSend("<div style='margin-left:-30px'>");   // ottelos Chart-Zentrierung (neg. x-Offset)
    WebChartSize(600, 260);
    // FESTES 1-h-Fenster: count=HK_N (60 × 1-Min), Ring beim Start vorbelegt -> sofort sichtbar.
    // 1 Nachkommastelle, interval=1 Min, ymin/ymax=0 -> Auto-Skala.
    WebChart(0, "Heizkessel (1 h)", "°C", 0xe74c3c, hk_pos, HK_N, hk_hist, 1, 1, 0.0, 0.0);
    webSend("</div>");
}

// ---- Konsole: HZG ----
void Command(char cmd[]) {
    char cs[64]; strcpy(cs, cmd); int len=strlen(cs);
    int pos=0; while (pos<len && cs[pos]==' ') pos=pos+1;
    if (pos>=len) { ow_reinit(); }   // "HZG" allein = neu scannen
    char r[96];
    sprintf(r, "Geraete %d, Vorlauf %.1f, Pumpe %d, Mischer %d", ow_cnt, vorlauf, pump, mixer);
    responseCmnd(r);
}

int main() {
    ow_reinit();
    addCommand("HZG");
    return 0;
}