commit 46c45518a3e0c2aa229ce0cd7eb02f74f93101e6 Author: Thebille Date: Thu Aug 6 06:09:57 2026 +0200 Initial commit: Haussteuerung V2/V3 Web-Interface diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cef4e78 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Betriebssystem-Dateien +Thumbs.db +Thumbs.db:encryptable +Desktop.ini +.DS_Store +*.swp +*.swo + +# Editor/IDE +.vscode/ +.idea/ +*.sublime-project +*.sublime-workspace + +# Temporäre Dateien +*.tmp +*.temp +*.log + +# Node.js (falls zukünftig verwendet) +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Build-Ausgaben +dist/ +build/ + +# Sicherheitskopien +*.bak +*.backup +*~ diff --git a/PROJECT.md b/PROJECT.md new file mode 100644 index 0000000..85939f9 --- /dev/null +++ b/PROJECT.md @@ -0,0 +1,177 @@ +# Haussteuerung V2 - Projekt Dokumentation + +## Projektübersicht +Webbasierte Haussteuerung für SPS-Steuerung (AWP-Webserver) + +## Dateistruktur +``` +web/ +├── indexV2.htm # Hauptseite mit Iframes +├── form.css # Zentrales CSS +├── lichtV2.htm # Lichtsteuerung +├── heizungV2.htm # Heizungssteuerung +├── steckdosenV2.htm # Steckdosensteuerung +├── rolladen.htm # Rolladensteuerung +├── stromzaehler.htm # Stromzähler mit Chart +├── licht_state.html # Licht Status (wird von SPS befüllt) +├── heizung_state.html # Heizung Status +├── steckdosen_state.html # Steckdosen Status +├── stromzaehler_state.html # Stromzähler Status +├── WebAppConfig.json # AWP-Webserver Konfiguration +├── Script/ +│ └── tth_script.js # JavaScript Funktionen +└── images/ # Bilder und Icons +``` + +## AWP Variablen + +### Licht (lichtV2.htm) +- `"Lichtmerker Küche"` +- `"Lichtmerker Essen"` +- `"Lichtmerker Wohnen 1"` +- `"Lichtmerker Wohnen 2"` +- `"Lichtmerker HWR"` +- `"Lichtmerker Arbeiten"` +- `"Lichtmerker Kind 3"` +- `"Lichtmerker Dusche"` +- `"Lichtmerker Abstellraum"` +- `"Lichtmerker Eltern"` +- `"Lichtmerker Noah"` +- `"Lichtmerker Niko"` +- `"Lichtmerker Bad"` +- `"Kickertaste"` +- `"wled_DB".Helligkeit` +- `"wled_DB".Befehl` +- `"wled_DB".RGB_hex` + +### Heizung (heizungV2.htm) +- `"Temp Wohnen"`, `"Temp Arbeiten"`, `"Temp HWR"`, `"Temp Bad"`, `"Temp Eltern"`, `"Temp Noah"`, `"Temp Mika"`, `"Temp Niko"`, `"Temp Dusche"` +- `"Einstellungen".Heizung."Heizung Wohnen min/max"` +- `"Einstellungen".Heizung."Heizung Arbeiten min/max"` +- `"Einstellungen".Heizung."Heizung HWR min/max"` +- `"Einstellungen".Heizung."Heizung Bad min/max"` +- `"Einstellungen".Heizung."Heizung Eltern min/max"` +- `"Einstellungen".Heizung."Heizung Kind1 min/max"` +- `"Einstellungen".Heizung."Heizung Kind2 min/max"` +- `"Einstellungen".Heizung."Heizung Gäste min/max"` +- `"Einstellungen".Heizung."Heizung Dusche min/max"` + +### Steckdosen (steckdosenV2.htm) +- `"Steckdose Deele Garder"` +- `"Steckdose Wohnen"` +- `"Steckdose Essen"` +- `"Steckdose Arbeiten"` +- `"Steckdose Kind 1"` +- `"Steckdose Kind 2"` +- `"Steckdose Gäste"` +- `"Steckdose Eltern"` +- `"Arduino Settimio"."Funksteckdose 1 an/aus"` +- `"Arduino Settimio"."KDY 1 an/aus"` +- `"Arduino Settimio"."KDY 2 an/aus"` +- `"Arduino Settimio"."Funksteckdose 4 an/aus"` + +### Rolladen (rolladen.htm) +- `"Rolladenmerker Ess l/r/m auf/ab"` +- `"Rolladenmerker Woh l/r auf/ab"` +- `"Rolladenmerker Wohtü auf/ab"` +- `"Rolladenmerker Arbei auf/ab"` +- `"Rolladenmerker HWR auf/ab"` +- `"Rolladenmerker Küc l/r auf/ab"` +- `"Rolladenmerker Elter auf/ab"` +- `"Rolladenmerker Noah auf/ab"` +- `"Rolladenmerker Bad auf/ab"` +- `"Rolladenmerker Niko auf/ab"` +- `"Rolladenmerker Mika auf/ab"` +- `"Rolladenmerker Dusche au/ab"` +- `"Rolladen alle auf/ab"` +- `"Jalousie_DB".Urlaub` + +### Stromzähler (stromzaehler.htm) +- 12 Zählerwerte aus `stromzaehler_state.html` +- Berechnung: -1 bis -8 als Differenzwerte + +### Sonstige (indexV2.htm) +- `"Aufgang_Untergang".aktuelle_CPU_Zeit.*` +- `"Aufgang_Untergang".Aufgang.HOUR/MINUTE` +- `"Aufgang_Untergang".Untergang.HOUR/MINUTE` +- `"Aufgang_Untergang".Aufgang_offset` +- `"Aufgang_Untergang".Untergang_offset` +- `"Türöffner Handy"` +- `"Arduino Settimio".Garagentor 1` +- `"Arduino Settimio".Garagentor 2` + +## CSS (form.css) +- `.ui-card` - Card-Container mit Hintergrund +- `.ui-table` - Tabellen-Styling +- `.ui-btn` - Button-Styling (primary, on, off) +- `.ui-iconbtn` - Icon-Buttons +- `.auto-style6` - Versteckte Status-Elemente (color: transparent, font-size: 1px) + +## JavaScript +- `tth_script.js` - Icon-Funktionen (leuchtet_icon, heizt_icon, strom_icon, urlaub_icon) +- Iframe-Resize in indexV2.htm +- Stromzähler: Differenzberechnung für -1 bis -8 +- Stromzähler: Canvas-basierter Leistungs-Chart (letzte 5 Min.) + +## Wichtige Änderungen +- OutputArea in allen Seiten mit `clip:rect(0,0,0,0)` versteckt +- Header-Zeile "Raum | Taster" aus steckdosenV2.htm und rolladen.htm entfernt +- Erste Tabellenzeile nicht mehr hervorgehoben +- Stromzähler-Werte untereinander angeordnet +- Stromzähler: -1 bis -8 als Differenzwerte + +## Server +- AWP-Webserver auf SPS (192.168.0.90) +- Standard-Seite: indexV2.htm + +## V3 (Single-Page, neu) +Parallel zu V2, lässt V2 unangetastet. Erreichbar über `indexV3.htm`. + +### Dateien V3 +- `indexV3.htm` — Produktivseite mit SPS-Web-API-Anbindung. +- `indexV3_preview.htm` — reine Optik-Vorschau ohne SPS, mit Dummy-Daten. +- `formV3.css` — CSS für V3 (Grid, Karten, responsive Stromzähler-Bereich). +- `Script/tth_v3_api.js` — JavaScript für `indexV3.htm`. + +### Architektur +- Keine Iframes, keine AWP-Formulare. +- Kommunikation mit der S7-1500 über die integrierte Web-API (`/api/jsonrpc`). +- Lesen per `PlcProgram.Read` (Batch), schreiben per `PlcProgram.Write`. +- Anmeldung über `Api.Login` mit Token (aktuell Benutzer `111`). +- Status wird zyklisch gelesen und in `lastValues` gepuffert. +- Schreibaktionen erfolgen direkt beim Klick auf Buttons/Inputs. + +### Module in indexV3.htm +1. **Stromzähler** — Tacho, Werte (Bezug/Einspeisung/Wemos), Power-Chart, Daily-Chart. +2. **Licht** — 14 Räume + WLED (Helligkeit/Farbe). +3. **Heizung** — 9 Räume mit IST, SOLL min/max, heizt-Status. +4. **Rolladen** — alle Räume + Urlaub-Modus. +5. **Steckdosen** — 8 normale Steckdosen + 4 Funksteckdosen. +6. **Haus** — Haustür + Garagentor. +7. **Uhrzeit** — CPU-Uhr, Sonnenauf-/untergang. + +### Wichtige Variablen-Unterschiede +- **Licht:** Lesen aus `licht_state.html` (`"Licht Küche"` etc.), Schreiben auf `"Lichtmerker ..."` etc. (unterschiedliche Variablen!). +- **Steckdosen:** Lesen und Schreiben identisch. Normale Steckdosen sind Bool: `false` = an, `true` = aus. Funksteckdosen haben separate an/aus-Variablen. +- **Rolladen:** Separate auf/ab-Variablen, Schreibwert `true`. +- **Heizung:** SOLL-Werte werden als Float geschrieben. +- **WLED:** `"wled_DB".Helligkeit` (Int), `"wled_DB".RGB_hex` (String), `"wled_DB".Befehl` (Int). + +### Charts +- **Tacho (`v3_gauge`):** Canvas, 180°-Skala, -8000 bis +8000 W, grün=Einspeisung, rot=Bezug. +- **Power-Chart (`v3_power_chart`):** Letzte 5 Minuten Leistung, weiche Kurve, Legende "akt. Leistung: xx W". +- **Daily-Chart (`v3_daily_chart`):** Balken für die letzten 8 Tage, Wochentage als X-Achse, heute gelb hervorgehoben, blaue Linie für täglichen Bezug. + +### Responsive Verhalten +- Stromzähler-Bereich bricht auf kleinen Bildschirmen um. +- Canvas-Elemente skalieren mit `aspect-ratio: 2 / 1` und bleiben scharf (HiDPI über `fitCanvas`). + +### Bekannte Einschränkungen / TODO +- Login-Daten (`user: '111'`, `password: '1q2w3e$R'`) sind im Klartext in `tth_v3_api.js` hinterlegt. +- Heizungs-Validierung prüft nur `min < max` clientseitig. +- Keine Rückmeldung, ob ein Schreibvorgang in der SPS tatsächlich wirksam wurde (nur API-Antwort). + +### Preview +- `indexV3_preview.htm` enthält feste Dummy-Werte und kein Polling. +- Wird verwendet, um das Layout offline zu testen. +- Taster in der Preview haben keine Funktion. diff --git a/Script/tth_script.js b/Script/tth_script.js new file mode 100644 index 0000000..9582f97 --- /dev/null +++ b/Script/tth_script.js @@ -0,0 +1,107 @@ +// function to check value +function check() +{ + var x = document.getElementById("wert1"); + if (x.value >= 1 && x.value <= 10 ) + { + return true; + } + else + { + alert ("Enter Flowrate between 1 and 10"); + document.getElementById("wert1").value = ""; + return false; + } +} + +function check_heizung(raum) { + var ist_min = document.getElementById("ist_" + raum + "_min").innerText; + var soll_min = document.getElementById("soll_" + raum + "_min").value; + var ist_max = document.getElementById("ist_" + raum + "_max").innerText; + var soll_max = document.getElementById("soll_" + raum + "_max").value; + + console.log('ist min ' + ist_min); + console.log('soll min ' + soll_min); + console.log('ist max ' + ist_max); + console.log('soll max ' + soll_max); + + if (soll_min != null) { + if (soll_min >= ist_max) { + alert ("Temperatur muss kleiner sein als max"); + document.getElementById("soll_" + raum + "_min").value = ""; + } + return false; + } + else + { + return true; + } + + if (soll_max != null) { + if (soll_max <= ist_min) { + alert ("Temperatur muss kleiner sein als max"); + document.getElementById("soll_" + raum + "_max").value = ""; + } + return false; + } + else + { + return true; + } +} + +function heizt_icon(label, status) { + //console.log(label); + if (status == 1) { + document.getElementById(label).src = "images/heizt1.png"; + } + else { + document.getElementById(label).src = "images/heizt0.png"; + } + return(label, status); +} + +function leuchtet_icon(label, status) { + //console.log(label); + if (status == 1) { + document.getElementById(label).src = "images/lampe1.jpg"; + } + else { + document.getElementById(label).src = "images/lampe0.jpg"; + } + return(label, status); +} + +function strom_icon(label, status) { + //console.log(label); + if (status == 1) { + document.getElementById(label).src = "images/blitz0.jpg"; + } + else { + document.getElementById(label).src = "images/blitz1.jpg"; + } + return(label, status); +} + +function wasser_icon(label, status) { + //console.log(label); + if (status == 1) { + document.getElementById(label).src = "images/wasser0.jpg"; + } + else { + document.getElementById(label).src = "images/wasser1.jpg"; + } + return(label, status); +} + +function urlaub_icon(label, status) { + //console.log(label); + if (status == 1) { + document.getElementById(label).src = "images/urlaub1.jpg"; + } + else { + document.getElementById(label).src = "images/urlaub0.jpg"; + } + return(label, status); +} + diff --git a/Script/tth_v3.js b/Script/tth_v3.js new file mode 100644 index 0000000..558e040 --- /dev/null +++ b/Script/tth_v3.js @@ -0,0 +1,349 @@ +// ==================================================================== +// tth_v3.js – Logik fuer indexV3.htm (Single-Page-Haussteuerung) +// +// Liest Status aus den vorhandenen *_state.html Dateien (UNVERAENDERT) +// per AJAX, parst sie isoliert mit DOMParser und liest die Werte ueber +// textContent. Dadurch: +// - kein innerText/"hidden"-Problem (textContent ist render-unabhaengig) +// - keine ID-Kollisionen (jede State-Datei landet in eigenem Dokument) +// Zeichnet Tacho + Leistungsverlauf selbst auf Canvas -> kein Internet noetig. +// ==================================================================== + +(function () { + 'use strict'; + + // Im versteckten Schreib-Iframe (target="awp_sink") NICHTS tun. + // Verhindert doppeltes Polling, wenn ein Formular gepostet wird. + if (window.top !== window.self) { return; } + + // ---- kleine Helfer ------------------------------------------------- + + function txt(doc, id) { + var el = doc.getElementById(id); + return el ? (el.textContent || '').replace(/\s+/g, ' ').trim() : ''; + } + + function setText(id, value) { + var el = document.getElementById(id); + if (el) { el.textContent = value; } + } + + // Status-Punkt umschalten. kind: 'on' (gruen) oder 'active' (gelb) + function setDot(id, on, kind) { + var el = document.getElementById(id); + if (!el) { return; } + el.classList.remove('v3-status-dot--on', 'v3-status-dot--off', 'v3-status-dot--active'); + if (on) { + el.classList.add(kind === 'active' ? 'v3-status-dot--active' : 'v3-status-dot--on'); + } else { + el.classList.add('v3-status-dot--off'); + } + } + + function pad(n) { + n = parseInt(n, 10); + if (isNaN(n)) { return '--'; } + return n < 10 ? '0' + n : '' + n; + } + + // Generischer Poll-Loop: holt url, parst sie, ruft handler(doc), + // und plant sich selbst nach 'interval' ms neu (auch bei Fehler). + function poll(url, handler, interval) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url + '?_=' + Date.now(), true); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + try { + var doc = new DOMParser().parseFromString(xhr.responseText, 'text/html'); + handler(doc); + } catch (e) { /* defensiv: einzelner Fehler stoppt den Loop nicht */ } + } + setTimeout(function () { poll(url, handler, interval); }, interval); + } + }; + xhr.send(null); + } + + // ---- LICHT --------------------------------------------------------- + // leuchtetx1..14 == '1' -> an + function handleLicht(doc) { + for (var i = 1; i <= 14; i++) { + setDot('li_dot_' + i, txt(doc, 'leuchtetx' + i) === '1', 'on'); + } + } + + // ---- HEIZUNG ------------------------------------------------------- + function tempClass(v) { + var t = parseFloat(v); + if (isNaN(t)) { return 'v3-temp--normal'; } + if (t >= 24) { return 'v3-temp--hot'; } + if (t >= 22) { return 'v3-temp--warm'; } + return 'v3-temp--normal'; + } + + function handleHeizung(doc) { + for (var i = 1; i <= 9; i++) { + // aktuelle Temperatur (heiztx101..109) + var temp = txt(doc, 'heiztx' + (100 + i)); + var tEl = document.getElementById('he_temp_' + i); + if (tEl) { + tEl.textContent = (temp === '' ? '–' : temp + ' °C'); + tEl.className = 'v3-temp ' + tempClass(temp); + } + // heizt aktiv? (heiztx1..9) + setDot('he_dot_' + i, txt(doc, 'heiztx' + i) === '1', 'active'); + // aktuell eingestellte Grenzen (heiz_min/max 1..9) + var istMin = txt(doc, 'heiz_min' + i); + var istMax = txt(doc, 'heiz_max' + i); + setText('he_ist_' + i, (istMin || '–') + ' / ' + (istMax || '–')); + } + } + + // ---- STECKDOSEN ---------------------------------------------------- + // ACHTUNG: invertierte Logik wie in V2 -> stromx == '0' bedeutet AN. + function handleSteckdosen(doc) { + for (var i = 1; i <= 8; i++) { + setDot('sd_dot_' + i, txt(doc, 'stromx' + i) === '0', 'on'); + } + } + + // ---- STROMZAEHLER -------------------------------------------------- + var powerHistory = []; + + function addPowerSample(v) { + if (!isFinite(v)) { return; } + var now = Date.now(); + powerHistory.push({ t: now, v: v }); + var cutoff = now - 5 * 60 * 1000; // 5 Minuten + while (powerHistory.length && powerHistory[0].t < cutoff) { + powerHistory.shift(); + } + } + + // Halbkreis-Tacho auf Canvas, -8000..+8000 W, Farbzonen wie Original-Gauge. + function drawGauge(value) { + var canvas = document.getElementById('v3_gauge'); + if (!canvas) { return; } + var ctx = canvas.getContext('2d'); + var w = canvas.width, h = canvas.height; + ctx.clearRect(0, 0, w, h); + + var cx = w / 2, cy = h - 18; + var r = Math.min(w / 2, h) - 16, lw = 14; + var MIN = -8000, MAX = 8000; + + function ang(v) { + v = Math.max(MIN, Math.min(MAX, v)); + return Math.PI + (v - MIN) / (MAX - MIN) * Math.PI; // pi .. 2pi + } + function zone(from, to, color) { + ctx.beginPath(); + ctx.lineWidth = lw; + ctx.strokeStyle = color; + ctx.arc(cx, cy, r, ang(from), ang(to)); + ctx.stroke(); + } + + zone(MIN, 0, '#2e7d32'); // gruen: Einspeisung / neutral + zone(0, 1000, '#f9a825'); // gelb + zone(1000, 5000, '#444444'); // neutraler Bereich + zone(5000, MAX, '#c62828'); // rot: hoher Bezug + + // Skalenstriche alle 2000 W + ctx.strokeStyle = 'rgba(255,255,255,0.7)'; + ctx.lineWidth = 1; + for (var t = MIN; t <= MAX; t += 2000) { + var a = ang(t); + ctx.beginPath(); + ctx.moveTo(cx + Math.cos(a) * (r - lw / 2 - 2), cy + Math.sin(a) * (r - lw / 2 - 2)); + ctx.lineTo(cx + Math.cos(a) * (r + lw / 2 + 2), cy + Math.sin(a) * (r + lw / 2 + 2)); + ctx.stroke(); + } + + // Zeiger + var v = isFinite(value) ? value : 0; + var na = ang(v); + ctx.strokeStyle = '#63d4ff'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.moveTo(cx, cy); + ctx.lineTo(cx + Math.cos(na) * (r - 4), cy + Math.sin(na) * (r - 4)); + ctx.stroke(); + ctx.fillStyle = '#63d4ff'; + ctx.beginPath(); + ctx.arc(cx, cy, 5, 0, Math.PI * 2); + ctx.fill(); + + // Digitalanzeige + Beschriftung + ctx.textAlign = 'center'; + ctx.fillStyle = '#00FF00'; + ctx.font = 'bold 20px Arial, sans-serif'; + ctx.fillText(Math.round(v) + ' W', cx, cy - 14); + ctx.fillStyle = 'rgba(234,240,255,0.6)'; + ctx.font = '10px Arial, sans-serif'; + ctx.fillText('-8000', cx - r, cy + 12); + ctx.fillText('+8000', cx + r, cy + 12); + ctx.fillText('Leistung', cx, 12); + } + + // Leistungsverlauf der letzten 5 Minuten auf Canvas. + function drawPowerChart() { + var canvas = document.getElementById('v3_power_chart'); + if (!canvas) { return; } + var ctx = canvas.getContext('2d'); + var w = canvas.width, h = canvas.height; + + ctx.clearRect(0, 0, w, h); + ctx.fillStyle = 'rgba(0,0,0,0.35)'; + ctx.fillRect(0, 0, w, h); + + if (!powerHistory.length) { + ctx.fillStyle = 'rgba(234,240,255,0.75)'; + ctx.font = '12px Segoe UI, Arial, sans-serif'; + ctx.fillText('Leistung (letzte 5 min) – warte auf Daten…', 12, 24); + return; + } + + var minV = powerHistory[0].v, maxV = powerHistory[0].v; + for (var i = 1; i < powerHistory.length; i++) { + var v = powerHistory[i].v; + if (v < minV) { minV = v; } + if (v > maxV) { maxV = v; } + } + if (minV === maxV) { minV -= 1; maxV += 1; } + + var padL = 38, padR = 10, padT = 18, padB = 22; + var plotW = w - padL - padR; + var plotH = h - padT - padB; + var t0 = powerHistory[0].t; + var t1 = powerHistory[powerHistory.length - 1].t; + if (t0 === t1) { t0 -= 1; t1 += 1; } + + // Gitter + ctx.strokeStyle = 'rgba(255,255,255,0.10)'; + ctx.lineWidth = 1; + for (var gy = 0; gy <= 4; gy++) { + var y = padT + (plotH * gy / 4); + ctx.beginPath(); + ctx.moveTo(padL, y); + ctx.lineTo(padL + plotW, y); + ctx.stroke(); + } + + // Beschriftung + ctx.fillStyle = 'rgba(234,240,255,0.75)'; + ctx.font = '12px Segoe UI, Arial, sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('Leistung – letzte 5 min', 12, 14); + ctx.fillStyle = 'rgba(234,240,255,0.65)'; + ctx.font = '11px Segoe UI, Arial, sans-serif'; + ctx.fillText(Math.round(maxV) + ' W', 6, padT + 10); + ctx.fillText(Math.round(minV) + ' W', 6, padT + plotH); + + // Linie + ctx.strokeStyle = 'rgba(99,212,255,0.95)'; + ctx.lineWidth = 2; + ctx.beginPath(); + for (var j = 0; j < powerHistory.length; j++) { + var p = powerHistory[j]; + var x = padL + ((p.t - t0) / (t1 - t0)) * plotW; + var yv = padT + (1 - ((p.v - minV) / (maxV - minV))) * plotH; + if (j === 0) { ctx.moveTo(x, yv); } else { ctx.lineTo(x, yv); } + } + ctx.stroke(); + + // letzter Punkt + var last = powerHistory[powerHistory.length - 1]; + var lx = padL + ((last.t - t0) / (t1 - t0)) * plotW; + var ly = padT + (1 - ((last.v - minV) / (maxV - minV))) * plotH; + ctx.fillStyle = 'rgba(124,255,178,0.95)'; + ctx.beginPath(); + ctx.arc(lx, ly, 3, 0, Math.PI * 2); + ctx.fill(); + } + + function num(s) { + var n = parseFloat(s); + return isNaN(n) ? null : n; + } + function diffText(a, b) { + if (a === null || b === null) { return '–'; } + return (a - b).toFixed(2) + ' kWh'; + } + + function handleStrom(doc) { + var e = []; + for (var i = 1; i <= 12; i++) { e[i] = num(txt(doc, 'stromx' + i)); } + + setText('sz_bezug', (e[1] === null ? '–' : e[1]) + ' kWh'); + setText('sz_einsp', (e[2] === null ? '–' : e[2]) + ' kWh'); + setText('sz_leist', (e[3] === null ? '–' : e[3]) + ' W'); + setText('sz_tag', (e[4] === null ? '–' : e[4]) + ' kWh'); + + // Differenzen aufeinanderfolgender Zaehlerstaende (-1 .. -8) + setText('sz_d1', diffText(e[1], e[5])); + setText('sz_d2', diffText(e[5], e[6])); + setText('sz_d3', diffText(e[6], e[7])); + setText('sz_d4', diffText(e[7], e[8])); + setText('sz_d5', diffText(e[8], e[9])); + setText('sz_d6', diffText(e[9], e[10])); + setText('sz_d7', diffText(e[10], e[11])); + setText('sz_d8', diffText(e[11], e[12])); + + var p = e[3]; + addPowerSample(p); + drawGauge(p === null ? 0 : p); + drawPowerChart(); + } + + // ---- UHR / SONNE / URLAUB ----------------------------------------- + function handleZeit(doc) { + setText('clock_time', pad(txt(doc, 'z_h')) + ':' + pad(txt(doc, 'z_mi')) + ':' + pad(txt(doc, 'z_s'))); + setText('clock_date', pad(txt(doc, 'z_d')) + '.' + pad(txt(doc, 'z_mo')) + '.' + (txt(doc, 'z_y') || '----')); + setText('sun_up', pad(txt(doc, 'z_auf_h')) + ':' + pad(txt(doc, 'z_auf_mi')) + ' Uhr (Offset ' + (txt(doc, 'z_auf_off') || '0') + ')'); + setText('sun_down', pad(txt(doc, 'z_unter_h')) + ':' + pad(txt(doc, 'z_unter_mi')) + ' Uhr (Offset ' + (txt(doc, 'z_unter_off') || '0') + ')'); + setDot('roll_urlaub_dot', txt(doc, 'z_urlaub') === '1', 'active'); + } + + // ---- Heizungs-Plausibilitaet (V2-Bug korrigiert) ------------------- + // Wird per onsubmit aufgerufen; verhindert SOLL-min >= SOLL-max. + // Min und Max liegen jetzt in GETRENNTEN Forms (eine Form schreibt nur + // ihren Wert). Plausibilitaet trotzdem ueber die ganze Zeile pruefen: + // neuer Wert vs. anderes SOLL-Feld bzw. aktuelle IST-Grenze. + window.checkHeizungV3 = function (form) { + var row = form.closest ? form.closest('.v3-heat-row') : null; + if (!row) { return true; } + var minEl = row.querySelector('input[data-soll="min"]'); + var maxEl = row.querySelector('input[data-soll="max"]'); + var sMin = minEl && minEl.value !== '' ? parseFloat(minEl.value) : null; + var sMax = maxEl && maxEl.value !== '' ? parseFloat(maxEl.value) : null; + // aktuelle Grenzen aus der IST-Anzeige ("min / max") + var istEl = row.querySelector('.v3-muted'); + var cur = istEl ? istEl.textContent.split('/') : []; + var effMin = sMin !== null ? sMin : parseFloat(cur[0]); + var effMax = sMax !== null ? sMax : parseFloat(cur[1]); + if (!isNaN(effMin) && !isNaN(effMax) && effMin >= effMax) { + alert('Min-Temperatur muss kleiner sein als Max.'); + return false; + } + return true; + }; + + // ---- Start --------------------------------------------------------- + function init() { + poll('licht_state.html', handleLicht, 800); + poll('heizung_state.html', handleHeizung, 1000); + poll('steckdosen_state.html', handleSteckdosen, 1000); + poll('stromzaehler_state.html', handleStrom, 800); + poll('zeit_state.html', handleZeit, 1000); + drawGauge(0); + drawPowerChart(); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/Script/tth_v3_api.js b/Script/tth_v3_api.js new file mode 100644 index 0000000..4e73290 --- /dev/null +++ b/Script/tth_v3_api.js @@ -0,0 +1,1034 @@ +// ==================================================================== +// tth_v3_api.js – indexV3.htm OHNE AWP, ueber die S7-1500 Web-API. +// +// - Automatische Anmeldung (Api.Login) mit hinterlegtem Benutzer -> Token. +// - Status zyklisch per Batch "PlcProgram.Read" (ein POST je Zyklus). +// - Schreiben per "PlcProgram.Write" direkt beim Klick (keine Forms/AWP). +// - Tacho + Leistungschart wie gehabt selbst auf Canvas (kein Internet). +// +// ANMELDUNG: Hat der Benutzer "Jeder" (anonym) auf der CPU das Recht +// "Variablen lesen/schreiben", ist KEIN Login noetig -> user leer lassen. +// Fuer einen benannten Benutzer stattdessen user/password setzen. +// ==================================================================== + +(function () { + 'use strict'; + + // ====== KONFIG ====== + var CFG = { + url: '/api/jsonrpc', // relativer Pfad -> selbe CPU/Origin + user: '111', // '' = anonym ueber "Jeder" (kein Login) + password: '1q2w3e$R', + readInterval: 500 // ms zwischen Status-Abfragen + }; + + var ANON = !CFG.user; // true => keine Anmeldung, kein Token + var token = null; + var rid = 0; + var statusEl = null; + + // ---- Anzeige Verbindungsstatus ------------------------------------ + function setStatus(connected, text) { + if (!statusEl) { statusEl = document.getElementById('api_status'); } + if (!statusEl) { return; } + statusEl.textContent = text; + statusEl.style.color = connected ? 'var(--v3-accent-2)' : 'var(--v3-danger)'; + } + + // ---- Low-Level: ein POST (Einzel- oder Batch-Request) ------------- + function doCall(batch) { + var headers = { 'Content-Type': 'application/json' }; + if (token) { headers['X-Auth-Token'] = token; } + return fetch(CFG.url, { method: 'POST', headers: headers, body: JSON.stringify(batch) }) + .then(function (r) { + if (r.status === 401 || r.status === 403) { token = null; throw new Error('unauthorized'); } + return r.json(); + }) + .then(function (j) { return Array.isArray(j) ? j : [j]; }); + } + + function login() { + token = null; + return doCall([{ jsonrpc: '2.0', id: ++rid, method: 'Api.Login', + params: { user: CFG.user, password: CFG.password } }]) + .then(function (arr) { + var res = arr[0]; + if (res && res.result && res.result.token) { + token = res.result.token; + return token; + } + throw new Error('Login fehlgeschlagen'); + }); + } + + // Anonym: direkt aufrufen. Sonst: Token sicherstellen, bei Auth-Fehler 1x neu anmelden. + function withAuth(fn) { + if (ANON) { return fn(); } + return (token ? Promise.resolve() : login()) + .then(fn) + .catch(function (e) { + // einmaliger Wiederanmelde-Versuch + return login().then(fn); + }); + } + + // ---- Mehrere Variablen lesen (chunked Batch) ---------------------- + function readMany(vars) { + return withAuth(function () { + var idMap = {}; + var reqs = vars.map(function (v) { + var id = ++rid; idMap[id] = v; + return { jsonrpc: '2.0', id: id, method: 'PlcProgram.Read', params: { var: v, mode: 'simple' } }; + }); + var chunks = []; + for (var i = 0; i < reqs.length; i += 24) { chunks.push(reqs.slice(i, i + 24)); } + return Promise.all(chunks.map(doCall)).then(function (res) { + var flat = [].concat.apply([], res); + var out = {}; + flat.forEach(function (r) { + if (r && r.id != null) { out[idMap[r.id]] = (r.error ? null : r.result); } + }); + return out; + }); + }); + } + + // ---- Eine Variable schreiben -------------------------------------- + function write(varName, value) { + varName = String(varName).trim(); + return withAuth(function () { + return doCall([{ jsonrpc: '2.0', id: ++rid, method: 'PlcProgram.Write', + params: { var: varName, value: value, mode: 'simple' } }]); + }).then(function (arr) { + var r = arr[0]; + if (r && r.error) { + console.warn('Write-Fehler', varName, + 'code=' + r.error.code, 'message=' + r.error.message, + JSON.stringify(r.error)); + } + return r; + }).catch(function (e) { console.warn('Write-Ausnahme', varName, e); }); + } + + // S7-String aus raw-Bytes dekodieren: [maxLen, curLen, char0, char1, ...] + function decodeS7String(raw) { + if (!Array.isArray(raw) || raw.length < 2) { return ''; } + var len = raw[1]; + var s = ''; + for (var i = 0; i < len && (2 + i) < raw.length; i++) { s += String.fromCharCode(raw[2 + i]); } + return s; + } + + // Wemos-String separat im raw-Modus lesen (simple liefert null wegen Steuerzeichen) + function readWemos() { + return withAuth(function () { + return doCall([{ jsonrpc: '2.0', id: ++rid, method: 'PlcProgram.Read', + params: { var: METER.wemos, mode: 'raw' } }]); + }).then(function (arr) { + var r = arr[0]; + var txt = (r && r.result) ? decodeS7String(r.result) : ''; + txt = txt.replace(/[\r\n\t]+/g, ' ').trim(); // Steuerzeichen fuer die Anzeige bereinigen + setText('sz_wemos', txt === '' ? '–' : txt); + }).catch(function () {}); + } + + // ====== Variablen-Zuordnung (Symbolik wie in den AWP-State-Dateien) = + + // Licht: [Status-Punkt-ID, Lese-Variable, Schreib-Variable] + var LIGHTS = [ + ['li_dot_1', '"Licht Küche"', '"Lichtmerker Küche"'], + ['li_dot_2', '"Licht Essen"', '"Lichtmerker Essen"'], + ['li_dot_3', '"Licht Wohnen 1"', '"Lichtmerker Wohnen 1"'], + ['li_dot_4', '"Licht Wohnen 2"', '"Lichtmerker Wohnen 2"'], + ['li_dot_5', '"Licht Arbeiten"', '"Lichtmerker Arbeiten"'], + ['li_dot_6', '"Licht HWR"', '"Lichtmerker HWR"'], + ['li_dot_7', '"Licht Niko"', '"Lichtmerker Niko"'], + ['li_dot_8', '"Licht Dusche"', '"Lichtmerker Dusche"'], + ['li_dot_9', '"Licht Abstellraum"', '"Lichtmerker Abstellraum"'], + ['li_dot_10', '"Licht Eltern"', '"Lichtmerker Eltern"'], + ['li_dot_11', '"Licht Noah"', '"Lichtmerker Noah"'], + ['li_dot_12', '"Licht Bad 2"', '"Lichtmerker Bad"'], + ['li_dot_13', '"Licht Kind 3"', '"Lichtmerker Kind 3"'], + ['li_dot_14', '"Licht Deele"', '"Kickertaste"'] + ]; + + // Heizung: {i, temp, active(heizt), min, max} + var HEAT = [ + { i: 1, temp: '"Temp Wohnen"', act: '"Heizung Essen Küche Wohn"', min: '"Einstellungen".Heizung."Heizung Wohnen min"', max: '"Einstellungen".Heizung."Heizung Wohnen max"' }, + { i: 2, temp: '"Temp Arbeiten"', act: '"Heizung Arbeiten"', min: '"Einstellungen".Heizung."Heizung Arbeiten min"', max: '"Einstellungen".Heizung."Heizung Arbeiten max"' }, + { i: 3, temp: '"Temp HWR"', act: '"Heizung HWR"', min: '"Einstellungen".Heizung."Heizung HWR min"', max: '"Einstellungen".Heizung."Heizung HWR max"' }, + { i: 4, temp: '"Temp Bad"', act: '"Heizung Bad"', min: '"Einstellungen".Heizung."Heizung Bad min"', max: '"Einstellungen".Heizung."Heizung Bad max"' }, + { i: 5, temp: '"Temp Eltern"', act: '"Heizung Eltern"', min: '"Einstellungen".Heizung."Heizung Eltern min"', max: '"Einstellungen".Heizung."Heizung Eltern max"' }, + { i: 6, temp: '"Temp Noah"', act: '"Heizung Noah"', min: '"Einstellungen".Heizung."Heizung Kind1 min"', max: '"Einstellungen".Heizung."Heizung Kind1 max"' }, + { i: 7, temp: '"Temp Mika"', act: '"Heizung Mika"', min: '"Einstellungen".Heizung."Heizung Kind2 min"', max: '"Einstellungen".Heizung."Heizung Kind2 max"' }, + { i: 8, temp: '"Temp Niko"', act: '"Heizung Niko"', min: '"Einstellungen".Heizung."Heizung Gäste min"', max: '"Einstellungen".Heizung."Heizung Gäste max"' }, + { i: 9, temp: '"Temp Dusche"', act: '"Heizung Gäste Dusche"', min: '"Einstellungen".Heizung."Heizung Dusche min"', max: '"Einstellungen".Heizung."Heizung Dusche max"' } + ]; + + // Steckdosen: [Punkt-ID, Status-Variable] (Status 0 = AN) + var SOCKETS = [ + ['sd_dot_1', '"Steckdose Deele Garder"'], + ['sd_dot_2', '"Steckdose Wohnen"'], + ['sd_dot_3', '"Steckdose Essen"'], + ['sd_dot_4', '"Steckdose Arbeiten"'], + ['sd_dot_5', '"Steckdose Kind 1"'], + ['sd_dot_6', '"Steckdose Kind 2"'], + ['sd_dot_7', '"Steckdose Gäste"'], + ['sd_dot_8', '"Steckdose Eltern"'] + ]; + + // Poolpumpe: Laufzeit-Zähler (Sekunden pro Tag) + SOLL-Laufzeit (Sek/Tag) + var POOL = { + sek: '"Funksteckdosen_DB"."Poolpumpenzähler Sek/Tag"', + max: '"Funksteckdosen_DB"."Poolpumpe min Laufzeit/Tag"' + }; + + var METER = { + bezug: '"Stromzähler".Stromzähler."Zählerstand Bezug"', + einsp: '"Stromzähler".Stromzähler."Zählerstand Einspeisung"', + leist: '"Stromzähler".Stromzähler."aktuelle Leistung"', + tag: '"Stromzähler".Stromzähler.Aufzeichnung."täglicher Bezug"', + wemos: '"Buffer"."Wemos RC".Senden', + hist: [ + '"Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[0]."Zählerstand Bezug"', + '"Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[1]."Zählerstand Bezug"', + '"Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[2]."Zählerstand Bezug"', + '"Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[3]."Zählerstand Bezug"', + '"Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[4]."Zählerstand Bezug"', + '"Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[5]."Zählerstand Bezug"', + '"Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[6]."Zählerstand Bezug"', + '"Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[7]."Zählerstand Bezug"' + ] + }; + + var WATER = { + stand: '"Wasserzähler".Zählerstand[0].aktueller_Stand', + seit: '"Wasserzähler".Zählerstand[0].seit_letzter_Ablesung', + wtemp: '"Wasserzähler".Zählerstand[0].Wassertemperatur', + tag: '"Wasserzähler"."täglicher Bezug"', + hist: [ + '"Wasserzähler".Zählerstand[0].aktueller_Stand', + '"Wasserzähler".Zählerstand[1].aktueller_Stand', + '"Wasserzähler".Zählerstand[2].aktueller_Stand', + '"Wasserzähler".Zählerstand[3].aktueller_Stand', + '"Wasserzähler".Zählerstand[4].aktueller_Stand', + '"Wasserzähler".Zählerstand[5].aktueller_Stand', + '"Wasserzähler".Zählerstand[6].aktueller_Stand', + '"Wasserzähler".Zählerstand[7].aktueller_Stand' + ] + }; + + var ZEIT = { + y: '"Aufgang_Untergang".aktuelle_CPU_Zeit.YEAR', + mo: '"Aufgang_Untergang".aktuelle_CPU_Zeit.MONTH', + d: '"Aufgang_Untergang".aktuelle_CPU_Zeit.DAY', + h: '"Aufgang_Untergang".aktuelle_CPU_Zeit.HOUR', + mi: '"Aufgang_Untergang".aktuelle_CPU_Zeit.MINUTE', + s: '"Aufgang_Untergang".aktuelle_CPU_Zeit.SECOND', + aufH: '"Aufgang_Untergang".Aufgang.HOUR', + aufMi: '"Aufgang_Untergang".Aufgang.MINUTE', + untH: '"Aufgang_Untergang".Untergang.HOUR', + untMi: '"Aufgang_Untergang".Untergang.MINUTE', + urlaub: '"Jalousie_DB".Urlaub' + }; + + // Liste aller zu lesenden Variablen zusammenbauen + function allReadVars() { + var v = []; + LIGHTS.forEach(function (l) { v.push(l[1]); }); + HEAT.forEach(function (h) { v.push(h.temp, h.act); }); // min/max separat (selten) + SOCKETS.forEach(function (s) { v.push(s[1]); }); + v.push(POOL.sek, POOL.max); + v.push(METER.bezug, METER.einsp, METER.leist, METER.tag); + METER.hist.forEach(function (x) { v.push(x); }); + v.push(WATER.stand, WATER.seit, WATER.wtemp, WATER.tag); + WATER.hist.forEach(function (x) { v.push(x); }); + Object.keys(ZEIT).forEach(function (k) { v.push(ZEIT[k]); }); + return v; + } + + // Heizungs-Grenzen (aendern sich fast nie) – separat, nur selten gelesen. + function settingsVars() { + var v = []; + HEAT.forEach(function (h) { v.push(h.min, h.max); }); + return v; + } + + // ====== Hilfen ====================================================== + function on(v) { return v === true || v === 1 || v === '1'; } + function num(v) { var n = parseFloat(v); return isNaN(n) ? null : n; } + function pad(n) { n = parseInt(n, 10); if (isNaN(n)) { return '--'; } return n < 10 ? '0' + n : '' + n; } + + function setText(id, value) { var el = document.getElementById(id); if (el) { el.textContent = value; } } + function setDot(id, isOn, kind) { + var el = document.getElementById(id); + if (!el) { return; } + el.classList.remove('v3-status-dot--on', 'v3-status-dot--off', 'v3-status-dot--active'); + el.classList.add(isOn ? (kind === 'active' ? 'v3-status-dot--active' : 'v3-status-dot--on') : 'v3-status-dot--off'); + } + function tempClass(t) { + if (t === null) { return 'v3-temp--normal'; } + if (t >= 24) { return 'v3-temp--hot'; } + if (t >= 22) { return 'v3-temp--warm'; } + return 'v3-temp--normal'; + } + + // Sekunden -> "X h Y min" (unter 1 Stunde nur "Y min") + function formatStdMin(sek) { + if (sek === null || !isFinite(sek)) { return '–'; } + var min = Math.round(sek / 60); + var h = Math.floor(min / 60); + var restMin = min % 60; + return (h > 0) ? (h + ' h ' + restMin + ' min') : (restMin + ' min'); + } + + // ====== Rendern ===================================================== + var powerHistory = []; + var gaugeTarget = 0, gaugeShown = 0; + var prevTemps = {}, arrowState = {}; + function addPowerSample(v) { + if (v === null || !isFinite(v)) { return; } + var now = Date.now(); + powerHistory.push({ t: now, v: v }); + var cutoff = now - 5 * 60 * 1000; + while (powerHistory.length && powerHistory[0].t < cutoff) { powerHistory.shift(); } + } + + function render(m) { + // Licht + LIGHTS.forEach(function (l) { setDot(l[0], on(m[l[1]]), 'on'); }); + + // Heizung + HEAT.forEach(function (h) { + var t = num(m[h.temp]); + var tEl = document.getElementById('he_temp_' + h.i); + if (tEl) { + if (t === null) { + tEl.textContent = '–'; + } else { + var p = prevTemps[h.i]; + if (p != null) { + if (t > p + 0.1) { arrowState[h.i] = ''; } + else if (t < p - 0.1) { arrowState[h.i] = ''; } + } + prevTemps[h.i] = t; + tEl.innerHTML = t.toFixed(1) + ' °C' + (arrowState[h.i] || ''); + } + tEl.className = 'v3-temp ' + tempClass(t); + } + setDot('he_dot_' + h.i, on(m[h.act]), 'active'); + var mn = num(m[h.min]), mx = num(m[h.max]); + setText('he_ist_' + h.i, (mn === null ? '–' : mn) + ' / ' + (mx === null ? '–' : mx)); + }); + + // Steckdosen (Status false/0 = AN, true/1 = AUS). Unbekannt/Lesefehler -> Punkt AUS. + SOCKETS.forEach(function (s) { + var val = m[s[1]]; + var known = (val !== null && val !== undefined); + setDot(s[0], known && (val === false || val === 0), 'on'); + }); + + // Poolpumpe: Laufzeit heute (Sekunden -> Stunden + Minuten) + var poolSek = num(m[POOL.sek]); + + // Fortschrittsbalken: IST vs. SOLL (beide in Sekunden) + var poolMax = num(m[POOL.max]); + var bar = document.getElementById('sd_pool_bar'); + var barLabel = document.getElementById('sd_pool_bar_label'); + if (bar && barLabel) { + if (poolSek === null || poolMax === null || poolMax <= 0) { + bar.style.width = '0%'; + barLabel.textContent = '– / –'; + bar.classList.remove('v3-pool-bar-fill--done'); + } else { + var pct = Math.min(100, poolSek / poolMax * 100); + bar.style.width = pct.toFixed(1) + '%'; + barLabel.textContent = formatStdMin(poolSek) + ' / ' + formatStdMin(poolMax); + bar.classList.toggle('v3-pool-bar-fill--done', pct >= 100); + } + } + + // Stromzähler + var bez = num(m[METER.bezug]), ein = num(m[METER.einsp]), lei = num(m[METER.leist]), tag = num(m[METER.tag]); + setText('sz_bezug', (bez === null ? '–' : bez.toFixed(1)) + ' kWh'); + setText('sz_einsp', (ein === null ? '–' : ein.toFixed(1)) + ' kWh'); + setText('sz_leist', (lei === null ? '–' : Math.round(lei)) + ' W'); + setText('sz_tag', (tag === null ? '–' : tag.toFixed(1)) + ' kWh'); + // Wemos-String wird separat im raw-Modus gelesen (siehe readWemos) + var hist = METER.hist.map(function (x) { return num(m[x]); }); + var seq = [bez].concat(hist); // [Bezug, h0..h7] + for (var d = 1; d <= 8; d++) { + var a = seq[d - 1], b = seq[d]; + setText('sz_d' + d, (a === null || b === null) ? '–' : (a - b).toFixed(2) + ' kWh'); + } + addPowerSample(lei); + gaugeTarget = (lei === null ? 0 : lei); // Balken laeuft in der Animation weich nach + + // Wasserzähler (nur aktuelle Werte aus [0]) + var wStand = num(m[WATER.stand]), wSeit = num(m[WATER.seit]); + var wTemp = num(m[WATER.wtemp]); + setText('hdr_wtemp', (wTemp === null ? '–' : 'Wasser: ' + wTemp.toFixed(0) + ' °C')); + setText('hdr_wstand', (wStand === null ? '–' : 'Stand: ' + wStand.toFixed(0) + ' L')); + setText('hdr_wseit', (wSeit === null ? '–' : 'seit Ablesung: ' + wSeit.toFixed(0) + ' L')); + + // Strom-Header-Werte + setText('hdr_sbezug', (bez === null ? '–' : 'Bezug: ' + bez.toFixed(1) + ' kWh')); + setText('hdr_seinsp', (ein === null ? '–' : 'Einsp.: ' + ein.toFixed(1) + ' kWh')); + + // Uhr / Sonne / Urlaub + setText('clock_time', pad(m[ZEIT.h]) + ':' + pad(m[ZEIT.mi]) + ':' + pad(m[ZEIT.s])); + setText('clock_date', pad(m[ZEIT.d]) + '.' + pad(m[ZEIT.mo]) + '.' + (m[ZEIT.y] != null ? m[ZEIT.y] : '----')); + setText('sun_up', pad(m[ZEIT.aufH]) + ':' + pad(m[ZEIT.aufMi]) + ' Uhr'); + setText('sun_down', pad(m[ZEIT.untH]) + ':' + pad(m[ZEIT.untMi]) + ' Uhr'); + setDot('roll_urlaub_dot', on(m[ZEIT.urlaub]), 'active'); + } + + // Canvas an CSS-Größe anpassen (scharf auf allen Displays) + function fitCanvas(canvas) { + var rect = canvas.getBoundingClientRect(); + var dpr = window.devicePixelRatio || 1; + var targetW = Math.round(rect.width * dpr); + var targetH = Math.round(rect.height * dpr); + if (canvas.width !== targetW || canvas.height !== targetH) { + canvas.width = targetW; + canvas.height = targetH; + } + var ctx = canvas.getContext('2d'); + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + return { w: rect.width, h: rect.height, ctx: ctx }; + } + + // ====== Strom-Tacho: horizontaler Balken von Mitte nach links/rechts ==== + function drawGauge(value) { + var canvas = document.getElementById('v3_gauge'); + if (!canvas) { return; } + var fit = fitCanvas(canvas); + var ctx = fit.ctx, w = fit.w, h = fit.h; + ctx.clearRect(0, 0, w, h); + + var MIN = -8000, MAX = 8000; + var v = Math.max(MIN, Math.min(MAX, isFinite(value) ? value : 0)); + var padT = 18, padB = 22, padL = 10, padR = 10; + var barY = h - padB - 10; + var barH = 14; + var cx = w / 2; + var halfW = (w - padL - padR) / 2; + + function xFor(vv) { + vv = Math.max(MIN, Math.min(MAX, vv)); + return cx + (vv / MAX) * halfW; + } + + // Hintergrundbalken (neutral) + ctx.fillStyle = 'rgba(255,255,255,0.08)'; + roundRect(ctx, padL, barY, w - padL - padR, barH, barH / 2); + ctx.fill(); + + // Aktiver Balken: von Mitte nach links (grün) oder rechts (rot) + var col = v < 0 ? '124,255,178' : '255,92,122'; + var xv = xFor(v); + ctx.save(); + ctx.fillStyle = 'rgba(' + col + ',0.95)'; + ctx.shadowColor = 'rgba(' + col + ',0.6)'; ctx.shadowBlur = 10; + if (v >= 0) { + roundRect(ctx, cx, barY, xv - cx, barH, barH / 2); + } else { + roundRect(ctx, xv, barY, cx - xv, barH, barH / 2); + } + ctx.fill(); + ctx.restore(); + + // Mittelstrich + ctx.strokeStyle = 'rgba(234,240,255,0.5)'; ctx.lineWidth = 1; + ctx.beginPath(); ctx.moveTo(cx, barY - 4); ctx.lineTo(cx, barY + barH + 4); ctx.stroke(); + + // Skalenstriche + ctx.fillStyle = 'rgba(234,240,255,0.55)'; ctx.font = '10px Segoe UI, Arial, sans-serif'; + ctx.textAlign = 'center'; + for (var t = -8000; t <= 8000; t += 2000) { + if (t === 0) { continue; } + var x = xFor(t); + ctx.beginPath(); ctx.moveTo(x, barY + barH + 3); ctx.lineTo(x, barY + barH + 7); ctx.stroke(); + ctx.fillText((t / 1000) + 'k', x, barY + barH + 18); + } + + // Digitalwert + Beschriftung + ctx.textAlign = 'center'; + ctx.fillStyle = 'rgba(' + col + ',1)'; ctx.font = 'bold 22px Segoe UI, Arial, sans-serif'; + ctx.fillText(Math.round(v) + ' W', cx, padT); + ctx.fillStyle = 'rgba(234,240,255,0.85)'; ctx.font = '12px Segoe UI, Arial, sans-serif'; + ctx.fillText('Leistung', cx, padT + 16); + } + + // Weiche Kurve (Catmull-Rom -> Bezier) + function smoothCurve(ctx, pts) { + if (pts.length < 2) { return; } + ctx.moveTo(pts[0].x, pts[0].y); + for (var i = 0; i < pts.length - 1; i++) { + var p0 = pts[i > 0 ? i - 1 : 0]; + var p1 = pts[i]; + var p2 = pts[i + 1]; + var p3 = pts[i + 2 < pts.length ? i + 2 : i + 1]; + var c1x = p1.x + (p2.x - p0.x) / 6, c1y = p1.y + (p2.y - p0.y) / 6; + var c2x = p2.x - (p3.x - p1.x) / 6, c2y = p2.y - (p3.y - p1.y) / 6; + ctx.bezierCurveTo(c1x, c1y, c2x, c2y, p2.x, p2.y); + } + } + + function roundRect(ctx, x, y, w, h, r) { + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.arcTo(x + w, y, x + w, y + h, r); + ctx.arcTo(x + w, y + h, x, y + h, r); + ctx.arcTo(x, y + h, x, y, r); + ctx.arcTo(x, y, x + w, y, r); + ctx.closePath(); + } + + function drawPowerChart() { + var canvas = document.getElementById('v3_power_chart'); + if (!canvas) { return; } + var fit = fitCanvas(canvas); + var ctx = fit.ctx, w = fit.w, h = fit.h; + var now = Date.now(); + ctx.clearRect(0, 0, w, h); + ctx.fillStyle = 'rgba(8,12,20,0.55)'; + ctx.fillRect(0, 0, w, h); + + var padL = 44, padR = 12, padT = 22, padB = 18; + var plotW = w - padL - padR, plotH = h - padT - padB; + + ctx.textAlign = 'left'; + ctx.fillStyle = 'rgba(234,240,255,0.85)'; + ctx.font = '12px Segoe UI, Arial, sans-serif'; + ctx.fillText('Leistung – letzte 5 min', 12, 14); + + if (!powerHistory.length) { + ctx.fillStyle = 'rgba(234,240,255,0.5)'; + ctx.fillText('warte auf Daten…', 12, padT + plotH / 2); + return; + } + + var WINDOW = 5 * 60 * 1000; + var t1 = now, t0 = now - WINDOW; + + // Y-Bereich inklusive 0 + var minV = 0, maxV = 0; + for (var i = 0; i < powerHistory.length; i++) { + var v = powerHistory[i].v; + if (v < minV) { minV = v; } + if (v > maxV) { maxV = v; } + } + if (minV === maxV) { minV -= 100; maxV += 100; } + var pad = (maxV - minV) * 0.12; + minV -= pad; maxV += pad; + + function X(t) { return padL + ((t - t0) / (t1 - t0)) * plotW; } + function Y(v) { return padT + (1 - (v - minV) / (maxV - minV)) * plotH; } + + // Gitter + ctx.strokeStyle = 'rgba(255,255,255,0.07)'; + ctx.lineWidth = 1; + for (var gy = 0; gy <= 4; gy++) { + var yy = padT + plotH * gy / 4; + ctx.beginPath(); ctx.moveTo(padL, yy); ctx.lineTo(padL + plotW, yy); ctx.stroke(); + } + for (var mm = 1; mm <= 5; mm++) { + var xx = X(now - mm * 60000); + ctx.beginPath(); ctx.moveTo(xx, padT); ctx.lineTo(xx, padT + plotH); ctx.stroke(); + } + + // Punkte im Fenster + bis "jetzt" verlaengern + var pts = []; + for (var k = 0; k < powerHistory.length; k++) { + var p = powerHistory[k]; + if (p.t < t0) { continue; } + pts.push({ x: X(p.t), y: Y(p.v), v: p.v }); + } + if (!pts.length) { return; } + var lastReal = pts[pts.length - 1]; + pts.push({ x: X(now), y: lastReal.y, v: lastReal.v }); + + var zeroY = Y(0); + var zeroFrac = Math.max(0, Math.min(1, (zeroY - padT) / plotH)); + + // Flaeche bis Nulllinie mit Verlauf (oben rot=Bezug, unten gruen=Einspeisung) + ctx.beginPath(); + smoothCurve(ctx, pts); + ctx.lineTo(pts[pts.length - 1].x, zeroY); + ctx.lineTo(pts[0].x, zeroY); + ctx.closePath(); + var grad = ctx.createLinearGradient(0, padT, 0, padT + plotH); + grad.addColorStop(0, 'rgba(255,92,122,0.38)'); + grad.addColorStop(Math.max(0, zeroFrac - 0.001), 'rgba(255,92,122,0.08)'); + grad.addColorStop(Math.min(1, zeroFrac + 0.001), 'rgba(124,255,178,0.08)'); + grad.addColorStop(1, 'rgba(124,255,178,0.38)'); + ctx.fillStyle = grad; + ctx.fill(); + + // Nulllinie + if (minV < 0 && maxV > 0) { + ctx.save(); + ctx.setLineDash([4, 4]); + ctx.strokeStyle = 'rgba(234,240,255,0.30)'; + ctx.lineWidth = 1; + ctx.beginPath(); ctx.moveTo(padL, zeroY); ctx.lineTo(padL + plotW, zeroY); ctx.stroke(); + ctx.restore(); + } + + // Linie mit Glow + ctx.save(); + ctx.beginPath(); + smoothCurve(ctx, pts); + ctx.strokeStyle = 'rgba(99,212,255,0.95)'; + ctx.lineWidth = 2.4; + ctx.lineJoin = 'round'; + ctx.shadowColor = 'rgba(99,212,255,0.55)'; + ctx.shadowBlur = 8; + ctx.stroke(); + ctx.restore(); + + // Y-Beschriftung + ctx.fillStyle = 'rgba(234,240,255,0.55)'; + ctx.font = '10px Segoe UI, Arial, sans-serif'; + ctx.textAlign = 'right'; + ctx.fillText(Math.round(maxV) + ' W', padL - 6, padT + 8); + ctx.fillText(Math.round(minV) + ' W', padL - 6, padT + plotH); + + // Live-Punkt (pulsierend), Farbe nach Vorzeichen + var lp = pts[pts.length - 1]; + var col = lp.v < 0 ? '124,255,178' : '255,92,122'; + var pulse = 0.5 + 0.5 * Math.sin(now / 320); + ctx.fillStyle = 'rgba(' + col + ',' + (0.16 + 0.14 * pulse) + ')'; + ctx.beginPath(); ctx.arc(lp.x, lp.y, 6 + 4 * pulse, 0, Math.PI * 2); ctx.fill(); + ctx.fillStyle = 'rgba(' + col + ',1)'; + ctx.beginPath(); ctx.arc(lp.x, lp.y, 3.2, 0, Math.PI * 2); ctx.fill(); + + // aktueller Wert als Pille oben rechts + var label = 'akt. Leistung: ' + Math.round(lp.v) + ' W'; + ctx.font = 'bold 11px Segoe UI, Arial, sans-serif'; + var tw = ctx.measureText(label).width; + var bw = tw + 14, bx = w - padR - bw, by = 4, bh = 17; + ctx.fillStyle = 'rgba(' + col + ',0.16)'; + roundRect(ctx, bx, by, bw, bh, 9); ctx.fill(); + ctx.fillStyle = 'rgba(' + col + ',1)'; + ctx.textAlign = 'left'; + ctx.fillText(label, bx + 7, by + 12); + } + + // ---- Tages-Chart: -1 .. -8 als Balken + täglicher Bezug als Linie ---- + function drawDailyChart() { + var canvas = document.getElementById('v3_daily_chart'); + if (!canvas) { return; } + var fit = fitCanvas(canvas); + var ctx = fit.ctx, w = fit.w, h = fit.h; + ctx.clearRect(0, 0, w, h); + ctx.fillStyle = 'rgba(8,12,20,0.55)'; ctx.fillRect(0, 0, w, h); + + var padL = 44, padR = 12, padT = 22, padB = 28; + var plotW = w - padL - padR, plotH = h - padT - padB; + + // Werte -1 .. -8 direkt aus den gelesenen Zählerständen berechnen + var bez = num(lastValues[METER.bezug]); + var hist = METER.hist.map(function (x) { return num(lastValues[x]); }); + var seq = [bez].concat(hist); // [Bezug, h0..h7] + var dailyValues = []; + for (var d = 1; d <= 8; d++) { + var a = seq[d - 1], b = seq[d]; + dailyValues.push((a === null || b === null) ? 0 : (a - b)); + } + // Zeitachse von links (alt) nach rechts (heute) – passt zu dayLabels + dailyValues.reverse(); + // täglicher Bezug + var tagVal = num(lastValues[METER.tag]) || 0; + + // Y-Bereich + var maxV = Math.max(tagVal, Math.max.apply(null, dailyValues)); + var minV = 0; + if (maxV < 1) { maxV = 10; } + var pd = (maxV - minV) * 0.12; + maxV += pd; + + function Y(v) { return padT + (1 - (v - minV) / (maxV - minV)) * plotH; } + function X(i) { return padL + (i + 0.5) * (plotW / 8); } + + // Wochentage für die letzten 8 Tage (rechts = heute) basierend auf CPU-Uhrzeit + var wochentage = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']; + var dayLabels = []; + var cpuY = lastValues[ZEIT.y], cpuM = lastValues[ZEIT.mo], cpuD = lastValues[ZEIT.d]; + var heute = (cpuY != null && cpuM != null && cpuD != null) + ? new Date(cpuY, cpuM - 1, cpuD) + : new Date(); + for (var i = 0; i < 8; i++) { + var d = new Date(heute.getFullYear(), heute.getMonth(), heute.getDate() - (7 - i)); + dayLabels.push(wochentage[d.getDay()]); + } + + // Titel + ctx.textAlign = 'left'; ctx.fillStyle = 'rgba(234,240,255,0.85)'; + ctx.font = '12px Segoe UI, Arial, sans-serif'; + ctx.fillText('Tagesbezug – letzte 8 Tage', 12, 14); + + // Gitter horizontal + ctx.strokeStyle = 'rgba(255,255,255,0.07)'; ctx.lineWidth = 1; + for (var gy = 0; gy <= 4; gy++) { + var yy = padT + plotH * gy / 4; + ctx.beginPath(); ctx.moveTo(padL, yy); ctx.lineTo(padL + plotW, yy); ctx.stroke(); + } + + // Balken für -8 .. -1 + var barW = plotW / 8 * 0.55; + for (var i = 0; i < 8; i++) { + var x = X(i); + var y0 = Y(0); + var y1 = Y(dailyValues[i]); + var bh = y0 - y1; + var bx = x - barW / 2; + var isToday = (i === 7); // -1 = heute + var grad = ctx.createLinearGradient(0, y1, 0, y0); + if (isToday) { + grad.addColorStop(0, 'rgba(255,217,99,0.95)'); + grad.addColorStop(1, 'rgba(255,217,99,0.25)'); + } else { + grad.addColorStop(0, 'rgba(124,255,178,0.95)'); + grad.addColorStop(1, 'rgba(124,255,178,0.25)'); + } + ctx.fillStyle = grad; + roundRect(ctx, bx, y1, barW, bh, 4); + ctx.fill(); + // Wert über dem Balken + ctx.fillStyle = isToday ? 'rgba(255,217,99,0.95)' : 'rgba(234,240,255,0.85)'; + ctx.font = 'bold 10px Segoe UI, Arial, sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(dailyValues[i].toFixed(2), x, y1 - 5); + // X-Beschriftung: Wochentag + ctx.fillStyle = isToday ? 'rgba(255,217,99,0.95)' : 'rgba(234,240,255,0.65)'; + ctx.font = 'bold 10px Segoe UI, Arial, sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(dayLabels[i], x, h - 8); + } + + // Linie für täglichen Bezug + var tagY = Y(tagVal); + ctx.save(); + ctx.strokeStyle = 'rgba(99,212,255,0.95)'; ctx.lineWidth = 2.5; + ctx.shadowColor = 'rgba(99,212,255,0.55)'; ctx.shadowBlur = 8; + ctx.beginPath(); ctx.moveTo(padL, tagY); ctx.lineTo(padL + plotW, tagY); ctx.stroke(); + ctx.restore(); + + // Y-Beschriftung + ctx.fillStyle = 'rgba(234,240,255,0.55)'; ctx.font = '10px Segoe UI, Arial, sans-serif'; + ctx.textAlign = 'right'; + ctx.fillText(maxV.toFixed(1) + ' kWh', padL - 6, padT + 8); + ctx.fillText(minV.toFixed(1) + ' kWh', padL - 6, padT + plotH); + + // Legende / täglicher Bezug als Pille + var label = 'täglicher Bezug: ' + tagVal.toFixed(2) + ' kWh'; + ctx.font = 'bold 11px Segoe UI, Arial, sans-serif'; + var tw = ctx.measureText(label).width, bw = tw + 14, bx = w - padR - bw, by = 4, bh = 17; + ctx.fillStyle = 'rgba(99,212,255,0.16)'; roundRect(ctx, bx, by, bw, bh, 9); ctx.fill(); + ctx.fillStyle = 'rgba(99,212,255,1)'; ctx.textAlign = 'left'; + ctx.fillText(label, bx + 7, by + 12); + } + + // ---- Wasser-Tageschart: -1 .. -8 als Balken + Durchschnittslinie ---- + function drawWaterChart() { + var canvas = document.getElementById('v3_water_chart'); + if (!canvas) { return; } + var fit = fitCanvas(canvas); + var ctx = fit.ctx, w = fit.w, h = fit.h; + ctx.clearRect(0, 0, w, h); + ctx.fillStyle = 'rgba(8,12,20,0.55)'; ctx.fillRect(0, 0, w, h); + + var padL = 50, padR = 16, padT = 22, padB = 28; + var plotW = w - padL - padR, plotH = h - padT - padB; + + // Werte -1 .. -8 aus den historischen Zählerständen berechnen + var hist = WATER.hist.map(function (x) { return num(lastValues[x]); }); + var dailyValues = []; + for (var d = 1; d < hist.length; d++) { + var a = hist[d - 1], b = hist[d]; + dailyValues.push((a === null || b === null) ? 0 : (a - b)); + } + // Zeitachse von links (alt) nach rechts (heute) + dailyValues.reverse(); + + // Y-Bereich + var maxV = Math.max.apply(null, dailyValues); + var minV = 0; + // täglicher Bezug aus DB + var tagVal = num(lastValues[WATER.tag]) || 0; + if (tagVal > maxV) { maxV = tagVal; } + if (maxV < 1) { maxV = 100; } + var pd = (maxV - minV) * 0.12; + maxV += pd; + + function Y(v) { return padT + (1 - (v - minV) / (maxV - minV)) * plotH; } + function X(i) { return padL + (i + 0.5) * (plotW / dailyValues.length); } + + // Wochentage für die letzten Tage (rechts = heute) + var wochentage = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']; + var dayLabels = []; + var cpuY = lastValues[ZEIT.y], cpuM = lastValues[ZEIT.mo], cpuD = lastValues[ZEIT.d]; + var heute = (cpuY != null && cpuM != null && cpuD != null) + ? new Date(cpuY, cpuM - 1, cpuD) + : new Date(); + for (var i = 0; i < dailyValues.length; i++) { + var d = new Date(heute.getFullYear(), heute.getMonth(), heute.getDate() - (dailyValues.length - 1 - i)); + dayLabels.push(wochentage[d.getDay()]); + } + + // Titel + ctx.textAlign = 'left'; ctx.fillStyle = 'rgba(234,240,255,0.85)'; + ctx.font = '12px Segoe UI, Arial, sans-serif'; + ctx.fillText('Wasserverbrauch – letzte 7 Tage', 12, 14); + + // Gitter horizontal + ctx.strokeStyle = 'rgba(255,255,255,0.07)'; ctx.lineWidth = 1; + for (var gy = 0; gy <= 4; gy++) { + var yy = padT + plotH * gy / 4; + ctx.beginPath(); ctx.moveTo(padL, yy); ctx.lineTo(padL + plotW, yy); ctx.stroke(); + } + + // Balken + var barW = plotW / dailyValues.length * 0.55; + for (var i = 0; i < dailyValues.length; i++) { + var x = X(i); + var y0 = Y(0); + var y1 = Y(dailyValues[i]); + var bh = y0 - y1; + var bx = x - barW / 2; + var isToday = (i === dailyValues.length - 1); + var grad = ctx.createLinearGradient(0, y1, 0, y0); + if (isToday) { + grad.addColorStop(0, 'rgba(99,212,255,0.95)'); + grad.addColorStop(1, 'rgba(99,212,255,0.25)'); + } else { + grad.addColorStop(0, 'rgba(99,212,255,0.75)'); + grad.addColorStop(1, 'rgba(99,212,255,0.15)'); + } + ctx.fillStyle = grad; + roundRect(ctx, bx, y1, barW, bh, 4); + ctx.fill(); + // Wert über dem Balken + ctx.fillStyle = isToday ? 'rgba(99,212,255,0.95)' : 'rgba(234,240,255,0.85)'; + ctx.font = 'bold 10px Segoe UI, Arial, sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(Math.round(dailyValues[i]), x, y1 - 5); + // X-Beschriftung: Wochentag + ctx.fillStyle = isToday ? 'rgba(99,212,255,0.95)' : 'rgba(234,240,255,0.65)'; + ctx.font = 'bold 10px Segoe UI, Arial, sans-serif'; + ctx.textAlign = 'center'; + ctx.fillText(dayLabels[i], x, h - 8); + } + + // Linie für täglichen Bezug + var tagY = Y(tagVal); + ctx.save(); + ctx.strokeStyle = 'rgba(255,217,99,0.95)'; ctx.lineWidth = 2.5; + ctx.setLineDash([6, 4]); + ctx.shadowColor = 'rgba(255,217,99,0.55)'; ctx.shadowBlur = 8; + ctx.beginPath(); ctx.moveTo(padL, tagY); ctx.lineTo(padL + plotW, tagY); ctx.stroke(); + ctx.restore(); + + // Y-Beschriftung + ctx.fillStyle = 'rgba(234,240,255,0.55)'; ctx.font = '10px Segoe UI, Arial, sans-serif'; + ctx.textAlign = 'right'; + ctx.fillText(Math.round(maxV) + ' L', padL - 6, padT + 8); + ctx.fillText(Math.round(minV) + ' L', padL - 6, padT + plotH); + + // Legende / täglicher Bezug als Pille + var label = 'täglicher Bezug: ' + Math.round(tagVal) + ' L'; + ctx.font = 'bold 11px Segoe UI, Arial, sans-serif'; + var tw = ctx.measureText(label).width, bw = tw + 14, bx = w - padR - bw, by = 4, bh = 17; + ctx.fillStyle = 'rgba(255,217,99,0.16)'; roundRect(ctx, bx, by, bw, bh, 9); ctx.fill(); + ctx.fillStyle = 'rgba(255,217,99,1)'; ctx.textAlign = 'left'; + ctx.fillText(label, bx + 7, by + 12); + } + + // Durchgehende Animation (pulsierender Punkt, weiches Scrollen der Zeitachse) + function startChartAnimation() { + var last = 0; + function frame(ts) { + requestAnimationFrame(frame); + if (ts - last < 33) { return; } // ~30 fps (schont CPU) + last = ts; + gaugeShown += (gaugeTarget - gaugeShown) * 0.18; // weiches Nachlaufen + if (Math.abs(gaugeTarget - gaugeShown) < 0.5) { gaugeShown = gaugeTarget; } + drawGauge(gaugeShown); + drawPowerChart(); + drawDailyChart(); + drawWaterChart(); + } + requestAnimationFrame(frame); + } + + // ====== Schreiben per Klick (Event-Delegation) ===================== + function parseVal(s) { + if (s === 'true') { return true; } + if (s === 'false') { return false; } + if (/^-?\d+(\.\d+)?$/.test(s)) { return Number(s); } + return s; + } + + // kurzes visuelles Feedback am Button (gruen=ok, rot=Fehler) + function flash(btn, r) { + if (!btn) { return; } + var cls = (r && !r.error) ? 'v3-flash-ok' : 'v3-flash-err'; + btn.classList.remove('v3-flash-ok', 'v3-flash-err'); + void btn.offsetWidth; // Reflow -> Animation neu starten + btn.classList.add(cls); + setTimeout(function () { btn.classList.remove(cls); }, 600); + } + + function onClick(ev) { + var btn = ev.target.closest ? ev.target.closest('button') : null; + // Mehrfach-Schreibvorgang (kommagetrennte Variablen in data-vars) + var ml = ev.target.closest ? ev.target.closest('[data-vars]') : null; + if (ml && ml.hasAttribute('data-value')) { + var vars = ml.getAttribute('data-vars').split(',').map(function (s) { return s.trim(); }); + var val = parseVal(ml.getAttribute('data-value')); + var results = []; + Promise.all(vars.map(function (v) { + return write(v, val).then(function (r) { results.push(r); return r; }); + })).then(function () { + var allOk = results.every(function (r) { return r && !r.error; }); + flash(btn, allOk ? { error: false } : { error: true }); + }); + return; + } + // einfacher Einzel-Schreibvorgang + var el = ev.target.closest ? ev.target.closest('[data-var]') : null; + if (el && el.hasAttribute('data-value')) { + write(el.getAttribute('data-var'), parseVal(el.getAttribute('data-value'))).then(function (r) { flash(btn, r); }); + return; + } + // Poolpumpe: SOLL-Laufzeit (Eingabe in Minuten -> Sekunden schreiben) + // WICHTIG: vor [data-write-input] pruefen, da der Pool-Button auch + // data-write-input traegt und sonst vom Heizungs-Handler abgefangen wird. + var pm = ev.target.closest ? ev.target.closest('[data-pool-max]') : null; + if (pm) { + var pInput = document.getElementById(pm.getAttribute('data-write-input')); + if (!pInput || pInput.value === '') { return; } + var pMin = parseFloat(pInput.value); + if (isNaN(pMin) || pMin < 0) { alert('Bitte eine gültige Zahl eingeben.'); return; } + write(pm.getAttribute('data-var'), Math.round(pMin * 60)).then(function (r) { + flash(btn, r); + // SOLL-Wert neu lesen, damit der Balken sofort stimmt + readMany([POOL.max]).then(function (m) { merge(m); render(lastValues); }).catch(function () {}); + }); + return; + } + // Heizung: Zahl aus zugehoerigem Eingabefeld schreiben + var hn = ev.target.closest ? ev.target.closest('[data-write-input]') : null; + if (hn) { + var input = document.getElementById(hn.getAttribute('data-write-input')); + if (!input || input.value === '') { return; } + var n = parseFloat(input.value); + if (isNaN(n)) { alert('Bitte eine Zahl eingeben.'); return; } + if (!heatPlausible(hn, n)) { return; } + write(hn.getAttribute('data-var'), n).then(function (r) { flash(btn, r); setTimeout(refreshSettings, 300); }); + return; + } + // WLED: Helligkeit bzw. Farbe + Befehl + var wl = ev.target.closest ? ev.target.closest('[data-wled]') : null; + if (wl) { + var kind = wl.getAttribute('data-wled'); + if (kind === 'bright') { + var b = document.getElementById('wled_bright'); + write('"wled_DB".Helligkeit', parseInt(b.value, 10)).then(function () { return write('"wled_DB".Befehl', 4); }).then(function (r) { flash(btn, r); }); + } else if (kind === 'color') { + var c = document.getElementById('wled_color'); + write('"wled_DB".RGB_hex', c.value).then(function () { return write('"wled_DB".Befehl', 5); }).then(function (r) { flash(btn, r); }); + } + return; + } + } + + // Plausibilitaet min < max ueber die ganze Heizungszeile + function heatPlausible(btn, newVal) { + var row = btn.closest('.v3-heat-row'); + if (!row) { return true; } + var kind = btn.getAttribute('data-kind'); + var minEl = row.querySelector('input[data-kind="min"]'); + var maxEl = row.querySelector('input[data-kind="max"]'); + var ist = row.querySelector('.v3-muted'); + var cur = ist ? ist.textContent.split('/') : []; + var sMin = (kind === 'min') ? newVal : (minEl && minEl.value !== '' ? parseFloat(minEl.value) : parseFloat(cur[0])); + var sMax = (kind === 'max') ? newVal : (maxEl && maxEl.value !== '' ? parseFloat(maxEl.value) : parseFloat(cur[1])); + if (!isNaN(sMin) && !isNaN(sMax) && sMin >= sMax) { + alert('Min-Temperatur muss kleiner sein als Max.'); + return false; + } + return true; + } + + // ====== Start ======================================================= + // Last reduzieren: jeden Zyklus nur die schnelle Variable (Leistung) lesen, + // alles Uebrige nur jeden SLOW_EVERY-ten Zyklus. Heizungs-Grenzen nur beim + // Start bzw. nach einem Heizungs-Schreibvorgang. Werte werden gepuffert. + var FAST_VARS = [METER.leist]; + var ALL_VARS = null; + var SET_VARS = null; + var SLOW_EVERY = 4; + var lastValues = {}; + var tickCount = 0; + var diagnosed = false; + + function merge(m) { Object.keys(m).forEach(function (k) { lastValues[k] = m[k]; }); } + + // Einmalige Diagnose: welche Variablen liefern keinen Wert (Tippfehler/Rechte)? + function diagnose(m, liste) { + var bad = liste.filter(function (v) { return m[v] === null || m[v] === undefined; }); + if (bad.length === liste.length) { + console.warn('Web-API: KEINE der ' + liste.length + ' Variablen gelesen – Rechte/Web-API pruefen.'); + } else if (bad.length) { + console.warn('Web-API: ' + bad.length + ' Variable(n) ohne Wert (Tippfehler im Namen?):', bad); + } else { + console.info('Web-API: alle ' + liste.length + ' Variablen ok.'); + } + } + + // Heizungs-Grenzen separat lesen (selten / nach Schreibvorgang) + function refreshSettings() { + readMany(SET_VARS).then(function (m) { merge(m); render(lastValues); }).catch(function () {}); + } + + function tick() { + tickCount++; + var isFull = (tickCount % SLOW_EVERY === 1); + var vars = isFull ? ALL_VARS : FAST_VARS; + readMany(vars) + .then(function (m) { + merge(m); + render(lastValues); + setStatus(true, 'verbunden'); + if (isFull) { readWemos(); } + if (isFull && !diagnosed) { diagnosed = true; diagnose(m, ALL_VARS); } + }) + .catch(function (e) { setStatus(false, 'keine Verbindung'); }) + .then(function () { setTimeout(tick, CFG.readInterval); }); + } + + function init() { + ALL_VARS = allReadVars(); + SET_VARS = settingsVars(); + document.addEventListener('click', onClick); + drawGauge(0); + startChartAnimation(); + setStatus(false, 'verbinde…'); + if (ANON) { + refreshSettings(); + } else { + login().then(function () { setStatus(true, 'verbunden'); refreshSettings(); }) + .catch(function () { setStatus(false, 'Login fehlgeschlagen'); }); + } + tick(); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/Script/tth_v4.js b/Script/tth_v4.js new file mode 100644 index 0000000..de184d5 --- /dev/null +++ b/Script/tth_v4.js @@ -0,0 +1,1320 @@ +// ==================================================================== +// tth_v4.js – indexV4.htm "HAUS-BRÜCKE" Kommandozentrale +// +// - Basiert auf der V3-Web-API-Logik (tth_v3_api.js) +// - Futuristisches HUD-Design, Boot-Sequence, Toasts, Touch-Tiles +// - Kommunikation mit der S7-1500 ueber /api/jsonrpc +// ==================================================================== + +(function () { + 'use strict'; + + // Preview-Modus: window.V4_PREVIEW = true in indexV4_preview.htm gesetzt + var PREVIEW = !!(window.V4_PREVIEW); + + // ====== KONFIG ====== + var CFG = { + url: '/api/jsonrpc', + user: '111', + password: '1q2w3e$R', + readInterval: 500 + }; + + var ANON = !CFG.user; + var token = null; + var rid = 0; + + // ====== Boot-Sequence ====== + var bootEls = { + sps: document.getElementById('boot_sps'), + energie: document.getElementById('boot_energie'), + kommando: document.getElementById('boot_kommando'), + status: document.getElementById('boot_status'), + bar: document.getElementById('boot_bar'), + done: document.getElementById('boot_done'), + wrap: document.getElementById('v4_boot') + }; + + function bootSet(el, text, ok) { + if (!el) { return; } + el.textContent = text; + el.classList.toggle('v4-boot-ok', !!ok); + } + + function runBoot() { + var steps = [ + { el: bootEls.sps, label: 'OK' }, + { el: bootEls.energie, label: 'OK' }, + { el: bootEls.kommando, label: 'OK' }, + { el: bootEls.status, label: 'OK' } + ]; + var i = 0; + function next() { + if (i < steps.length) { + var s = steps[i]; + bootSet(s.el, s.label, true); + bootEls.bar.style.width = ((i + 1) / steps.length * 100) + '%'; + i++; + setTimeout(next, 350); + } else { + bootEls.bar.style.width = '100%'; + bootEls.done.classList.add('v4-boot-done--show'); + setTimeout(function () { + bootEls.wrap.classList.add('v4-boot--hidden'); + setTimeout(function () { bootEls.wrap.style.display = 'none'; }, 600); + }, 600); + } + } + next(); + } + + // ====== Status ====== + var statusOrb = document.getElementById('api_status_orb'); + var statusText = document.getElementById('api_status'); + function setStatus(connected, text) { + if (statusText) { statusText.textContent = text; } + if (statusOrb) { statusOrb.classList.toggle('v4-status-orb--on', connected); } + } + + // ====== Toasts ====== + var toastWrap = document.getElementById('v4_toasts'); + function toast(msg, kind) { + if (!toastWrap) { return; } + var el = document.createElement('div'); + el.className = 'v4-toast' + (kind === 'ok' ? ' v4-toast--ok' : kind === 'err' ? ' v4-toast--err' : ''); + el.textContent = msg; + toastWrap.appendChild(el); + setTimeout(function () { + el.classList.add('v4-toast--out'); + setTimeout(function () { el.remove(); }, 300); + }, 2500); + } + + // ====== Preview: Dummy-Daten ====== + function makePreviewValues() { + var m = {}; + var now = new Date(); + // Uhr + m[ZEIT.y] = now.getFullYear(); + m[ZEIT.mo] = now.getMonth() + 1; + m[ZEIT.d] = now.getDate(); + m[ZEIT.h] = now.getHours(); + m[ZEIT.mi] = now.getMinutes(); + m[ZEIT.s] = now.getSeconds(); + m[ZEIT.aufH] = 6; m[ZEIT.aufMi] = 12; + m[ZEIT.untH] = 20; m[ZEIT.untMi] = 48; + m[ZEIT.urlaub] = false; + // Strom + m[METER.bezug] = 12543.2; + m[METER.einsp] = 8421.7; + m[METER.leist] = 300 + Math.sin(Date.now() / 2000) * 1500; + m[METER.tag] = 18.4; + var base = 12543.2; + for (var i = 0; i < 8; i++) { + base -= 15 + i * 2.3; + m[METER.hist[i]] = base; + } + // Wasser + m[WATER.stand] = 247300; + m[WATER.seit] = 120; + m[WATER.wtemp] = 18; + m[WATER.tag] = 160; + base = 247300; + for (var j = 0; j < 8; j++) { + base -= 130 + j * 20; + m[WATER.hist[j]] = base; + } + // Licht: jeder 2. an + LIGHTS.forEach(function (l, idx) { m[l[1]] = (idx % 3 === 0); }); + // Heizung + HEAT.forEach(function (h) { + m[h.temp] = 19 + (h.i % 4) * 0.8; + m[h.act] = (h.i % 3 === 0); + m[h.min] = 19; + m[h.max] = 23; + }); + // Steckdosen: false = AN + SOCKETS.forEach(function (s, idx) { m[s[1]] = (idx % 2 === 0) ? false : true; }); + // Pool + m[POOL.sek] = 4 * 3600 + 12 * 60; + m[POOL.max] = 5 * 3600; + m[METER.wemos] = ''; + return m; + } + + // ====== Robustheit: Backoff, Mutex, Token-Reset ====== + var pollBusy = false; + var backoffMs = CFG.readInterval; + var MAX_BACKOFF = 10000; + var wemosCounter = 0; + + function resetBackoff() { backoffMs = CFG.readInterval; } + function increaseBackoff() { backoffMs = Math.min(MAX_BACKOFF, backoffMs * 2); } + + // ====== Low-Level API ====== + function doCall(batch) { + var headers = { 'Content-Type': 'application/json' }; + if (token) { headers['X-Auth-Token'] = token; } + return fetch(CFG.url, { method: 'POST', headers: headers, body: JSON.stringify(batch) }) + .catch(function (e) { token = null; throw e; }) // Netzwerkfehler -> Token verwerfen + .then(function (r) { + if (r.status === 401 || r.status === 403) { token = null; throw new Error('unauthorized'); } + return r.json().catch(function () { throw new Error('leere Antwort'); }); + }) + .then(function (j) { return Array.isArray(j) ? j : [j]; }); + } + + function login() { + token = null; + return doCall([{ jsonrpc: '2.0', id: ++rid, method: 'Api.Login', + params: { user: CFG.user, password: CFG.password } }]) + .then(function (arr) { + var res = arr[0]; + if (res && res.result && res.result.token) { + token = res.result.token; + return token; + } + throw new Error('Login fehlgeschlagen'); + }); + } + + function withAuth(fn) { + if (ANON) { return fn(); } + return (token ? Promise.resolve() : login()) + .then(fn) + .catch(function () { return login().then(fn); }); + } + + function readMany(vars) { + if (PREVIEW) { + var pv = makePreviewValues(); + var out = {}; + vars.forEach(function (v) { out[v] = pv[v] != null ? pv[v] : null; }); + return Promise.resolve(out); + } + return withAuth(function () { + var idMap = {}; + var reqs = vars.map(function (v) { + var id = ++rid; idMap[id] = v; + return { jsonrpc: '2.0', id: id, method: 'PlcProgram.Read', params: { var: v, mode: 'simple' } }; + }); + var chunks = []; + for (var i = 0; i < reqs.length; i += 24) { chunks.push(reqs.slice(i, i + 24)); } + return Promise.all(chunks.map(doCall)).then(function (res) { + var flat = [].concat.apply([], res); + var out = {}; + flat.forEach(function (r) { + if (r && r.id != null) { out[idMap[r.id]] = (r.error ? null : r.result); } + }); + return out; + }); + }); + } + + function write(varName, value) { + varName = String(varName).trim(); + if (PREVIEW) { + // Schreibvorgang simulieren: Zielvariable im Dummy-Speicher togglen + var key = varName; + var cur = lastValues[key]; + if (typeof value === 'boolean' && typeof cur === 'boolean') { + lastValues[key] = value; + } + render(lastValues); + return Promise.resolve({ jsonrpc: '2.0', id: 0, result: true }); + } + return withAuth(function () { + return doCall([{ jsonrpc: '2.0', id: ++rid, method: 'PlcProgram.Write', + params: { var: varName, value: value, mode: 'simple' } }]); + }).then(function (arr) { + var r = arr[0]; + if (r && r.error) { + console.warn('Write-Fehler', varName, r.error); + } + return r; + }).catch(function (e) { console.warn('Write-Ausnahme', varName, e); }); + } + + function decodeS7String(raw) { + if (!Array.isArray(raw) || raw.length < 2) { return ''; } + var len = raw[1]; + var s = ''; + for (var i = 0; i < len && (2 + i) < raw.length; i++) { s += String.fromCharCode(raw[2 + i]); } + return s; + } + + function readWemos() { + if (PREVIEW) { + setText('sz_wemos', 'PREVIEW MODUS – keine SPS'); + return Promise.resolve(); + } + return withAuth(function () { + return doCall([{ jsonrpc: '2.0', id: ++rid, method: 'PlcProgram.Read', + params: { var: METER.wemos, mode: 'raw' } }]); + }).then(function (arr) { + var r = arr[0]; + var txt = (r && r.result) ? decodeS7String(r.result) : ''; + txt = txt.replace(/[\r\n\t]+/g, ' ').trim(); + setText('sz_wemos', txt === '' ? '–' : txt); + }).catch(function () {}); + } + + // ====== Variablen-Zuordnung ====== + var LIGHTS = [ + ['Küche', '"Licht Küche"', '"Lichtmerker Küche"'], + ['Esszimmer', '"Licht Essen"', '"Lichtmerker Essen"'], + ['Wohnen 1', '"Licht Wohnen 1"', '"Lichtmerker Wohnen 1"'], + ['Wohnen 2', '"Licht Wohnen 2"', '"Lichtmerker Wohnen 2"'], + ['Arbeiten', '"Licht Arbeiten"', '"Lichtmerker Arbeiten"'], + ['HWR', '"Licht HWR"', '"Lichtmerker HWR"'], + ['Niko', '"Licht Niko"', '"Lichtmerker Niko"'], + ['Dusche', '"Licht Dusche"', '"Lichtmerker Dusche"'], + ['Abstellraum', '"Licht Abstellraum"', '"Lichtmerker Abstellraum"'], + ['Eltern', '"Licht Eltern"', '"Lichtmerker Eltern"'], + ['Noah', '"Licht Noah"', '"Lichtmerker Noah"'], + ['Bad', '"Licht Bad 2"', '"Lichtmerker Bad"'], + ['Mika', '"Licht Kind 3"', '"Lichtmerker Kind 3"'], + ['Kicker', '"Licht Deele"', '"Kickertaste"'] + ]; + + var HEAT = [ + { i: 1, name: 'Wohnzimmer', temp: '"Temp Wohnen"', act: '"Heizung Essen Küche Wohn"', min: '"Einstellungen".Heizung."Heizung Wohnen min"', max: '"Einstellungen".Heizung."Heizung Wohnen max"' }, + { i: 2, name: 'Arbeiten', temp: '"Temp Arbeiten"', act: '"Heizung Arbeiten"', min: '"Einstellungen".Heizung."Heizung Arbeiten min"', max: '"Einstellungen".Heizung."Heizung Arbeiten max"' }, + { i: 3, name: 'HWR', temp: '"Temp HWR"', act: '"Heizung HWR"', min: '"Einstellungen".Heizung."Heizung HWR min"', max: '"Einstellungen".Heizung."Heizung HWR max"' }, + { i: 4, name: 'Bad oben', temp: '"Temp Bad"', act: '"Heizung Bad"', min: '"Einstellungen".Heizung."Heizung Bad min"', max: '"Einstellungen".Heizung."Heizung Bad max"' }, + { i: 5, name: 'Eltern', temp: '"Temp Eltern"', act: '"Heizung Eltern"', min: '"Einstellungen".Heizung."Heizung Eltern min"', max: '"Einstellungen".Heizung."Heizung Eltern max"' }, + { i: 6, name: 'Noah', temp: '"Temp Noah"', act: '"Heizung Noah"', min: '"Einstellungen".Heizung."Heizung Kind1 min"', max: '"Einstellungen".Heizung."Heizung Kind1 max"' }, + { i: 7, name: 'Mika', temp: '"Temp Mika"', act: '"Heizung Mika"', min: '"Einstellungen".Heizung."Heizung Kind2 min"', max: '"Einstellungen".Heizung."Heizung Kind2 max"' }, + { i: 8, name: 'Niko', temp: '"Temp Niko"', act: '"Heizung Niko"', min: '"Einstellungen".Heizung."Heizung Gäste min"', max: '"Einstellungen".Heizung."Heizung Gäste max"' }, + { i: 9, name: 'Dusche', temp: '"Temp Dusche"', act: '"Heizung Gäste Dusche"', min: '"Einstellungen".Heizung."Heizung Dusche min"', max: '"Einstellungen".Heizung."Heizung Dusche max"' } + ]; + + var SOCKETS = [ + ['Deele', '"Steckdose Deele Garder"'], + ['Wohnzimmer', '"Steckdose Wohnen"'], + ['Esszimmer', '"Steckdose Essen"'], + ['Arbeiten', '"Steckdose Arbeiten"'], + ['Noah', '"Steckdose Kind 1"'], + ['Mika', '"Steckdose Kind 2"'], + ['Niko', '"Steckdose Gäste"'], + ['Eltern', '"Steckdose Eltern"'] + ]; + + var ROLLADEN = [ + ['Küche', '"Rolladenmerker Küc l auf"', '"Rolladenmerker Küc l ab"', '"Rolladenmerker Küc r auf"', '"Rolladenmerker Küc r ab"'], + ['Esszimmer', '"Rolladenmerker Ess l auf"', '"Rolladenmerker Ess l ab"', '"Rolladenmerker Ess m auf"', '"Rolladenmerker Ess m ab"', '"Rolladenmerker Ess r auf"', '"Rolladenmerker Ess r ab"'], + ['Wohnzimmer', '"Rolladenmerker Woh l auf"', '"Rolladenmerker Woh l ab"', '"Rolladenmerker Woh r auf"', '"Rolladenmerker Woh r ab"', '"Rolladenmerker Wohtü auf"', '"Rolladenmerker Wohtü ab"'], + ['Arbeiten', '"Rolladenmerker Arbei auf"', '"Rolladenmerker Arbei ab"'], + ['HWR', '"Rolladenmerker HWR auf"', '"Rolladenmerker HWR ab"'], + ['Niko', '"Rolladenmerker Niko auf"', '"Rolladenmerker Niko ab"'], + ['Dusche', '"Rolladenmerker Dusche au"', '"Rolladenmerker Dusche ab"'], + ['Eltern', '"Rolladenmerker Elter auf"', '"Rolladenmerker Elter ab"'], + ['Noah', '"Rolladenmerker Noah auf"', '"Rolladenmerker Noah ab"'], + ['Bad', '"Rolladenmerker Bad auf"', '"Rolladenmerker Bad ab"'], + ['Mika', '"Rolladenmerker Mika auf"', '"Rolladenmerker Mika ab"'] + ]; + + var POOL = { + sek: '"Funksteckdosen_DB"."Poolpumpenzähler Sek/Tag"', + max: '"Funksteckdosen_DB"."Poolpumpe min Laufzeit/Tag"' + }; + + var METER = { + bezug: '"Stromzähler".Stromzähler."Zählerstand Bezug"', + einsp: '"Stromzähler".Stromzähler."Zählerstand Einspeisung"', + leist: '"Stromzähler".Stromzähler."aktuelle Leistung"', + tag: '"Stromzähler".Stromzähler.Aufzeichnung."täglicher Bezug"', + wemos: '"Buffer"."Wemos RC".Senden', + hist: [ + '"Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[0]."Zählerstand Bezug"', + '"Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[1]."Zählerstand Bezug"', + '"Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[2]."Zählerstand Bezug"', + '"Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[3]."Zählerstand Bezug"', + '"Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[4]."Zählerstand Bezug"', + '"Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[5]."Zählerstand Bezug"', + '"Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[6]."Zählerstand Bezug"', + '"Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[7]."Zählerstand Bezug"' + ] + }; + + var WATER = { + stand: '"Wasserzähler".Zählerstand[0].aktueller_Stand', + seit: '"Wasserzähler".Zählerstand[0].seit_letzter_Ablesung', + wtemp: '"Wasserzähler".Zählerstand[0].Wassertemperatur', + tag: '"Wasserzähler"."täglicher Bezug"', + hist: [ + '"Wasserzähler".Zählerstand[0].aktueller_Stand', + '"Wasserzähler".Zählerstand[1].aktueller_Stand', + '"Wasserzähler".Zählerstand[2].aktueller_Stand', + '"Wasserzähler".Zählerstand[3].aktueller_Stand', + '"Wasserzähler".Zählerstand[4].aktueller_Stand', + '"Wasserzähler".Zählerstand[5].aktueller_Stand', + '"Wasserzähler".Zählerstand[6].aktueller_Stand', + '"Wasserzähler".Zählerstand[7].aktueller_Stand' + ] + }; + + var ZEIT = { + y: '"Aufgang_Untergang".aktuelle_CPU_Zeit.YEAR', + mo: '"Aufgang_Untergang".aktuelle_CPU_Zeit.MONTH', + d: '"Aufgang_Untergang".aktuelle_CPU_Zeit.DAY', + h: '"Aufgang_Untergang".aktuelle_CPU_Zeit.HOUR', + mi: '"Aufgang_Untergang".aktuelle_CPU_Zeit.MINUTE', + s: '"Aufgang_Untergang".aktuelle_CPU_Zeit.SECOND', + aufH: '"Aufgang_Untergang".Aufgang.HOUR', + aufMi: '"Aufgang_Untergang".Aufgang.MINUTE', + untH: '"Aufgang_Untergang".Untergang.HOUR', + untMi: '"Aufgang_Untergang".Untergang.MINUTE', + urlaub: '"Jalousie_DB".Urlaub' + }; + + function allReadVars() { + var v = []; + LIGHTS.forEach(function (l) { v.push(l[1]); }); + HEAT.forEach(function (h) { v.push(h.temp, h.act); }); + SOCKETS.forEach(function (s) { v.push(s[1]); }); + v.push(POOL.sek, POOL.max); + v.push(METER.bezug, METER.einsp, METER.leist, METER.tag); + METER.hist.forEach(function (x) { v.push(x); }); + v.push(WATER.stand, WATER.seit, WATER.wtemp, WATER.tag); + WATER.hist.forEach(function (x) { v.push(x); }); + Object.keys(ZEIT).forEach(function (k) { v.push(ZEIT[k]); }); + return v; + } + + function settingsVars() { + var v = []; + HEAT.forEach(function (h) { v.push(h.min, h.max); }); + return v; + } + + // ====== Hilfen ====== + function on(v) { return v === true || v === 1 || v === '1'; } + function num(v) { var n = parseFloat(v); return isNaN(n) ? null : n; } + function pad(n) { n = parseInt(n, 10); if (isNaN(n)) { return '--'; } return n < 10 ? '0' + n : '' + n; } + function setText(id, value) { var el = document.getElementById(id); if (el) { el.textContent = value; } } + function tempClass(t) { + if (t === null) { return 'v4-heat-temp--normal'; } + if (t >= 24) { return 'v4-heat-temp--hot'; } + if (t >= 22) { return 'v4-heat-temp--warm'; } + return 'v4-heat-temp--normal'; + } + function formatStdMin(sek) { + if (sek === null || !isFinite(sek)) { return '–'; } + var min = Math.round(sek / 60); + var h = Math.floor(min / 60); + var restMin = min % 60; + return (h > 0) ? (h + ' h ' + restMin + ' min') : (restMin + ' min'); + } + + // ====== UI aufbauen ====== + function buildLightTiles() { + var wrap = document.getElementById('v4_light_tiles'); + if (!wrap) { return; } + LIGHTS.forEach(function (l, idx) { + var tile = document.createElement('div'); + tile.className = 'v4-tile'; + tile.id = 'li_tile_' + idx; + tile.innerHTML = '' + l[0] + ''; + tile.addEventListener('click', function (ev) { + // Ripple + var rect = tile.getBoundingClientRect(); + var r = document.createElement('span'); + r.className = 'v4-tile-ripple'; + var size = Math.max(rect.width, rect.height); + r.style.width = r.style.height = size + 'px'; + r.style.left = (ev.clientX - rect.left - size / 2) + 'px'; + r.style.top = (ev.clientY - rect.top - size / 2) + 'px'; + tile.appendChild(r); + setTimeout(function () { r.remove(); }, 500); + write(l[2], true).then(function (res) { + flash(tile, res); + toast(l[0] + ': geschaltet', res && !res.error ? 'ok' : 'err'); + }); + }); + wrap.appendChild(tile); + }); + } + + function buildRolladen() { + var wrap = document.getElementById('v4_rolladen'); + if (!wrap) { return; } + ROLLADEN.forEach(function (r) { + var row = document.createElement('div'); + row.className = 'v4-roll-row'; + var name = document.createElement('span'); + name.className = 'v4-roll-name'; + name.textContent = r[0]; + var btns = document.createElement('span'); + btns.className = 'v4-roll-btns'; + // auf/ab Paare + for (var i = 1; i < r.length; i += 2) { + var up = document.createElement('button'); + up.className = 'v4-btn v4-btn--on'; + up.type = 'button'; + up.textContent = '↑'; + up.title = 'hoch'; + up.setAttribute('data-var', r[i]); + up.setAttribute('data-value', 'true'); + var down = document.createElement('button'); + down.className = 'v4-btn v4-btn--off'; + down.type = 'button'; + down.textContent = '↓'; + down.title = 'runter'; + down.setAttribute('data-var', r[i + 1]); + down.setAttribute('data-value', 'true'); + btns.appendChild(up); + btns.appendChild(down); + } + row.appendChild(name); + row.appendChild(btns); + wrap.appendChild(row); + }); + } + + function buildSockets() { + var wrap = document.getElementById('v4_sockets'); + if (!wrap) { return; } + SOCKETS.forEach(function (s, idx) { + var row = document.createElement('div'); + row.className = 'v4-socket-row'; + var name = document.createElement('span'); + name.className = 'v4-socket-name'; + name.textContent = s[0]; + var orb = document.createElement('span'); + orb.className = 'v4-socket-orb'; + orb.id = 'sd_orb_' + idx; + orb.title = 'an/aus'; + orb.addEventListener('click', function () { + // Status false/0 = AN, true/1 = AUS -> umschalten + var cur = lastValues[s[1]]; + var isOn = (cur === false || cur === 0); + write(s[1], isOn ? true : false).then(function (res) { + flash(orb, res); + toast(s[0] + ': ' + (isOn ? 'AUS' : 'AN'), res && !res.error ? 'ok' : 'err'); + }); + }); + row.appendChild(name); + row.appendChild(orb); + wrap.appendChild(row); + }); + } + + function buildHeat() { + var wrap = document.getElementById('v4_heat'); + if (!wrap) { return; } + HEAT.forEach(function (h) { + var row = document.createElement('div'); + row.className = 'v4-heat-row'; + row.id = 'he_row_' + h.i; + row.innerHTML = + '' + h.name + '' + + '' + + '' + + '' + + '' + + ''; + wrap.appendChild(row); + }); + } + + // Plausibilitaet min < max ueber die ganze Heizungszeile + function heatPlausible(i, kind, newVal) { + var mn = num(lastValues[HEAT[i - 1].min]); + var mx = num(lastValues[HEAT[i - 1].max]); + if (kind === 'min') { mn = newVal; } else { mx = newVal; } + if (mn !== null && mx !== null && mn >= mx) { + toast('Min-Temperatur muss kleiner sein als Max.', 'err'); + return false; + } + return true; + } + + // ====== Rendern ====== + var powerHistory = []; + var gaugeTarget = 0, gaugeShown = 0; + var prevTemps = {}, arrowState = {}; + var lastValues = {}; + + function addPowerSample(v) { + if (v === null || !isFinite(v)) { return; } + var now = Date.now(); + powerHistory.push({ t: now, v: v }); + var cutoff = now - 5 * 60 * 1000; + while (powerHistory.length && powerHistory[0].t < cutoff) { powerHistory.shift(); } + } + + function render(m) { + // Licht-Tiles + LIGHTS.forEach(function (l, idx) { + var tile = document.getElementById('li_tile_' + idx); + if (tile) { tile.classList.toggle('v4-tile--on', on(m[l[1]])); } + }); + + // Heizung + HEAT.forEach(function (h) { + var t = num(m[h.temp]); + var tEl = document.getElementById('he_temp_' + h.i); + if (tEl) { + if (t === null) { + tEl.textContent = '–'; + } else { + var p = prevTemps[h.i]; + if (p != null) { + if (t > p + 0.1) { arrowState[h.i] = ' ▲'; } + else if (t < p - 0.1) { arrowState[h.i] = ' ▼'; } + } + prevTemps[h.i] = t; + tEl.textContent = t.toFixed(1) + '°' + (arrowState[h.i] || ''); + } + tEl.className = 'v4-heat-temp ' + tempClass(t); + } + var dot = document.getElementById('he_dot_' + h.i); + if (dot) { dot.classList.toggle('v4-heat-dot--on', on(m[h.act])); } + // SOLL-Stepper-Werte anzeigen + var minEl = document.getElementById('he_min_' + h.i); + var maxEl = document.getElementById('he_max_' + h.i); + var mnVal = num(m[h.min]), mxVal = num(m[h.max]); + if (minEl) { minEl.textContent = (mnVal === null ? '–' : mnVal.toFixed(1) + '°'); } + if (maxEl) { maxEl.textContent = (mxVal === null ? '–' : mxVal.toFixed(1) + '°'); } + // Temp-Bar: IST vs. SOLL-Band + var barWrap = document.getElementById('he_bar_' + h.i); + if (barWrap) { + barWrap.innerHTML = ''; + var mn = num(m[h.min]), mx = num(m[h.max]); + if (t !== null && mn !== null && mx !== null && mx > mn) { + var lo = 10, hi = 30; // Skala 10..30°C + var pct = Math.max(0, Math.min(100, (t - lo) / (hi - lo) * 100)); + var bar = document.createElement('div'); + bar.className = 'v4-heat-bar'; + bar.style.width = pct + '%'; + barWrap.appendChild(bar); + var band = document.createElement('div'); + band.className = 'v4-heat-band'; + var bLo = Math.max(0, Math.min(100, (mn - lo) / (hi - lo) * 100)); + var bHi = Math.max(0, Math.min(100, (mx - lo) / (hi - lo) * 100)); + band.style.left = bLo + '%'; + band.style.width = Math.max(2, bHi - bLo) + '%'; + barWrap.appendChild(band); + } + } + }); + + // Steckdosen (Status false/0 = AN) + SOCKETS.forEach(function (s, idx) { + var orb = document.getElementById('sd_orb_' + idx); + if (orb) { + var val = m[s[1]]; + var known = (val !== null && val !== undefined); + orb.classList.toggle('v4-socket-orb--on', known && (val === false || val === 0)); + } + }); + + // Poolpumpe + var poolSek = num(m[POOL.sek]); + var poolMax = num(m[POOL.max]); + // SOLL min im Eingabefeld anzeigen (Sekunden -> Minuten) + var poolInput = document.getElementById('sd_pool_max_input'); + if (poolInput && document.activeElement !== poolInput && poolMax !== null) { + poolInput.value = Math.round(poolMax / 60); + } + var bar = document.getElementById('sd_pool_bar'); + var barLabel = document.getElementById('sd_pool_bar_label'); + if (bar && barLabel) { + if (poolSek === null || poolMax === null || poolMax <= 0) { + bar.style.width = '0%'; + barLabel.textContent = '– / –'; + bar.classList.remove('v4-pool-bar-fill--done'); + } else { + var pct = Math.min(100, poolSek / poolMax * 100); + bar.style.width = pct.toFixed(1) + '%'; + barLabel.textContent = formatStdMin(poolSek) + ' / ' + formatStdMin(poolMax); + bar.classList.toggle('v4-pool-bar-fill--done', pct >= 100); + } + } + + // Stromzähler + var bez = num(m[METER.bezug]), ein = num(m[METER.einsp]), lei = num(m[METER.leist]), tag = num(m[METER.tag]); + setText('sz_bezug', (bez === null ? '–' : bez.toFixed(1)) + ' kWh'); + setText('sz_einsp', (ein === null ? '–' : ein.toFixed(1)) + ' kWh'); + setText('sz_leist', (lei === null ? '–' : Math.round(lei)) + ' W'); + setText('sz_tag', (tag === null ? '–' : tag.toFixed(1)) + ' kWh'); + addPowerSample(lei); + gaugeTarget = (lei === null ? 0 : lei); + + // Wasser + var wStand = num(m[WATER.stand]), wSeit = num(m[WATER.seit]); + var wTemp = num(m[WATER.wtemp]); + setText('hdr_wtemp', (wTemp === null ? '–' : wTemp.toFixed(0) + ' °C')); + setText('hdr_wstand', (wStand === null ? '–' : wStand.toFixed(0) + ' L')); + setText('hdr_wseit', (wSeit === null ? '–' : wSeit.toFixed(0) + ' L')); + + // Uhr / Sonne / Urlaub + setText('clock_time', pad(m[ZEIT.h]) + ':' + pad(m[ZEIT.mi]) + ':' + pad(m[ZEIT.s])); + setText('clock_date', pad(m[ZEIT.d]) + '.' + pad(m[ZEIT.mo]) + '.' + (m[ZEIT.y] != null ? m[ZEIT.y] : '----')); + setText('sun_up', pad(m[ZEIT.aufH]) + ':' + pad(m[ZEIT.aufMi])); + setText('sun_down', pad(m[ZEIT.untH]) + ':' + pad(m[ZEIT.untMi])); + var urlaub = on(m[ZEIT.urlaub]); + var udot = document.getElementById('roll_urlaub_dot'); + if (udot) { udot.classList.toggle('v4-urlaub-indicator--on', urlaub); } + setText('roll_urlaub_text', urlaub ? 'AN' : 'AUS'); + } + + // ====== Canvas ====== + function fitCanvas(canvas) { + var rect = canvas.getBoundingClientRect(); + var dpr = window.devicePixelRatio || 1; + var targetW = Math.round(rect.width * dpr); + var targetH = Math.round(rect.height * dpr); + if (canvas.width !== targetW || canvas.height !== targetH) { + canvas.width = targetW; + canvas.height = targetH; + } + var ctx = canvas.getContext('2d'); + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + return { w: rect.width, h: rect.height, ctx: ctx }; + } + + function roundRect(ctx, x, y, w, h, r) { + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.arcTo(x + w, y, x + w, y + h, r); + ctx.arcTo(x + w, y + h, x, y + h, r); + ctx.arcTo(x, y + h, x, y, r); + ctx.arcTo(x, y, x + w, y, r); + ctx.closePath(); + } + + // HUD-Dial (Halbkreis-Tacho) + function drawGauge(value) { + var canvas = document.getElementById('v4_gauge'); + if (!canvas) { return; } + var fit = fitCanvas(canvas); + var ctx = fit.ctx, w = fit.w, h = fit.h; + ctx.clearRect(0, 0, w, h); + + var MIN = -8000, MAX = 8000; + var v = Math.max(MIN, Math.min(MAX, isFinite(value) ? value : 0)); + var cx = w / 2; + var cy = h - 20; + var R = Math.min(w / 2 - 20, h - 30); + + // Bogen-Hintergrund + ctx.strokeStyle = 'rgba(0, 229, 255, 0.15)'; + ctx.lineWidth = 14; + ctx.beginPath(); + ctx.arc(cx, cy, R, Math.PI, 0); + ctx.stroke(); + + // Skala + ctx.strokeStyle = 'rgba(0, 229, 255, 0.4)'; + ctx.lineWidth = 1; + for (var t = -8000; t <= 8000; t += 1000) { + var ang = Math.PI + (t - MIN) / (MAX - MIN) * Math.PI; + var x1 = cx + Math.cos(ang) * (R - 8); + var y1 = cy + Math.sin(ang) * (R - 8); + var x2 = cx + Math.cos(ang) * (R + 8); + var y2 = cy + Math.sin(ang) * (R + 8); + ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); + } + + // Aktiver Bogen (grün links = Einspeisung, rot rechts = Bezug) + var frac = (v - MIN) / (MAX - MIN); + var col = v < 0 ? '0,255,156' : '255,59,92'; + ctx.save(); + ctx.strokeStyle = 'rgba(' + col + ',0.9)'; + ctx.lineWidth = 14; + ctx.shadowColor = 'rgba(' + col + ',0.6)'; + ctx.shadowBlur = 12; + ctx.beginPath(); + if (v >= 0) { + ctx.arc(cx, cy, R, Math.PI, Math.PI + frac * Math.PI); + } else { + ctx.arc(cx, cy, R, Math.PI + frac * Math.PI, 0); + } + ctx.stroke(); + ctx.restore(); + + // Nadel + var angN = Math.PI + frac * Math.PI; + var nx = cx + Math.cos(angN) * (R - 20); + var ny = cy + Math.sin(angN) * (R - 20); + ctx.save(); + ctx.strokeStyle = 'rgba(' + col + ',1)'; + ctx.lineWidth = 2; + ctx.shadowColor = 'rgba(' + col + ',0.8)'; + ctx.shadowBlur = 8; + ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(nx, ny); ctx.stroke(); + ctx.restore(); + + // Digitalwert + ctx.textAlign = 'center'; + ctx.fillStyle = 'rgba(' + col + ',1)'; + ctx.font = 'bold 26px Consolas, monospace'; + ctx.shadowColor = 'rgba(' + col + ',0.6)'; + ctx.shadowBlur = 10; + ctx.fillText(Math.round(v) + ' W', cx, cy - R + 30); + ctx.shadowBlur = 0; + ctx.fillStyle = 'rgba(230,247,255,0.6)'; + ctx.font = '12px Consolas, monospace'; + ctx.fillText('LEISTUNG', cx, cy - R + 48); + } + + // Weiche Kurve + function smoothCurve(ctx, pts) { + if (pts.length < 2) { return; } + ctx.moveTo(pts[0].x, pts[0].y); + for (var i = 0; i < pts.length - 1; i++) { + var p0 = pts[i > 0 ? i - 1 : 0]; + var p1 = pts[i]; + var p2 = pts[i + 1]; + var p3 = pts[i + 2 < pts.length ? i + 2 : i + 1]; + var c1x = p1.x + (p2.x - p0.x) / 6, c1y = p1.y + (p2.y - p0.y) / 6; + var c2x = p2.x - (p3.x - p1.x) / 6, c2y = p2.y - (p3.y - p1.y) / 6; + ctx.bezierCurveTo(c1x, c1y, c2x, c2y, p2.x, p2.y); + } + } + + function drawPowerChart() { + var canvas = document.getElementById('v4_power_chart'); + if (!canvas) { return; } + var fit = fitCanvas(canvas); + var ctx = fit.ctx, w = fit.w, h = fit.h; + var now = Date.now(); + ctx.clearRect(0, 0, w, h); + ctx.fillStyle = 'rgba(0, 10, 20, 0.6)'; + ctx.fillRect(0, 0, w, h); + + var padL = 44, padR = 12, padT = 22, padB = 18; + var plotW = w - padL - padR, plotH = h - padT - padB; + + ctx.textAlign = 'left'; + ctx.fillStyle = 'rgba(0, 229, 255, 0.9)'; + ctx.font = 'bold 12px Consolas, monospace'; + ctx.fillText('LEISTUNG – 5 MIN', 12, 14); + + if (!powerHistory.length) { + ctx.fillStyle = 'rgba(230,247,255,0.5)'; + ctx.font = '12px Consolas, monospace'; + ctx.fillText('warte auf Daten…', 12, padT + plotH / 2); + return; + } + + var WINDOW = 5 * 60 * 1000; + var t1 = now, t0 = now - WINDOW; + + var minV = 0, maxV = 0; + for (var i = 0; i < powerHistory.length; i++) { + var v = powerHistory[i].v; + if (v < minV) { minV = v; } + if (v > maxV) { maxV = v; } + } + if (minV === maxV) { minV -= 100; maxV += 100; } + var pad = (maxV - minV) * 0.12; + minV -= pad; maxV += pad; + + function X(t) { return padL + ((t - t0) / (t1 - t0)) * plotW; } + function Y(v) { return padT + (1 - (v - minV) / (maxV - minV)) * plotH; } + + // Gitter + ctx.strokeStyle = 'rgba(0, 229, 255, 0.08)'; + ctx.lineWidth = 1; + for (var gy = 0; gy <= 4; gy++) { + var yy = padT + plotH * gy / 4; + ctx.beginPath(); ctx.moveTo(padL, yy); ctx.lineTo(padL + plotW, yy); ctx.stroke(); + } + for (var mm = 1; mm <= 5; mm++) { + var xx = X(now - mm * 60000); + ctx.beginPath(); ctx.moveTo(xx, padT); ctx.lineTo(xx, padT + plotH); ctx.stroke(); + } + + var pts = []; + for (var k = 0; k < powerHistory.length; k++) { + var p = powerHistory[k]; + if (p.t < t0) { continue; } + pts.push({ x: X(p.t), y: Y(p.v), v: p.v }); + } + if (!pts.length) { return; } + var lastReal = pts[pts.length - 1]; + pts.push({ x: X(now), y: lastReal.y, v: lastReal.v }); + + var zeroY = Y(0); + var zeroFrac = Math.max(0, Math.min(1, (zeroY - padT) / plotH)); + + // Fläche + ctx.beginPath(); + smoothCurve(ctx, pts); + ctx.lineTo(pts[pts.length - 1].x, zeroY); + ctx.lineTo(pts[0].x, zeroY); + ctx.closePath(); + var grad = ctx.createLinearGradient(0, padT, 0, padT + plotH); + grad.addColorStop(0, 'rgba(255,59,92,0.35)'); + grad.addColorStop(Math.max(0, zeroFrac - 0.001), 'rgba(255,59,92,0.05)'); + grad.addColorStop(Math.min(1, zeroFrac + 0.001), 'rgba(0,255,156,0.05)'); + grad.addColorStop(1, 'rgba(0,255,156,0.35)'); + ctx.fillStyle = grad; + ctx.fill(); + + // Nulllinie + if (minV < 0 && maxV > 0) { + ctx.save(); + ctx.setLineDash([4, 4]); + ctx.strokeStyle = 'rgba(230,247,255,0.3)'; + ctx.lineWidth = 1; + ctx.beginPath(); ctx.moveTo(padL, zeroY); ctx.lineTo(padL + plotW, zeroY); ctx.stroke(); + ctx.restore(); + } + + // Linie + ctx.save(); + ctx.beginPath(); + smoothCurve(ctx, pts); + ctx.strokeStyle = 'rgba(0, 229, 255, 0.95)'; + ctx.lineWidth = 2.4; + ctx.lineJoin = 'round'; + ctx.shadowColor = 'rgba(0, 229, 255, 0.55)'; + ctx.shadowBlur = 8; + ctx.stroke(); + ctx.restore(); + + // Y-Beschriftung + ctx.fillStyle = 'rgba(230,247,255,0.55)'; + ctx.font = '10px Consolas, monospace'; + ctx.textAlign = 'right'; + ctx.fillText(Math.round(maxV) + ' W', padL - 6, padT + 8); + ctx.fillText(Math.round(minV) + ' W', padL - 6, padT + plotH); + + // Live-Punkt + var lp = pts[pts.length - 1]; + var col = lp.v < 0 ? '0,255,156' : '255,59,92'; + var pulse = 0.5 + 0.5 * Math.sin(now / 320); + ctx.fillStyle = 'rgba(' + col + ',' + (0.16 + 0.14 * pulse) + ')'; + ctx.beginPath(); ctx.arc(lp.x, lp.y, 6 + 4 * pulse, 0, Math.PI * 2); ctx.fill(); + ctx.fillStyle = 'rgba(' + col + ',1)'; + ctx.beginPath(); ctx.arc(lp.x, lp.y, 3.2, 0, Math.PI * 2); ctx.fill(); + + // Wert-Pille + var label = 'akt. Leistung: ' + Math.round(lp.v) + ' W'; + ctx.font = 'bold 11px Consolas, monospace'; + var tw = ctx.measureText(label).width; + var bw = tw + 14, bx = w - padR - bw, by = 4, bh = 17; + ctx.fillStyle = 'rgba(' + col + ',0.16)'; + roundRect(ctx, bx, by, bw, bh, 9); ctx.fill(); + ctx.fillStyle = 'rgba(' + col + ',1)'; + ctx.textAlign = 'left'; + ctx.fillText(label, bx + 7, by + 12); + } + + function drawDailyChart() { + var canvas = document.getElementById('v4_daily_chart'); + if (!canvas) { return; } + var fit = fitCanvas(canvas); + var ctx = fit.ctx, w = fit.w, h = fit.h; + ctx.clearRect(0, 0, w, h); + ctx.fillStyle = 'rgba(0, 10, 20, 0.6)'; ctx.fillRect(0, 0, w, h); + + var padL = 44, padR = 12, padT = 22, padB = 28; + var plotW = w - padL - padR, plotH = h - padT - padB; + + var bez = num(lastValues[METER.bezug]); + var hist = METER.hist.map(function (x) { return num(lastValues[x]); }); + var seq = [bez].concat(hist); + var dailyValues = []; + for (var d = 1; d <= 8; d++) { + var a = seq[d - 1], b = seq[d]; + dailyValues.push((a === null || b === null) ? 0 : (a - b)); + } + dailyValues.reverse(); + var tagVal = num(lastValues[METER.tag]) || 0; + + var maxV = Math.max(tagVal, Math.max.apply(null, dailyValues)); + var minV = 0; + if (maxV < 1) { maxV = 10; } + var pd = (maxV - minV) * 0.12; + maxV += pd; + + function Y(v) { return padT + (1 - (v - minV) / (maxV - minV)) * plotH; } + function X(i) { return padL + (i + 0.5) * (plotW / 8); } + + var wochentage = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']; + var dayLabels = []; + var cpuY = lastValues[ZEIT.y], cpuM = lastValues[ZEIT.mo], cpuD = lastValues[ZEIT.d]; + var heute = (cpuY != null && cpuM != null && cpuD != null) + ? new Date(cpuY, cpuM - 1, cpuD) + : new Date(); + for (var i = 0; i < 8; i++) { + var dd = new Date(heute.getFullYear(), heute.getMonth(), heute.getDate() - (7 - i)); + dayLabels.push(wochentage[dd.getDay()]); + } + + ctx.textAlign = 'left'; ctx.fillStyle = 'rgba(0, 229, 255, 0.9)'; + ctx.font = 'bold 12px Consolas, monospace'; + ctx.fillText('TAGESBEZUG – 8 TAGE', 12, 14); + + ctx.strokeStyle = 'rgba(0, 229, 255, 0.08)'; ctx.lineWidth = 1; + for (var gy = 0; gy <= 4; gy++) { + var yy = padT + plotH * gy / 4; + ctx.beginPath(); ctx.moveTo(padL, yy); ctx.lineTo(padL + plotW, yy); ctx.stroke(); + } + + var barW = plotW / 8 * 0.55; + for (var i = 0; i < 8; i++) { + var x = X(i); + var y0 = Y(0); + var y1 = Y(dailyValues[i]); + var bh = y0 - y1; + var bx = x - barW / 2; + var isToday = (i === 7); + var grad = ctx.createLinearGradient(0, y1, 0, y0); + if (isToday) { + grad.addColorStop(0, 'rgba(255,179,0,0.95)'); + grad.addColorStop(1, 'rgba(255,179,0,0.25)'); + } else { + grad.addColorStop(0, 'rgba(0,255,156,0.95)'); + grad.addColorStop(1, 'rgba(0,255,156,0.25)'); + } + ctx.fillStyle = grad; + roundRect(ctx, bx, y1, barW, bh, 4); + ctx.fill(); + ctx.fillStyle = isToday ? 'rgba(255,179,0,0.95)' : 'rgba(230,247,255,0.85)'; + ctx.font = 'bold 10px Consolas, monospace'; + ctx.textAlign = 'center'; + ctx.fillText(dailyValues[i].toFixed(2), x, y1 - 5); + ctx.fillStyle = isToday ? 'rgba(255,179,0,0.95)' : 'rgba(230,247,255,0.65)'; + ctx.font = 'bold 10px Consolas, monospace'; + ctx.textAlign = 'center'; + ctx.fillText(dayLabels[i], x, h - 8); + } + + var tagY = Y(tagVal); + ctx.save(); + ctx.strokeStyle = 'rgba(0, 229, 255, 0.95)'; ctx.lineWidth = 2.5; + ctx.shadowColor = 'rgba(0, 229, 255, 0.55)'; ctx.shadowBlur = 8; + ctx.beginPath(); ctx.moveTo(padL, tagY); ctx.lineTo(padL + plotW, tagY); ctx.stroke(); + ctx.restore(); + + ctx.fillStyle = 'rgba(230,247,255,0.55)'; ctx.font = '10px Consolas, monospace'; + ctx.textAlign = 'right'; + ctx.fillText(maxV.toFixed(1) + ' kWh', padL - 6, padT + 8); + ctx.fillText(minV.toFixed(1) + ' kWh', padL - 6, padT + plotH); + + var label = 'täglicher Bezug: ' + tagVal.toFixed(2) + ' kWh'; + ctx.font = 'bold 11px Consolas, monospace'; + var tw = ctx.measureText(label).width, bw = tw + 14, bx = w - padR - bw, by = 4, bh = 17; + ctx.fillStyle = 'rgba(0,229,255,0.16)'; roundRect(ctx, bx, by, bw, bh, 9); ctx.fill(); + ctx.fillStyle = 'rgba(0,229,255,1)'; ctx.textAlign = 'left'; + ctx.fillText(label, bx + 7, by + 12); + } + + function drawWaterChart() { + var canvas = document.getElementById('v4_water_chart'); + if (!canvas) { return; } + var fit = fitCanvas(canvas); + var ctx = fit.ctx, w = fit.w, h = fit.h; + ctx.clearRect(0, 0, w, h); + ctx.fillStyle = 'rgba(0, 10, 20, 0.6)'; ctx.fillRect(0, 0, w, h); + + var padL = 50, padR = 16, padT = 22, padB = 28; + var plotW = w - padL - padR, plotH = h - padT - padB; + + var hist = WATER.hist.map(function (x) { return num(lastValues[x]); }); + var dailyValues = []; + for (var d = 1; d < hist.length; d++) { + var a = hist[d - 1], b = hist[d]; + dailyValues.push((a === null || b === null) ? 0 : (a - b)); + } + dailyValues.reverse(); + + var maxV = Math.max.apply(null, dailyValues); + var minV = 0; + var tagVal = num(lastValues[WATER.tag]) || 0; + if (tagVal > maxV) { maxV = tagVal; } + if (maxV < 1) { maxV = 100; } + var pd = (maxV - minV) * 0.12; + maxV += pd; + + function Y(v) { return padT + (1 - (v - minV) / (maxV - minV)) * plotH; } + function X(i) { return padL + (i + 0.5) * (plotW / dailyValues.length); } + + var wochentage = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa']; + var dayLabels = []; + var cpuY = lastValues[ZEIT.y], cpuM = lastValues[ZEIT.mo], cpuD = lastValues[ZEIT.d]; + var heute = (cpuY != null && cpuM != null && cpuD != null) + ? new Date(cpuY, cpuM - 1, cpuD) + : new Date(); + for (var i = 0; i < dailyValues.length; i++) { + var dd = new Date(heute.getFullYear(), heute.getMonth(), heute.getDate() - (dailyValues.length - 1 - i)); + dayLabels.push(wochentage[dd.getDay()]); + } + + ctx.textAlign = 'left'; ctx.fillStyle = 'rgba(0, 229, 255, 0.9)'; + ctx.font = 'bold 12px Consolas, monospace'; + ctx.fillText('WASSER – 7 TAGE', 12, 14); + + ctx.strokeStyle = 'rgba(0, 229, 255, 0.08)'; ctx.lineWidth = 1; + for (var gy = 0; gy <= 4; gy++) { + var yy = padT + plotH * gy / 4; + ctx.beginPath(); ctx.moveTo(padL, yy); ctx.lineTo(padL + plotW, yy); ctx.stroke(); + } + + var barW = plotW / dailyValues.length * 0.55; + for (var i = 0; i < dailyValues.length; i++) { + var x = X(i); + var y0 = Y(0); + var y1 = Y(dailyValues[i]); + var bh = y0 - y1; + var bx = x - barW / 2; + var isToday = (i === dailyValues.length - 1); + var grad = ctx.createLinearGradient(0, y1, 0, y0); + if (isToday) { + grad.addColorStop(0, 'rgba(0,229,255,0.95)'); + grad.addColorStop(1, 'rgba(0,229,255,0.25)'); + } else { + grad.addColorStop(0, 'rgba(0,229,255,0.75)'); + grad.addColorStop(1, 'rgba(0,229,255,0.15)'); + } + ctx.fillStyle = grad; + roundRect(ctx, bx, y1, barW, bh, 4); + ctx.fill(); + ctx.fillStyle = isToday ? 'rgba(0,229,255,0.95)' : 'rgba(230,247,255,0.85)'; + ctx.font = 'bold 10px Consolas, monospace'; + ctx.textAlign = 'center'; + ctx.fillText(Math.round(dailyValues[i]), x, y1 - 5); + ctx.fillStyle = isToday ? 'rgba(0,229,255,0.95)' : 'rgba(230,247,255,0.65)'; + ctx.font = 'bold 10px Consolas, monospace'; + ctx.textAlign = 'center'; + ctx.fillText(dayLabels[i], x, h - 8); + } + + var tagY = Y(tagVal); + ctx.save(); + ctx.strokeStyle = 'rgba(255,179,0,0.95)'; ctx.lineWidth = 2.5; + ctx.setLineDash([6, 4]); + ctx.shadowColor = 'rgba(255,179,0,0.55)'; ctx.shadowBlur = 8; + ctx.beginPath(); ctx.moveTo(padL, tagY); ctx.lineTo(padL + plotW, tagY); ctx.stroke(); + ctx.restore(); + + ctx.fillStyle = 'rgba(230,247,255,0.55)'; ctx.font = '10px Consolas, monospace'; + ctx.textAlign = 'right'; + ctx.fillText(Math.round(maxV) + ' L', padL - 6, padT + 8); + ctx.fillText(Math.round(minV) + ' L', padL - 6, padT + plotH); + + var label = 'täglicher Bezug: ' + Math.round(tagVal) + ' L'; + ctx.font = 'bold 11px Consolas, monospace'; + var tw = ctx.measureText(label).width, bw = tw + 14, bx = w - padR - bw, by = 4, bh = 17; + ctx.fillStyle = 'rgba(255,179,0,0.16)'; roundRect(ctx, bx, by, bw, bh, 9); ctx.fill(); + ctx.fillStyle = 'rgba(255,179,0,1)'; ctx.textAlign = 'left'; + ctx.fillText(label, bx + 7, by + 12); + } + + // ====== Animation ====== + function startChartAnimation() { + var last = 0; + function frame(ts) { + requestAnimationFrame(frame); + if (ts - last < 33) { return; } + last = ts; + gaugeShown += (gaugeTarget - gaugeShown) * 0.18; + if (Math.abs(gaugeTarget - gaugeShown) < 0.5) { gaugeShown = gaugeTarget; } + drawGauge(gaugeShown); + drawPowerChart(); + drawDailyChart(); + drawWaterChart(); + } + requestAnimationFrame(frame); + } + + // ====== Schreiben per Klick (Event-Delegation) ====== + function parseVal(s) { + if (s === 'true') { return true; } + if (s === 'false') { return false; } + if (/^-?\d+(\.\d+)?$/.test(s)) { return Number(s); } + return s; + } + + function flash(el, r) { + if (!el) { return; } + var cls = (r && !r.error) ? 'v4-flash-ok' : 'v4-flash-err'; + el.classList.remove('v4-flash-ok', 'v4-flash-err'); + void el.offsetWidth; + el.classList.add(cls); + setTimeout(function () { el.classList.remove(cls); }, 600); + } + + function onClick(ev) { + var btn = ev.target.closest ? ev.target.closest('button') : null; + // Mehrfach-Schreibvorgang + var ml = ev.target.closest ? ev.target.closest('[data-vars]') : null; + if (ml && ml.hasAttribute('data-value')) { + var vars = ml.getAttribute('data-vars').split(',').map(function (s) { return s.trim(); }); + var val = parseVal(ml.getAttribute('data-value')); + var results = []; + Promise.all(vars.map(function (v) { + return write(v, val).then(function (r) { results.push(r); return r; }); + })).then(function () { + var allOk = results.every(function (r) { return r && !r.error; }); + flash(btn, allOk ? { error: false } : { error: true }); + toast('Schnellbefehl ausgeführt', allOk ? 'ok' : 'err'); + }); + return; + } + // Einzel-Schreibvorgang + var el = ev.target.closest ? ev.target.closest('[data-var]') : null; + if (el && el.hasAttribute('data-value')) { + write(el.getAttribute('data-var'), parseVal(el.getAttribute('data-value'))).then(function (r) { flash(btn, r); }); + return; + } + // Heizung: Stepper (+/-) – Wert um 0.5°C aendern und schreiben + var hs = ev.target.closest ? ev.target.closest('[data-heat-step]') : null; + if (hs) { + var hI = parseInt(hs.getAttribute('data-heat-i'), 10); + var hStep = hs.getAttribute('data-heat-step'); + var hDir = parseInt(hs.getAttribute('data-heat-dir'), 10); + var hObj = HEAT[hI - 1]; + if (!hObj) { return; } + var cur = num(lastValues[hObj[hStep]]); + if (cur === null) { cur = 20; } + var newVal = Math.round((cur + hDir * 0.5) * 10) / 10; + if (!heatPlausible(hI, hStep, newVal)) { return; } + write(hObj[hStep], newVal).then(function (r) { + flash(btn, r); + toast(hObj.name + ' ' + hStep + ': ' + newVal.toFixed(1) + '°C', r && !r.error ? 'ok' : 'err'); + setTimeout(refreshSettings, 300); + }); + return; + } + // Poolpumpe + var pm = ev.target.closest ? ev.target.closest('[data-pool-max]') : null; + if (pm) { + var pInput = document.getElementById(pm.getAttribute('data-write-input')); + if (!pInput || pInput.value === '') { return; } + var pMin = parseFloat(pInput.value); + if (isNaN(pMin) || pMin < 0) { toast('Bitte eine gültige Zahl eingeben.', 'err'); return; } + write(pm.getAttribute('data-var'), Math.round(pMin * 60)).then(function (r) { + flash(btn, r); + toast('Poolpumpe SOLL gesetzt', r && !r.error ? 'ok' : 'err'); + readMany([POOL.max]).then(function (m) { merge(m); render(lastValues); }).catch(function () {}); + }); + return; + } + // WLED + var wl = ev.target.closest ? ev.target.closest('[data-wled]') : null; + if (wl) { + var kind = wl.getAttribute('data-wled'); + if (kind === 'bright') { + var b = document.getElementById('wled_bright'); + write('"wled_DB".Helligkeit', parseInt(b.value, 10)).then(function () { return write('"wled_DB".Befehl', 4); }).then(function (r) { flash(btn, r); toast('WLED Helligkeit', r && !r.error ? 'ok' : 'err'); }); + } else if (kind === 'color') { + var c = document.getElementById('wled_color'); + write('"wled_DB".RGB_hex', c.value).then(function () { return write('"wled_DB".Befehl', 5); }).then(function (r) { flash(btn, r); toast('WLED Farbe', r && !r.error ? 'ok' : 'err'); }); + } + return; + } + } + + // ====== Polling ====== + var FAST_VARS = [METER.leist]; + var ALL_VARS = null; + var SET_VARS = null; + var SLOW_EVERY = 4; + var tickCount = 0; + + function merge(m) { Object.keys(m).forEach(function (k) { lastValues[k] = m[k]; }); } + + function refreshSettings() { + readMany(SET_VARS).then(function (m) { merge(m); render(lastValues); }).catch(function () {}); + } + + function tick() { + if (pollBusy) { return; } // Mutex: kein überlappender Request + pollBusy = true; + tickCount++; + var isFull = (tickCount % SLOW_EVERY === 1); + var vars = isFull ? ALL_VARS : FAST_VARS; + readMany(vars).then(function (m) { + resetBackoff(); + merge(m); + render(lastValues); + setStatus(true, 'ONLINE'); + // readWemos nur jeden 10. vollen Zyklus, um Request-Last zu reduzieren + if (isFull) { + wemosCounter++; + if (wemosCounter >= 10) { + wemosCounter = 0; + readWemos(); + } + } + pollBusy = false; + }).catch(function () { + increaseBackoff(); // exponentieller Backoff bei Fehler + setStatus(false, 'RECONNECT IN ' + Math.round(backoffMs / 1000) + 's'); + pollBusy = false; + }); + } + + // Polling mit dynamischem Intervall (Backoff) + function scheduleNext() { + setTimeout(function () { + tick(); + scheduleNext(); + }, backoffMs); + } + + // ====== Scanline-Toggle ====== + var scanEl = document.getElementById('v4_scanlines'); + var scanBtn = document.getElementById('scan_toggle'); + if (scanBtn && scanEl) { + scanBtn.addEventListener('click', function () { + var on = scanEl.classList.toggle('v4-scanlines--on'); + try { localStorage.setItem('v4_scanlines', on ? '1' : '0'); } catch (e) {} + }); + try { + if (localStorage.getItem('v4_scanlines') === '1') { + scanEl.classList.add('v4-scanlines--on'); + } + } catch (e) {} + } + + // ====== Start ====== + document.addEventListener('click', onClick); + buildLightTiles(); + buildRolladen(); + buildSockets(); + buildHeat(); + + ALL_VARS = allReadVars(); + SET_VARS = settingsVars(); + + if (PREVIEW) { + // Sofort Dummy-Daten laden, kein Login/Polling + var pv = makePreviewValues(); + Object.keys(pv).forEach(function (k) { lastValues[k] = pv[k]; }); + render(lastValues); + setStatus(true, 'PREVIEW'); + setText('sz_wemos', 'PREVIEW MODUS – keine SPS'); + // Dummy-Leistung zyklisch variieren + setInterval(function () { + lastValues[METER.leist] = 300 + Math.sin(Date.now() / 2000) * 1500; + render(lastValues); + }, 500); + runBoot(); + startChartAnimation(); + return; + } + + runBoot(); + startChartAnimation(); + + // Erstes volles Lesen, dann zyklisch + readMany(ALL_VARS).then(function (m) { + merge(m); + render(lastValues); + setStatus(true, 'ONLINE'); + readWemos(); + // Heizungs-SOLL min/max einmalig beim Start lesen (werden sonst nie angezeigt) + refreshSettings(); + }).catch(function () { + setStatus(false, 'OFFLINE'); + }); + scheduleNext(); // dynamisches Polling mit Backoff statt setInterval +})(); diff --git a/WebAppConfig.json b/WebAppConfig.json new file mode 100644 index 0000000..30719f3 --- /dev/null +++ b/WebAppConfig.json @@ -0,0 +1 @@ +{"directoriesToIgnoreForUpload":[],"resourcesToIgnoreForUpload":["indexV2.htm","indexV3_preview.htm","zeit_state.html","licht_state.html","heizung_state.html","steckdosen_state.html","stromzaehler_state.html","form.css","Script/tth_script.js"],"fileExtensionsToIgnoreForUpload":[],"name":"web","state":1,"type":1,"redirect_mode":0,"default_page":"indexV3.htm","not_found_page":"indexV3.htm","not_authorized_page":"indexV3.htm","protectedResources":[],"pathToWebAppDirectory":"C:\\Users\\Tobias Thebille\\OneDrive\\SPS\\Haus\\HausV2\\web"} \ No newline at end of file diff --git a/form.css b/form.css new file mode 100644 index 0000000..0d3045d --- /dev/null +++ b/form.css @@ -0,0 +1,245 @@ + /* CSS layout */ +body { + margin: 0; + padding: 0; +} + +#masthead { +} + +#top_nav { +} + +#container { + min-width: 600px; +} + +#left_col { + width: 200px; + float: left; +} + +#page_content { + margin-left: 10px; +} + +#footer { + clear: both; +} + + + + + +.onoffswitch { + position: relative; width: 90px; + -webkit-user-select:none; -moz-user-select:none; -ms-user-select: none; +} +.onoffswitch-checkbox { + display: none; +} +.onoffswitch-label { + display: block; overflow: hidden; cursor: pointer; + border: 2px solid #999999; border-radius: 20px; +} +.onoffswitch-inner { + display: block; width: 200%; margin-left: -100%; + transition: margin 0.3s ease-in 0s; +} +.onoffswitch-inner:before, .onoffswitch-inner:after { + display: block; float: left; width: 50%; height: 30px; padding: 0; line-height: 30px; + font-size: 14px; color: white; font-family: Trebuchet, Arial, sans-serif; font-weight: bold; + box-sizing: border-box; +} +.onoffswitch-inner:before { + content: "ON"; + padding-left: 10px; + background-color: #34A7C1; color: #FFFFFF; +} +.onoffswitch-inner:after { + content: "OFF"; + padding-right: 10px; + background-color: #EEEEEE; color: #999999; + text-align: right; +} +.onoffswitch-switch { + display: block; width: 18px; margin: 6px; + background: #FFFFFF; + position: absolute; top: 0; bottom: 0; + right: 56px; + border: 2px solid #999999; border-radius: 20px; + transition: all 0.3s ease-in 0s; +} +.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-inner { + margin-left: 0; +} +.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-switch { + right: 0px; +} + +/* ---- Modern UI additions (non-breaking) ---- */ +:root{ + --ui-bg: #070A10; + --ui-surface: rgba(255,255,255,0.06); + --ui-surface-2: rgba(255,255,255,0.10); + --ui-border: rgba(255,255,255,0.14); + --ui-text: #EAF0FF; + --ui-muted: rgba(234,240,255,0.70); + --ui-accent: #63D4FF; + --ui-accent-2: #7CFFB2; + --ui-danger: #FF5C7A; + --ui-shadow: 0 10px 30px rgba(0,0,0,0.45); + --ui-radius: 14px; + --ui-radius-sm: 10px; + --ui-gap: 12px; +} + +html, body{ + font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; +} + +/* Keep inline body colors working, but improve readability */ +body{ + line-height: 1.35; +} + +h3{ + margin: 14px 0 10px 0; + letter-spacing: 0.2px; +} + +/* Card wrapper (optional) */ +.ui-card{ + background: var(--ui-surface); + border: 1px solid var(--ui-border); + border-radius: var(--ui-radius); + box-shadow: var(--ui-shadow); + padding: 14px; + backdrop-filter: blur(8px); + /* keep modules compact & left-aligned like original */ + display: inline-block; + max-width: calc(100vw - 24px); +} + +/* Tables */ +table{ + border-collapse: separate; + border-spacing: 0; +} + +table.ui-table, table[border]{ + background: var(--ui-surface); + border: 1px solid var(--ui-border); + border-radius: var(--ui-radius); + overflow: hidden; +} + +table.ui-table td, table[border] td{ + border: 1px solid rgba(255,255,255,0.08); + padding: 10px 12px; + vertical-align: middle; +} + + +/* Links */ +a{ + color: var(--ui-accent); + text-decoration: none; +} +a:hover{ + text-decoration: underline; +} + +/* Buttons & inputs */ +button.ui-btn, +input.ui-btn, +input[type="submit"]{ + appearance: none; + border: 1px solid var(--ui-border); + background: rgba(255,255,255,0.08); + color: var(--ui-text); + border-radius: var(--ui-radius-sm); + padding: 10px 12px; + font-weight: 650; + letter-spacing: 0.2px; + cursor: pointer; + transition: transform 120ms ease, background 120ms ease, border-color 120ms ease; +} + +button.ui-btn:hover, +input.ui-btn:hover, +input[type="submit"]:hover{ + background: rgba(255,255,255,0.12); + border-color: rgba(255,255,255,0.22); + transform: translateY(-1px); +} + +button.ui-btn:active, +input.ui-btn:active, +input[type="submit"]:active{ + transform: translateY(0px); +} + +.ui-btn--primary{ + border-color: rgba(99,212,255,0.35); + background: rgba(99,212,255,0.12); +} +.ui-btn--on{ + border-color: rgba(124,255,178,0.35); + background: rgba(124,255,178,0.12); +} +.ui-btn--off{ + border-color: rgba(255,92,122,0.35); + background: rgba(255,92,122,0.10); +} + +/* Icon buttons (lamp etc.) */ +.ui-iconbtn{ + border: 1px solid var(--ui-border); + background: rgba(255,255,255,0.06); + border-radius: 999px; + padding: 8px; + cursor: pointer; +} +.ui-iconbtn:hover{ + background: rgba(255,255,255,0.10); +} +.ui-icon{ + display: block; +} + +/* Text inputs */ +input[type="text"], input[type="number"]{ + background: rgba(0,0,0,0.25); + color: var(--ui-text); + border: 1px solid var(--ui-border); + border-radius: 10px; + padding: 8px 10px; +} + +/* Range + color */ +input[type="range"]{ + width: 220px; + accent-color: var(--ui-accent); +} +input[type="color"]{ + width: 48px; + height: 36px; + padding: 0; + border: 1px solid var(--ui-border); + border-radius: 10px; + background: transparent; +} + +/* Iframes (index pages) */ +iframe{ + border: 0; + display: block; + background: transparent; +} + +/* Tiny status text coming from *_state.html */ +.auto-style6{ + color: transparent; + font-size: 1px; +} diff --git a/formV3.css b/formV3.css new file mode 100644 index 0000000..b51c96a --- /dev/null +++ b/formV3.css @@ -0,0 +1,762 @@ +:root { + --v3-bg: #070A10; + --v3-surface: rgba(255,255,255,0.06); + --v3-surface-2: rgba(255,255,255,0.10); + --v3-border: rgba(255,255,255,0.14); + --v3-border-hover: rgba(255,255,255,0.22); + --v3-text: #EAF0FF; + --v3-text-secondary: rgba(234,240,255,0.70); + --v3-accent: #63D4FF; + --v3-accent-2: #7CFFB2; + --v3-danger: #FF5C7A; + --v3-warn: #FFD963; + --v3-shadow: 0 10px 30px rgba(0,0,0,0.45); + --v3-radius: 14px; + --v3-radius-sm: 10px; + --v3-gap: 12px; +} + +* { + box-sizing: border-box; +} + +html, body { + margin: 0; + padding: 0; + font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif; + background: var(--v3-bg); + color: var(--v3-text); + line-height: 1.4; + min-height: 100vh; +} + +.v3-page { + max-width: 1400px; + margin: 0 auto; + padding: 16px; + display: flex; + flex-direction: column; + gap: 16px; +} + +/* ---- CARD ---- */ +.v3-card { + background: var(--v3-surface); + border: 1px solid var(--v3-border); + border-radius: var(--v3-radius); + box-shadow: var(--v3-shadow); + padding: 16px; + backdrop-filter: blur(8px); + transition: border-color 0.25s ease, box-shadow 0.25s ease; +} + +.v3-card:hover { + border-color: var(--v3-border-hover); +} + +.v3-card-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 1.1rem; + font-weight: 700; + margin-bottom: 14px; + color: var(--v3-text); + letter-spacing: 0.3px; +} + +.v3-card-title span:first-child { + font-size: 1.3rem; +} + +.v3-section-title { + font-size: 0.9rem; + font-weight: 600; + color: var(--v3-text-secondary); + margin-bottom: 8px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* ---- BUTTONS ---- */ +.v3-btn { + appearance: none; + border: 1px solid var(--v3-border); + background: var(--v3-surface-2); + color: var(--v3-text); + border-radius: var(--v3-radius-sm); + padding: 10px 16px; + font-weight: 600; + font-size: 0.9rem; + letter-spacing: 0.2px; + cursor: pointer; + transition: transform 0.12s ease, background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; + display: inline-flex; + align-items: center; + gap: 6px; +} + +.v3-btn:hover { + background: rgba(255,255,255,0.14); + border-color: var(--v3-border-hover); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0,0,0,0.3); +} + +.v3-btn:active { + transform: translateY(0); +} + +.v3-btn--sm { + padding: 6px 12px; + font-size: 0.8rem; +} + +.v3-btn--primary { + border-color: rgba(99,212,255,0.35); + background: rgba(99,212,255,0.12); + color: var(--v3-accent); +} + +.v3-btn--primary:hover { + background: rgba(99,212,255,0.22); + border-color: rgba(99,212,255,0.55); +} + +.v3-btn--on { + border-color: rgba(124,255,178,0.35); + background: rgba(124,255,178,0.12); + color: var(--v3-accent-2); +} + +.v3-btn--on:hover { + background: rgba(124,255,178,0.22); + border-color: rgba(124,255,178,0.55); +} + +.v3-btn--off { + border-color: rgba(255,92,122,0.35); + background: rgba(255,92,122,0.10); + color: var(--v3-danger); +} + +.v3-btn--off:hover { + background: rgba(255,92,122,0.20); + border-color: rgba(255,92,122,0.55); +} + +/* ---- STATUS DOT ---- */ +.v3-status-dot { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + background: rgba(255,255,255,0.2); + transition: background 0.3s ease, box-shadow 0.3s ease; + flex-shrink: 0; +} + +.v3-status-dot--on { + background: var(--v3-accent-2); + box-shadow: 0 0 8px rgba(124,255,178,0.6); +} + +.v3-status-dot--off { + background: rgba(255,255,255,0.2); +} + +.v3-status-dot--active { + background: var(--v3-warn); + box-shadow: 0 0 8px rgba(255,217,99,0.6); +} + +.v3-status { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 0.8rem; + color: var(--v3-text-secondary); +} + +.v3-title-values { + display: inline-flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + justify-content: flex-end; +} + +.v3-title-value { + font-size: 0.8rem; + font-weight: 600; + color: var(--v3-text); + background: var(--v3-surface-2); + border: 1px solid var(--v3-border); + border-radius: var(--v3-radius-sm); + padding: 4px 8px; + white-space: nowrap; +} + +#hdr_wtemp, +#hdr_wstand, +#hdr_wseit, +#hdr_sbezug, +#hdr_seinsp { + font-size: 0.8rem; +} + +/* ---- ROOM ROW (Licht) ---- */ +.v3-room-row { + background: var(--v3-surface); + border: 1px solid var(--v3-border); + border-radius: var(--v3-radius-sm); + padding: 12px 14px; + display: flex; + align-items: center; + justify-content: space-between; + transition: border-color 0.25s ease, box-shadow 0.25s ease, background 0.25s ease; +} + +.v3-room-row:hover { + border-color: var(--v3-border-hover); +} + +.v3-room-name { + font-weight: 600; + font-size: 0.9rem; +} + +.v3-room-controls { + display: flex; + align-items: center; + gap: 10px; +} + +/* ---- INFO ITEMS ---- */ +.v3-info-item { + display: flex; + flex-direction: column; + gap: 2px; +} + +.v3-info-label { + font-size: 0.7rem; + color: var(--v3-text-secondary); + text-transform: uppercase; + letter-spacing: 0.4px; +} + +.v3-info-value { + font-size: 1rem; + font-weight: 700; + color: var(--v3-text); +} + +/* ---- TEMP ---- */ +.v3-temp { + font-size: 1.4rem; + font-weight: 700; +} + +.v3-temp--normal { + color: var(--v3-text); +} + +.v3-temp--hot { + color: var(--v3-danger); +} + +.v3-temp--warm { + color: var(--v3-warn); +} + +/* ---- INPUTS ---- */ +.v3-input { + background: rgba(0,0,0,0.25); + color: var(--v3-text); + border: 1px solid var(--v3-border); + border-radius: var(--v3-radius-sm); + padding: 6px 10px; + font-size: 0.9rem; + width: 60px; + text-align: center; + transition: border-color 0.2s ease; +} + +.v3-input:focus { + outline: none; + border-color: var(--v3-accent); +} + +/* ---- SLIDER ---- */ +.v3-slider { + width: 180px; + accent-color: var(--v3-accent); +} + +/* ---- COLOR ---- */ +.v3-color { + width: 48px; + height: 36px; + padding: 0; + border: 1px solid var(--v3-border); + border-radius: var(--v3-radius-sm); + background: transparent; + cursor: pointer; +} + +/* ---- WLED ---- */ +.v3-wled { + display: flex; + flex-direction: column; + gap: 12px; +} + +.v3-wled-row { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.v3-wled-label { + font-size: 0.85rem; + color: var(--v3-text-secondary); + min-width: 80px; +} + +.v3-wled-val { + font-size: 0.85rem; + color: var(--v3-text); + font-weight: 600; + min-width: 50px; +} + +/* ---- GAUGE ---- */ +.v3-gauge-container { + display: flex; + justify-content: center; + align-items: center; +} + +#v3_gauge { + border-radius: var(--v3-radius); +} + +/* ---- CHART ---- */ +.v3-chart-container { + background: rgba(0,0,0,0.2); + border-radius: var(--v3-radius-sm); + padding: 8px; + overflow: hidden; +} + +#v3_power_chart { + width: 100%; + height: 200px; + display: block; +} + +/* ---- CLOCK ---- */ +.v3-clock { + text-align: center; + padding: 12px; +} + +.v3-clock-time { + font-size: 1.8rem; + font-weight: 700; + color: var(--v3-accent); + font-variant-numeric: tabular-nums; +} + +.v3-clock-date { + font-size: 0.9rem; + color: var(--v3-text-secondary); + margin-bottom: 8px; +} + +.v3-sun-row { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + margin-top: 6px; +} + +.v3-sun-row img { + vertical-align: middle; +} + +.v3-sun-text { + font-size: 0.9rem; + color: var(--v3-text-secondary); +} + +/* ---- IFRAME ---- */ +iframe { + border: 0; + display: block; + background: transparent; + width: 100%; +} + +/* ---- RESPONSIVE ---- */ +@media (max-width: 768px) { + .v3-page { + padding: 8px; + } + + .v3-card { + padding: 12px; + } + + .v3-btn { + padding: 8px 12px; + font-size: 0.8rem; + } + + .v3-wled-row { + flex-direction: column; + align-items: flex-start; + } +} + +/* ---- ANIMATIONS ---- */ +@keyframes pulse-glow { + 0%, 100% { box-shadow: 0 0 6px rgba(124,255,178,0.4); } + 50% { box-shadow: 0 0 14px rgba(124,255,178,0.7); } +} + +.v3-status-dot--on { + animation: pulse-glow 2s ease-in-out infinite; +} + +/* ---- SCROLLBAR ---- */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: var(--v3-bg); +} + +::-webkit-scrollbar-thumb { + background: var(--v3-border); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--v3-border-hover); +} + +/* ==================================================================== + indexV3 – Grid-Layout & Modul-Bausteine + ==================================================================== */ + +.v3-header { + display: flex; + align-items: baseline; + justify-content: space-between; + flex-wrap: wrap; + gap: 8px; +} + +.v3-header h1 { + font-size: 1.5rem; + font-weight: 700; + margin: 0; + letter-spacing: 0.3px; +} + +.v3-header .v3-sub { + font-size: 0.85rem; + color: var(--v3-text-secondary); +} + +/* Karten strikt untereinander (eine Spalte). Reihenfolge per order unten. */ +.v3-grid { + display: grid; + grid-template-columns: 1fr; + gap: 16px; + align-items: start; +} + +/* Gewuenschte Reihenfolge von oben nach unten: + Stromzaehler, Licht, Rolladen, Steckdosen, Heizung, Haus, Uhr. + Quell-Reihenfolge im HTML ist: + 1 Stromzaehler, 2 Licht, 3 Heizung, 4 Rolladen, 5 Steckdosen, 6 Haus, 7 Uhr. */ +.v3-grid > section:nth-child(1) { order: 1; } /* Stromzaehler */ +.v3-grid > section:nth-child(2) { order: 2; } /* Licht */ +.v3-grid > section:nth-child(3) { order: 5; } /* Heizung -> nach unten */ +.v3-grid > section:nth-child(4) { order: 3; } /* Rolladen */ +.v3-grid > section:nth-child(5) { order: 4; } /* Steckdosen */ +.v3-grid > section:nth-child(6) { order: 6; } /* Haus */ +.v3-grid > section:nth-child(7) { order: 7; } /* Uhr */ + +/* Karten, die mehr Platz brauchen (Heizung, Stromzähler) */ +.v3-card--wide { + grid-column: 1 / -1; +} + +.v3-rows { + display: flex; + flex-direction: column; + gap: 8px; +} + +.v3-muted { + color: var(--v3-text-secondary); + font-size: 0.8rem; + font-variant-numeric: tabular-nums; +} + +/* Verstecktes Schreib-Iframe (AWP-POST-Ziel) */ +.v3-sink { + position: fixed; + left: -9999px; + width: 0; + height: 0; + border: 0; +} + +/* Knopf-Paare (auf/ab, an/aus) */ +.v3-btn-group { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.v3-btn-group form { + margin: 0; +} + +/* ---- Heizungs-Zeile ---- */ +.v3-heat-row { + display: grid; + grid-template-columns: 1.3fr 0.9fr auto 0.9fr auto auto; + gap: 10px; + align-items: center; + background: var(--v3-surface); + border: 1px solid var(--v3-border); + border-radius: var(--v3-radius-sm); + padding: 10px 12px; + margin: 0; + transition: border-color 0.25s ease; +} + +.v3-heat-row:hover { + border-color: var(--v3-border-hover); +} + +.v3-heat-head { + display: grid; + grid-template-columns: 1.3fr 0.9fr auto 0.9fr auto auto; + gap: 10px; + padding: 0 12px 4px; + font-size: 0.65rem; + text-transform: uppercase; + letter-spacing: 0.4px; + color: var(--v3-text-secondary); +} + +/* Getrennte Schreib-Form je SOLL-Wert (min bzw. max) */ +.v3-heat-set { + display: flex; + align-items: center; + gap: 4px; + margin: 0; +} + +.v3-heat-set .v3-input { + width: 54px; +} + +/* ---- Zähler (Strom + Wasser) ---- */ +.v3-strom-layout { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-template-rows: auto auto; + gap: 16px; + justify-items: center; + align-items: start; +} + +.v3-strom-layout > .v3-gauge-container { + grid-column: 1; + grid-row: 1; +} + +.v3-strom-values { + grid-column: 1; + grid-row: 2; + display: flex; + flex-direction: row; + gap: 16px; + justify-content: center; + align-items: flex-start; + min-width: 140px; +} + +.v3-strom-layout > .v3-chart-container:nth-child(3) { + grid-column: 2; + grid-row: 1 / 3; + align-self: center; +} + +.v3-strom-layout > .v3-chart-container:nth-child(4) { + grid-column: 3; + grid-row: 1 / 3; + align-self: center; +} + +.v3-strom-values .v3-info-value { + font-size: 0.95rem; +} + +/* Wasserzähler */ +.v3-water-layout { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: auto; + gap: 16px; + justify-items: start; + align-items: start; +} + +.v3-water-layout > .v3-chart-container { + grid-column: 1; + grid-row: 1; + align-self: center; + justify-self: start; + width: fit-content; +} + +@media (max-width: 1100px) { + .v3-strom-layout, + .v3-water-layout { + grid-template-columns: 1fr; + grid-template-rows: auto; + } + + .v3-strom-layout > .v3-gauge-container, + .v3-strom-layout > .v3-strom-values, + .v3-strom-layout > .v3-chart-container:nth-child(3), + .v3-strom-layout > .v3-chart-container:nth-child(4), + .v3-water-layout > .v3-chart-container { + grid-column: 1; + grid-row: auto; + align-self: center; + } +} + +#v3_gauge, +#v3_power_chart, +#v3_daily_chart, +#v3_water_chart { + background: rgba(0, 0, 0, 0.25); + border-radius: var(--v3-radius-sm); + border: 1px solid var(--v3-border); + display: block; + width: 100%; + max-width: 360px; + height: auto; + aspect-ratio: 2 / 1; +} + +#v3_gauge { + border-radius: var(--v3-radius); + max-width: 360px; + aspect-ratio: 3 / 1; +} + +@media (max-width: 768px) { + .v3-heat-row, + .v3-heat-head { + grid-template-columns: 1fr auto auto; + grid-auto-rows: auto; + } + .v3-heat-head { display: none; } +} + +/* Sanfte Uebergaenge + Schalt-Feedback (V3) */ +.v3-temp { transition: color 0.3s ease; } + +@keyframes v3-flash-ok-anim { + from { box-shadow: 0 0 0 2px rgba(124, 255, 178, 0.85); } + to { box-shadow: 0 0 0 2px rgba(124, 255, 178, 0); } +} +@keyframes v3-flash-err-anim { + from { box-shadow: 0 0 0 2px rgba(255, 92, 122, 0.9); } + to { box-shadow: 0 0 0 2px rgba(255, 92, 122, 0); } +} +.v3-flash-ok { animation: v3-flash-ok-anim 0.6s ease-out; } +.v3-flash-err { animation: v3-flash-err-anim 0.6s ease-out; } + +/* ---- Poolpumpe Fortschrittsbalken (IST vs. SOLL Laufzeit) ---- */ +.v3-pool-row { + display: flex; + align-items: center; + gap: 10px; +} + +.v3-pool-bar { + position: relative; + flex: 1; + min-width: 0; + height: 18px; + background: rgba(255,255,255,0.08); + border: 1px solid var(--v3-border); + border-radius: 999px; + overflow: hidden; +} + +.v3-pool-bar-fill { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 0%; + background: linear-gradient(90deg, rgba(99,212,255,0.55), rgba(99,212,255,0.95)); + border-radius: 999px; + transition: width 0.6s ease; +} + +.v3-pool-bar-fill--done { + background: linear-gradient(90deg, rgba(124,255,178,0.55), rgba(124,255,178,0.95)); + box-shadow: 0 0 10px rgba(124,255,178,0.5); +} + +.v3-pool-bar-label { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.72rem; + font-weight: 600; + color: var(--v3-text); + text-shadow: 0 1px 3px rgba(0,0,0,0.8); + white-space: nowrap; + pointer-events: none; +} + +.v3-pool-set { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; +} + +.v3-pool-set-label { + font-size: 0.75rem; + color: var(--v3-text-secondary); + white-space: nowrap; +} + +.v3-pool-set .v3-input { + width: 80px; +} diff --git a/formV4.css b/formV4.css new file mode 100644 index 0000000..8d3dd4d --- /dev/null +++ b/formV4.css @@ -0,0 +1,842 @@ +:root { + --v4-bg: #02040A; + --v4-surface: rgba(0, 20, 40, 0.35); + --v4-surface-2: rgba(0, 30, 60, 0.5); + --v4-border: rgba(0, 229, 255, 0.25); + --v4-border-hover: rgba(0, 229, 255, 0.55); + --v4-text: #E6F7FF; + --v4-text-secondary: rgba(230, 247, 255, 0.6); + --v4-cyan: #00E5FF; + --v4-green: #00FF9C; + --v4-amber: #FFB300; + --v4-red: #FF3B5C; + --v4-mono: 'Consolas', 'JetBrains Mono', 'Courier New', monospace; + --v4-sans: 'Segoe UI', system-ui, -apple-system, Arial, sans-serif; + --v4-radius: 10px; + --v4-glow-cyan: 0 0 12px rgba(0, 229, 255, 0.35); + --v4-glow-green: 0 0 12px rgba(0, 255, 156, 0.35); + --v4-glow-amber: 0 0 12px rgba(255, 179, 0, 0.35); + --v4-glow-red: 0 0 12px rgba(255, 59, 92, 0.35); +} + +* { box-sizing: border-box; } + +html, body { + margin: 0; + padding: 0; + font-family: var(--v4-sans); + background: var(--v4-bg); + color: var(--v4-text); + min-height: 100vh; +} + +/* ===== Sternenfeld ===== */ +body::before { + content: ''; + position: fixed; + inset: 0; + background-image: + radial-gradient(1px 1px at 20% 30%, rgba(255,255,255,0.4) 50%, transparent 50%), + radial-gradient(1px 1px at 70% 15%, rgba(255,255,255,0.3) 50%, transparent 50%), + radial-gradient(1.5px 1.5px at 40% 70%, rgba(255,255,255,0.25) 50%, transparent 50%), + radial-gradient(1px 1px at 85% 60%, rgba(255,255,255,0.35) 50%, transparent 50%), + radial-gradient(1px 1px at 10% 80%, rgba(255,255,255,0.3) 50%, transparent 50%), + radial-gradient(1.5px 1.5px at 55% 40%, rgba(255,255,255,0.2) 50%, transparent 50%), + radial-gradient(1px 1px at 90% 90%, rgba(255,255,255,0.3) 50%, transparent 50%), + radial-gradient(1px 1px at 30% 10%, rgba(255,255,255,0.4) 50%, transparent 50%); + pointer-events: none; + z-index: 0; +} + +/* ===== Scanlines ===== */ +.v4-scanlines { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 9999; + background: repeating-linear-gradient( + 0deg, + rgba(0, 0, 0, 0.12) 0px, + rgba(0, 0, 0, 0.12) 1px, + transparent 1px, + transparent 3px + ); + opacity: 0; + transition: opacity 0.3s ease; +} +.v4-scanlines.v4-scanlines--on { opacity: 1; } + +/* ===== Boot-Sequence ===== */ +.v4-boot { + position: fixed; + inset: 0; + background: var(--v4-bg); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + transition: opacity 0.6s ease; +} +.v4-boot.v4-boot--hidden { opacity: 0; pointer-events: none; } +.v4-boot-inner { + width: 420px; + max-width: 90vw; + border: 1px solid var(--v4-border); + border-radius: var(--v4-radius); + padding: 24px; + background: var(--v4-surface); + box-shadow: var(--v4-glow-cyan); +} +.v4-boot-logo { + text-align: center; + font-family: var(--v4-mono); + font-size: 1.1rem; + color: var(--v4-cyan); + text-shadow: var(--v4-glow-cyan); + margin-bottom: 20px; + letter-spacing: 2px; +} +.v4-boot-line { + display: flex; + justify-content: space-between; + font-family: var(--v4-mono); + font-size: 0.85rem; + color: var(--v4-text-secondary); + padding: 4px 0; +} +.v4-boot-status { color: var(--v4-amber); } +.v4-boot-status.v4-boot-ok { color: var(--v4-green); } +.v4-boot-bar { + height: 6px; + background: rgba(255,255,255,0.08); + border-radius: 3px; + margin-top: 16px; + overflow: hidden; +} +.v4-boot-bar-fill { + height: 100%; + width: 0%; + background: linear-gradient(90deg, var(--v4-cyan), var(--v4-green)); + box-shadow: var(--v4-glow-cyan); + transition: width 0.3s ease; +} +.v4-boot-done { + text-align: center; + font-family: var(--v4-mono); + font-size: 1rem; + color: var(--v4-green); + text-shadow: var(--v4-glow-green); + margin-top: 16px; + letter-spacing: 3px; + opacity: 0; + transition: opacity 0.4s ease; +} +.v4-boot-done.v4-boot-done--show { opacity: 1; } + +/* ===== Seite ===== */ +.v4-page { + position: relative; + z-index: 1; + max-width: 1500px; + margin: 0 auto; + padding: 12px; + display: flex; + flex-direction: column; + gap: 12px; +} + +/* ===== Header ===== */ +.v4-header { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 10px; + padding: 10px 16px; + background: var(--v4-surface); + border: 1px solid var(--v4-border); + border-radius: var(--v4-radius); + box-shadow: var(--v4-glow-cyan); +} +.v4-header-logo { + font-family: var(--v4-mono); + font-size: 1.2rem; + font-weight: 700; + color: var(--v4-cyan); + text-shadow: var(--v4-glow-cyan); + letter-spacing: 2px; +} +.v4-header-clock { display: flex; align-items: baseline; gap: 10px; } +.v4-clock-time { + font-family: var(--v4-mono); + font-size: 1.6rem; + font-weight: 700; + color: var(--v4-cyan); + text-shadow: var(--v4-glow-cyan); + font-variant-numeric: tabular-nums; +} +.v4-clock-date { font-size: 0.85rem; color: var(--v4-text-secondary); } +.v4-header-sun { display: flex; gap: 12px; } +.v4-sun-item { + font-family: var(--v4-mono); + font-size: 0.9rem; + color: var(--v4-amber); + text-shadow: var(--v4-glow-amber); +} +.v4-header-status { display: flex; align-items: center; gap: 8px; } +.v4-status-orb { + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--v4-red); + box-shadow: var(--v4-glow-red); + transition: background 0.3s ease, box-shadow 0.3s ease; +} +.v4-status-orb.v4-status-orb--on { + background: var(--v4-green); + box-shadow: var(--v4-glow-green); + animation: v4-pulse 2s ease-in-out infinite; +} +.v4-status-text { font-size: 0.8rem; color: var(--v4-text-secondary); } + +/* ===== Grid ===== */ +.v4-grid { + display: grid; + grid-template-columns: 1fr 1.2fr 1fr; + gap: 12px; + align-items: start; +} + +/* ===== Zone ===== */ +.v4-zone { + background: var(--v4-surface); + border: 1px solid var(--v4-border); + border-radius: var(--v4-radius); + padding: 14px; + position: relative; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.4); +} +/* Eck-Clips (Cockpit-Look) */ +.v4-zone::before, +.v4-zone::after { + content: ''; + position: absolute; + width: 14px; + height: 14px; + border: 2px solid var(--v4-cyan); + opacity: 0.6; +} +.v4-zone::before { top: -1px; left: -1px; border-right: none; border-bottom: none; } +.v4-zone::after { bottom: -1px; right: -1px; border-left: none; border-top: none; } + +.v4-zone-title { + display: flex; + align-items: center; + gap: 8px; + font-family: var(--v4-mono); + font-size: 0.95rem; + font-weight: 700; + color: var(--v4-cyan); + text-shadow: var(--v4-glow-cyan); + letter-spacing: 2px; + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom: 1px solid var(--v4-border); +} +.v4-zone-icon { font-size: 1.1rem; } + +.v4-sub-title { + font-family: var(--v4-mono); + font-size: 0.75rem; + font-weight: 700; + color: var(--v4-text-secondary); + letter-spacing: 2px; + margin: 14px 0 8px; + text-transform: uppercase; +} + +/* ===== Buttons ===== */ +.v4-btn { + appearance: none; + border: 1px solid var(--v4-border); + background: var(--v4-surface-2); + color: var(--v4-text); + border-radius: 6px; + padding: 8px 14px; + font-weight: 600; + font-size: 0.85rem; + cursor: pointer; + transition: transform 0.12s ease, background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; + display: inline-flex; + align-items: center; + gap: 6px; +} +.v4-btn:hover { + background: rgba(0, 229, 255, 0.12); + border-color: var(--v4-border-hover); + transform: translateY(-1px); + box-shadow: var(--v4-glow-cyan); +} +.v4-btn:active { transform: translateY(0); } +.v4-btn--primary { + border-color: rgba(0, 229, 255, 0.4); + background: rgba(0, 229, 255, 0.1); + color: var(--v4-cyan); +} +.v4-btn--on { + border-color: rgba(0, 255, 156, 0.4); + background: rgba(0, 255, 156, 0.1); + color: var(--v4-green); +} +.v4-btn--off { + border-color: rgba(255, 59, 92, 0.4); + background: rgba(255, 59, 92, 0.1); + color: var(--v4-red); +} +.v4-btn--big { padding: 12px 18px; font-size: 0.95rem; } +.v4-btn--icon { padding: 6px 10px; font-size: 1rem; } + +/* ===== Flash-Feedback ===== */ +@keyframes v4-flash-ok-anim { + from { box-shadow: 0 0 0 2px rgba(0, 255, 156, 0.9); } + to { box-shadow: 0 0 0 2px rgba(0, 255, 156, 0); } +} +@keyframes v4-flash-err-anim { + from { box-shadow: 0 0 0 2px rgba(255, 59, 92, 0.9); } + to { box-shadow: 0 0 0 2px rgba(255, 59, 92, 0); } +} +.v4-flash-ok { animation: v4-flash-ok-anim 0.6s ease-out; } +.v4-flash-err { animation: v4-flash-err-anim 0.6s ease-out; } + +/* ===== Energie-Zone ===== */ +.v4-gauge-wrap { + display: flex; + justify-content: center; + margin-bottom: 10px; +} +#v4_gauge { + width: 100%; + max-width: 360px; + height: auto; + aspect-ratio: 3 / 2; + display: block; +} +.v4-energy-stats { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + margin-bottom: 12px; +} +.v4-stat { + background: var(--v4-surface-2); + border: 1px solid var(--v4-border); + border-radius: 6px; + padding: 8px; + text-align: center; +} +.v4-stat-label { + display: block; + font-size: 0.65rem; + color: var(--v4-text-secondary); + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 4px; +} +.v4-stat-value { + font-family: var(--v4-mono); + font-size: 0.95rem; + font-weight: 700; + color: var(--v4-text); +} +.v4-chart-wrap { + background: rgba(0, 0, 0, 0.3); + border: 1px solid var(--v4-border); + border-radius: 6px; + padding: 6px; + margin-bottom: 10px; + overflow: hidden; +} +#v4_power_chart, +#v4_daily_chart, +#v4_water_chart { + width: 100%; + height: auto; + aspect-ratio: 2 / 1; + display: block; +} +.v4-water { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; + margin-bottom: 10px; +} +.v4-water-item { + background: var(--v4-surface-2); + border: 1px solid var(--v4-border); + border-radius: 6px; + padding: 8px; + text-align: center; +} +.v4-water-label { + display: block; + font-size: 0.65rem; + color: var(--v4-text-secondary); + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 4px; +} +.v4-water-value { + font-family: var(--v4-mono); + font-size: 0.9rem; + font-weight: 700; + color: var(--v4-cyan); +} +.v4-wemos { + font-family: var(--v4-mono); + font-size: 0.8rem; + color: var(--v4-text-secondary); + text-align: center; + padding: 6px; + border: 1px dashed var(--v4-border); + border-radius: 6px; +} + +/* ===== Kommando-Zone: Licht-Tiles ===== */ +.v4-tiles { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; +} +.v4-tile { + background: var(--v4-surface-2); + border: 1px solid var(--v4-border); + border-radius: 8px; + padding: 12px 6px; + text-align: center; + cursor: pointer; + transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease, transform 0.1s ease; + position: relative; + overflow: hidden; +} +.v4-tile:hover { border-color: var(--v4-border-hover); } +.v4-tile:active { transform: scale(0.96); } +.v4-tile-name { + display: block; + font-size: 0.75rem; + font-weight: 600; + color: var(--v4-text-secondary); + margin-bottom: 6px; +} +.v4-tile-orb { + display: block; + width: 14px; + height: 14px; + margin: 0 auto; + border-radius: 50%; + background: rgba(255, 255, 255, 0.15); + transition: background 0.3s ease, box-shadow 0.3s ease; +} +.v4-tile--on { + background: rgba(0, 255, 156, 0.12); + border-color: rgba(0, 255, 156, 0.5); + box-shadow: var(--v4-glow-green); +} +.v4-tile--on .v4-tile-name { color: var(--v4-green); } +.v4-tile--on .v4-tile-orb { + background: var(--v4-green); + box-shadow: var(--v4-glow-green); + animation: v4-pulse 2s ease-in-out infinite; +} +/* Ripple */ +.v4-tile-ripple { + position: absolute; + border-radius: 50%; + background: rgba(0, 229, 255, 0.3); + transform: scale(0); + animation: v4-ripple 0.5s ease-out; + pointer-events: none; +} +@keyframes v4-ripple { + to { transform: scale(3); opacity: 0; } +} + +/* ===== WLED ===== */ +.v4-wled { display: flex; flex-direction: column; gap: 10px; } +.v4-wled-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } +.v4-wled-label { font-size: 0.8rem; color: var(--v4-text-secondary); min-width: 80px; } +.v4-slider { flex: 1; min-width: 120px; accent-color: var(--v4-cyan); } +.v4-color { width: 48px; height: 36px; padding: 0; border: 1px solid var(--v4-border); border-radius: 6px; background: transparent; cursor: pointer; } + +/* ===== Rolladen ===== */ +.v4-rolladen { display: flex; flex-direction: column; gap: 6px; } +.v4-roll-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + background: var(--v4-surface-2); + border: 1px solid var(--v4-border); + border-radius: 6px; + padding: 6px 10px; +} +.v4-roll-name { font-size: 0.8rem; font-weight: 600; } +.v4-roll-btns { display: flex; gap: 4px; } +.v4-roll-btns .v4-btn { padding: 4px 8px; font-size: 0.75rem; } + +/* ===== Steckdosen ===== */ +.v4-sockets { display: flex; flex-direction: column; gap: 6px; } +.v4-socket-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + background: var(--v4-surface-2); + border: 1px solid var(--v4-border); + border-radius: 6px; + padding: 6px 10px; +} +.v4-socket-name { font-size: 0.8rem; font-weight: 600; } +.v4-socket-orb { + width: 16px; + height: 16px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.15); + cursor: pointer; + transition: background 0.3s ease, box-shadow 0.3s ease; +} +.v4-socket-orb.v4-socket-orb--on { + background: var(--v4-green); + box-shadow: var(--v4-glow-green); + animation: v4-pulse 2s ease-in-out infinite; +} + +/* ===== Haus ===== */ +.v4-house { display: flex; flex-direction: column; gap: 8px; } + +/* ===== Funksteckdosen ===== */ +.v4-funk { display: flex; flex-direction: column; gap: 6px; } +.v4-funk-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + background: var(--v4-surface-2); + border: 1px solid var(--v4-border); + border-radius: 6px; + padding: 6px 10px; +} +.v4-funk-state { + background: rgba(0, 0, 0, 0.25); + border: 1px solid var(--v4-border); + border-radius: 6px; + padding: 8px; + margin-bottom: 10px; +} +.v4-funk-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 6px; +} +.v4-funk-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; + background: var(--v4-surface-2); + border: 1px solid var(--v4-border); + border-radius: 6px; + padding: 5px 8px; +} +.v4-funk-name { font-size: 0.75rem; font-weight: 600; color: var(--v4-text-secondary); } +.v4-funk-val { + font-family: var(--v4-mono); + font-size: 0.8rem; + font-weight: 700; + color: var(--v4-text); +} +.v4-funk-val.v4-funk-val--on { color: var(--v4-green); text-shadow: var(--v4-glow-green); } +.v4-funk-val.v4-funk-val--off { color: var(--v4-red); text-shadow: var(--v4-glow-red); } +.v4-btn-group { display: flex; gap: 4px; flex-wrap: wrap; } +.v4-funk-row .v4-btn { padding: 4px 8px; font-size: 0.75rem; } + +/* ===== Status-Zone: Heizung ===== */ +.v4-heat { display: flex; flex-direction: column; gap: 6px; } +.v4-heat-row { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: auto auto; + gap: 6px 12px; + align-items: center; + background: var(--v4-surface-2); + border: 1px solid var(--v4-border); + border-radius: 6px; + padding: 8px 10px; + transition: border-color 0.2s ease; +} +.v4-heat-row:hover { border-color: var(--v4-border-hover); } +.v4-heat-name { font-size: 0.8rem; font-weight: 600; grid-column: 1; grid-row: 1; } +.v4-heat-tempwrap { + grid-column: 2; + grid-row: 1; + justify-self: end; + display: flex; + align-items: center; + gap: 6px; +} +.v4-heat-temp { + font-family: var(--v4-mono); + font-size: 1.1rem; + font-weight: 700; + text-align: right; +} +.v4-heat-temp--normal { color: var(--v4-text); } +.v4-heat-temp--warm { color: var(--v4-amber); } +.v4-heat-temp--hot { color: var(--v4-red); } +.v4-heat-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.15); +} +.v4-heat-dot--on { + background: var(--v4-amber); + box-shadow: var(--v4-glow-amber); + animation: v4-pulse 1.5s ease-in-out infinite; +} +.v4-heat-bar-wrap { + grid-column: 1 / 3; + grid-row: 2; + height: 8px; + background: rgba(255, 255, 255, 0.08); + border-radius: 4px; + position: relative; + overflow: hidden; +} +.v4-heat-set { + display: flex; + align-items: center; + gap: 4px; +} +.v4-heat-set--min { grid-column: 1; grid-row: 3; } +.v4-heat-set--max { grid-column: 2; grid-row: 3; } +.v4-heat-input { width: 46px; padding: 4px 6px; font-size: 0.8rem; } +.v4-btn--sm { padding: 4px 8px; font-size: 0.75rem; } + +/* ===== Stepper (Drehschalter) ===== */ +.v4-stepper { + display: inline-flex; + align-items: center; + gap: 2px; + background: rgba(0, 0, 0, 0.3); + border: 1px solid var(--v4-border); + border-radius: 999px; + padding: 2px; +} +.v4-stepper-btn { + appearance: none; + border: none; + background: var(--v4-surface-2); + color: var(--v4-cyan); + width: 26px; + height: 26px; + border-radius: 50%; + font-size: 1.1rem; + font-weight: 700; + line-height: 1; + cursor: pointer; + transition: background 0.2s ease, box-shadow 0.2s ease, transform 0.1s ease; + display: inline-flex; + align-items: center; + justify-content: center; +} +.v4-stepper-btn:hover { + background: rgba(0, 229, 255, 0.2); + box-shadow: var(--v4-glow-cyan); +} +.v4-stepper-btn:active { transform: scale(0.9); } +.v4-stepper-val { + font-family: var(--v4-mono); + font-size: 0.85rem; + font-weight: 700; + color: var(--v4-text); + min-width: 48px; + text-align: center; + padding: 0 4px; +} +.v4-heat-bar { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 0%; + background: linear-gradient(90deg, var(--v4-cyan), var(--v4-amber), var(--v4-red)); + border-radius: 4px; + transition: width 0.5s ease; +} +.v4-heat-band { + position: absolute; + top: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.15); + border-left: 1px solid rgba(255, 255, 255, 0.3); + border-right: 1px solid rgba(255, 255, 255, 0.3); +} + +/* ===== Pool ===== */ +.v4-pool { display: flex; flex-direction: column; gap: 8px; } +.v4-pool-bar { + position: relative; + height: 20px; + background: rgba(255, 255, 255, 0.08); + border: 1px solid var(--v4-border); + border-radius: 999px; + overflow: hidden; +} +.v4-pool-bar-fill { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 0%; + background: linear-gradient(90deg, rgba(0, 229, 255, 0.5), var(--v4-cyan)); + border-radius: 999px; + box-shadow: var(--v4-glow-cyan); + transition: width 0.6s ease; +} +.v4-pool-bar-fill--done { + background: linear-gradient(90deg, rgba(0, 255, 156, 0.5), var(--v4-green)); + box-shadow: var(--v4-glow-green); +} +.v4-pool-bar-label { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + font-family: var(--v4-mono); + font-size: 0.75rem; + font-weight: 600; + color: var(--v4-text); + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8); + pointer-events: none; +} +.v4-pool-set { display: flex; align-items: center; gap: 8px; } +.v4-pool-set-label { font-size: 0.75rem; color: var(--v4-text-secondary); } +.v4-input { + background: rgba(0, 0, 0, 0.3); + color: var(--v4-text); + border: 1px solid var(--v4-border); + border-radius: 6px; + padding: 6px 10px; + font-size: 0.9rem; + width: 80px; + text-align: center; +} +.v4-input:focus { outline: none; border-color: var(--v4-cyan); } + +/* ===== Urlaub ===== */ +.v4-urlaub { + display: flex; + align-items: center; + gap: 10px; + background: var(--v4-surface-2); + border: 1px solid var(--v4-border); + border-radius: 6px; + padding: 10px; +} +.v4-urlaub-indicator { + width: 16px; + height: 16px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.15); + transition: background 0.3s ease, box-shadow 0.3s ease; +} +.v4-urlaub-indicator.v4-urlaub-indicator--on { + background: var(--v4-amber); + box-shadow: var(--v4-glow-amber); + animation: v4-pulse 1.5s ease-in-out infinite; +} +.v4-urlaub-text { + font-family: var(--v4-mono); + font-size: 0.9rem; + font-weight: 700; + color: var(--v4-text-secondary); + flex: 1; +} + +/* ===== Footer ===== */ +.v4-footer { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + padding: 10px 16px; + background: var(--v4-surface); + border: 1px solid var(--v4-border); + border-radius: var(--v4-radius); + box-shadow: var(--v4-glow-cyan); +} +.v4-footer-label { + font-family: var(--v4-mono); + font-size: 0.8rem; + color: var(--v4-cyan); + text-shadow: var(--v4-glow-cyan); + letter-spacing: 1px; +} + +/* ===== Toasts ===== */ +.v4-toasts { + position: fixed; + bottom: 20px; + right: 20px; + display: flex; + flex-direction: column; + gap: 8px; + z-index: 10001; +} +.v4-toast { + background: var(--v4-surface-2); + border: 1px solid var(--v4-border); + border-left: 3px solid var(--v4-cyan); + border-radius: 6px; + padding: 10px 14px; + font-size: 0.85rem; + color: var(--v4-text); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); + animation: v4-toast-in 0.3s ease; + max-width: 300px; +} +.v4-toast--ok { border-left-color: var(--v4-green); } +.v4-toast--err { border-left-color: var(--v4-red); } +@keyframes v4-toast-in { + from { transform: translateX(30px); opacity: 0; } + to { transform: translateX(0); opacity: 1; } +} +.v4-toast.v4-toast--out { animation: v4-toast-out 0.3s ease forwards; } +@keyframes v4-toast-out { + to { transform: translateX(30px); opacity: 0; } +} + +/* ===== Animationen ===== */ +@keyframes v4-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } +} + +/* ===== Responsive ===== */ +@media (max-width: 1100px) { + .v4-grid { grid-template-columns: 1fr; } +} +@media (max-width: 768px) { + .v4-page { padding: 8px; } + .v4-header { flex-direction: column; align-items: flex-start; } + .v4-energy-stats { grid-template-columns: repeat(2, 1fr); } + .v4-tiles { grid-template-columns: repeat(2, 1fr); } + .v4-water { grid-template-columns: 1fr; } +} + +/* ===== Scrollbar ===== */ +::-webkit-scrollbar { width: 8px; } +::-webkit-scrollbar-track { background: var(--v4-bg); } +::-webkit-scrollbar-thumb { background: var(--v4-border); border-radius: 4px; } +::-webkit-scrollbar-thumb:hover { background: var(--v4-border-hover); } \ No newline at end of file diff --git a/heizung.gif b/heizung.gif new file mode 100644 index 0000000..5f7996b Binary files /dev/null and b/heizung.gif differ diff --git a/heizungV2.htm b/heizungV2.htm new file mode 100644 index 0000000..2122c8a --- /dev/null +++ b/heizungV2.htm @@ -0,0 +1,304 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +TTh + + + + + + + + + + +
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Raumaktuell °CheiztIST °C minIST °C maxSOLL °C minSOLL °C max
Wohnzimmer:="Temp Wohnen":°C + :="Einstellungen".Heizung."Heizung Wohnen min"::="Einstellungen".Heizung."Heizung Wohnen max": + + +
Arbeitszimmer:="Temp Arbeiten":°C + :="Einstellungen".Heizung."Heizung Arbeiten min"::="Einstellungen".Heizung."Heizung Arbeiten max": + + +
HWR:="Temp HWR":°C + :="Einstellungen".Heizung."Heizung HWR min"::="Einstellungen".Heizung."Heizung HWR max": + + +
Bad oben:="Temp Bad":°C + :="Einstellungen".Heizung."Heizung Bad min"::="Einstellungen".Heizung."Heizung Bad max": + + +
Eltern:="Temp Eltern":°C + :="Einstellungen".Heizung."Heizung Eltern min"::="Einstellungen".Heizung."Heizung Eltern max": + + +
Noah:="Temp Noah":°C + :="Einstellungen".Heizung."Heizung Kind1 min"::="Einstellungen".Heizung."Heizung Kind1 max": + + +
Mika:="Temp Mika":°C + :="Einstellungen".Heizung."Heizung Kind2 min"::="Einstellungen".Heizung."Heizung Kind2 max": + + +
Niko:="Temp Niko":°C + :="Einstellungen".Heizung."Heizung Gäste min"::="Einstellungen".Heizung."Heizung Gäste max": + + +
Dusche Gäste:="Temp Dusche":°C + :="Einstellungen".Heizung."Heizung Dusche min"::="Einstellungen".Heizung."Heizung Dusche max": + + +
+
+
+
+ + + + + + +
+ + + + diff --git a/heizung_state.html b/heizung_state.html new file mode 100644 index 0000000..96bf12b --- /dev/null +++ b/heizung_state.html @@ -0,0 +1,37 @@ +

:="Heizung Essen Küche Wohn": +

:="Heizung Arbeiten": +

:="Heizung HWR": +

:="Heizung Bad": +

:="Heizung Eltern": +

:="Heizung Noah": +

:="Heizung Mika": +

:="Heizung Niko": +

:="Heizung Gäste Dusche": +

:="Temp Wohnen": +

:="Temp Arbeiten": +

:="Temp HWR": +

:="Temp Bad": +

:="Temp Eltern": +

:="Temp Noah": +

:="Temp Mika": +

:="Temp Niko": +

:="Temp Dusche": +

:="Einstellungen".Heizung."Heizung Wohnen min": +

:="Einstellungen".Heizung."Heizung Wohnen max": +

:="Einstellungen".Heizung."Heizung Arbeiten min": +

:="Einstellungen".Heizung."Heizung Arbeiten max": +

:="Einstellungen".Heizung."Heizung HWR min": +

:="Einstellungen".Heizung."Heizung HWR max": +

:="Einstellungen".Heizung."Heizung Bad min": +

:="Einstellungen".Heizung."Heizung Bad max": +

:="Einstellungen".Heizung."Heizung Eltern min": +

:="Einstellungen".Heizung."Heizung Eltern max": +

:="Einstellungen".Heizung."Heizung Kind1 min": +

:="Einstellungen".Heizung."Heizung Kind1 max": +

:="Einstellungen".Heizung."Heizung Kind2 min": +

:="Einstellungen".Heizung."Heizung Kind2 max": +

:="Einstellungen".Heizung."Heizung Gäste min": +

:="Einstellungen".Heizung."Heizung Gäste max": +

:="Einstellungen".Heizung."Heizung Dusche min": +

:="Einstellungen".Heizung."Heizung Dusche max": +

\ No newline at end of file diff --git a/home-icon-hi.png b/home-icon-hi.png new file mode 100644 index 0000000..ac6088f Binary files /dev/null and b/home-icon-hi.png differ diff --git a/images/FX_149.gif b/images/FX_149.gif new file mode 100644 index 0000000..7072e9e Binary files /dev/null and b/images/FX_149.gif differ diff --git a/images/aufgang.png b/images/aufgang.png new file mode 100644 index 0000000..9906224 Binary files /dev/null and b/images/aufgang.png differ diff --git a/images/blitz-warnzeichen.png b/images/blitz-warnzeichen.png new file mode 100644 index 0000000..3bb43f9 Binary files /dev/null and b/images/blitz-warnzeichen.png differ diff --git a/images/blitz0.jpg b/images/blitz0.jpg new file mode 100644 index 0000000..bba8941 Binary files /dev/null and b/images/blitz0.jpg differ diff --git a/images/blitz1.jpg b/images/blitz1.jpg new file mode 100644 index 0000000..ee93128 Binary files /dev/null and b/images/blitz1.jpg differ diff --git a/images/heizt0.png b/images/heizt0.png new file mode 100644 index 0000000..78143f5 Binary files /dev/null and b/images/heizt0.png differ diff --git a/images/heizt0_alt.png b/images/heizt0_alt.png new file mode 100644 index 0000000..3f0977a Binary files /dev/null and b/images/heizt0_alt.png differ diff --git a/images/heizt1.png b/images/heizt1.png new file mode 100644 index 0000000..a1ee8ff Binary files /dev/null and b/images/heizt1.png differ diff --git a/images/lampe0.jpg b/images/lampe0.jpg new file mode 100644 index 0000000..49f0f6e Binary files /dev/null and b/images/lampe0.jpg differ diff --git a/images/lampe1.jpg b/images/lampe1.jpg new file mode 100644 index 0000000..9f33d2f Binary files /dev/null and b/images/lampe1.jpg differ diff --git a/images/leuchtet0.png b/images/leuchtet0.png new file mode 100644 index 0000000..fd65493 Binary files /dev/null and b/images/leuchtet0.png differ diff --git a/images/leuchtet1.png b/images/leuchtet1.png new file mode 100644 index 0000000..83f8802 Binary files /dev/null and b/images/leuchtet1.png differ diff --git a/images/untergang.png b/images/untergang.png new file mode 100644 index 0000000..b88d02c Binary files /dev/null and b/images/untergang.png differ diff --git a/images/urlaub0.jpg b/images/urlaub0.jpg new file mode 100644 index 0000000..d8d50ca Binary files /dev/null and b/images/urlaub0.jpg differ diff --git a/images/urlaub1.jpg b/images/urlaub1.jpg new file mode 100644 index 0000000..d9bff1d Binary files /dev/null and b/images/urlaub1.jpg differ diff --git a/images/wasser0.jpg b/images/wasser0.jpg new file mode 100644 index 0000000..083ed45 Binary files /dev/null and b/images/wasser0.jpg differ diff --git a/images/wasser1.jpg b/images/wasser1.jpg new file mode 100644 index 0000000..e9d3c1d Binary files /dev/null and b/images/wasser1.jpg differ diff --git a/indexMika_89.htm b/indexMika_89.htm new file mode 100644 index 0000000..988acbe --- /dev/null +++ b/indexMika_89.htm @@ -0,0 +1,70 @@ + + + + + + + Mika – Zimmersteuerung + + + + +
+
+ + +
+
Stromzählerverbinde…
+
+
+ +
+
+
Bezug
+
Einspeisung
+
Wemos RC
+
+
+ +
+
+ +
+
+
+ + +
+
💡Licht Mika
+
+
+ Mika + + + + +
+
+
+ + +
+
🪟Rolladen Mika
+
+
+ Mika + + + + +
+
+
+ +
+
+ + + + + diff --git a/indexNiko_17.htm b/indexNiko_17.htm new file mode 100644 index 0000000..61b06fe --- /dev/null +++ b/indexNiko_17.htm @@ -0,0 +1,70 @@ + + + + + + + Niko – Zimmersteuerung + + + + +
+
+ + +
+
Stromzählerverbinde…
+
+
+ +
+
+
Bezug
+
Einspeisung
+
Wemos RC
+
+
+ +
+
+ +
+
+
+ + +
+
💡Licht Niko
+
+
+ Niko + + + + +
+
+
+ + +
+
🪟Rolladen Niko
+
+
+ Niko + + + + +
+
+
+ +
+
+ + + + + diff --git a/indexNoah_23.htm b/indexNoah_23.htm new file mode 100644 index 0000000..7775167 --- /dev/null +++ b/indexNoah_23.htm @@ -0,0 +1,70 @@ + + + + + + + Noah – Zimmersteuerung + + + + +
+
+ + +
+
Stromzählerverbinde…
+
+
+ +
+
+
Bezug
+
Einspeisung
+
Wemos RC
+
+
+ +
+
+ +
+
+
+ + +
+
💡Licht Noah
+
+
+ Noah + + + + +
+
+
+ + +
+
🪟Rolladen Noah
+
+
+ Noah + + + + +
+
+
+ +
+
+ + + + + diff --git a/indexV2.htm b/indexV2.htm new file mode 100644 index 0000000..fc475f9 --- /dev/null +++ b/indexV2.htm @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + +Haussteuerung + + + + + + +
+ + + + + + +

Haustür

+
+ + +
+ +

Garagentor

+
+ + +
+
+ + +
+ +
+

Haussteuerung

+

+ + :="Aufgang_Untergang".aktuelle_CPU_Zeit.DAY:-:="Aufgang_Untergang".aktuelle_CPU_Zeit.MONTH:-:="Aufgang_Untergang".aktuelle_CPU_Zeit.YEAR: + :="Aufgang_Untergang".aktuelle_CPU_Zeit.HOUR:::="Aufgang_Untergang".aktuelle_CPU_Zeit.MINUTE: + CPU Uhrzeit + +
+ + :="Aufgang_Untergang".Aufgang.HOUR:::="Aufgang_Untergang".Aufgang.MINUTE: Uhr + offset :="Aufgang_Untergang".Aufgang_offset: +
+ + :="Aufgang_Untergang".Untergang.HOUR:::="Aufgang_Untergang".Untergang.MINUTE: Uhr + offset :="Aufgang_Untergang".Untergang_offset: +

+
+
+ + + + + \ No newline at end of file diff --git a/indexV3.htm b/indexV3.htm new file mode 100644 index 0000000..507ae48 --- /dev/null +++ b/indexV3.htm @@ -0,0 +1,173 @@ + + + + + + + + + Haussteuerung V3 + + + + +
+ +
+ + +
+
+ ⚡💧 + Zähler + + + + + + + + verbinde… +
+ + +
Strom
+
+
+ +
+
+
Wemos RC
+
+
+ +
+
+ +
+
+ + +
Wasser
+
+
+ +
+
+
+ + +
+
💡Licht
+
+
Küche
+
Esszimmer
+
Wohnzimmer 1
+
Wohnzimmer 2
+
Arbeitszimmer
+
Hauswirtschaftsraum
+
Niko
+
Dusche
+
Abstellraum
+
Schlafzimmer
+
Noah
+
Badezimmer
+
Mika
+
Kickertaste
+
+
WLED Bad
+
+
+
Helligkeit
+
Farbe
+
+
+ + +
+
🔥Heizung
+
RaumAktuellheiztIST min/maxSOLL minSOLL max
+
+
Wohnzimmer–/–
+
Arbeitszimmer–/–
+
HWR–/–
+
Bad oben–/–
+
Eltern–/–
+
Noah–/–
+
Mika–/–
+
Niko–/–
+
Dusche Gäste–/–
+
+
+ + +
+
🪟Rollladen
+
+
alle Rollladen
+
Küche
+
Esszimmer
+
Wohnzimmer
+
Arbeitszimmer
+
Hauswirtschaftsraum
+
Niko
+
Dusche
+
Schlafzimmer
+
Noah
+
Badezimmer
+
Mika
+
Urlaub
+
+
+ + +
+
🔌Steckdosen
+
+
Deele
+
Wohnzimmer
+
Esszimmer
+
Arbeitszimmer
+
Noah
+
Mika
+
Niko
+
Eltern
+
Funksteckdosen
+
Funksteckdose 1
+
Saunalampe
+
KDY2
+
Poolpumpe
+
+
– / –
+
SOLL min
+
+
+
+ + +
+
🚪Haus
+
+
Haustür
+
Garagentor
+
+
+ + +
+
🕐Uhrzeit
+
+
--.--.----
+
--:--:--
+
Aufgang--:-- Uhr
+
Untergang--:-- Uhr
+
+
+ +
+
+ + + + + diff --git a/indexV4.htm b/indexV4.htm new file mode 100644 index 0000000..8dcb868 --- /dev/null +++ b/indexV4.htm @@ -0,0 +1,175 @@ + + + + + + HAUS-BRÜCKE V4 + + + + + +
+
+ +
VERBINDE ZUR SPS
+
LADE ENERGIE-MODUL
+
LADE KOMMANDO-MODUL
+
LADE STATUS-MODUL
+
+
SYSTEM ONLINE
+
+
+ + +
+ +
+ + +
+ +
+ --:--:-- + --.--.---- +
+
+ --:-- + --:-- +
+
+ + verbinde… + +
+
+ + +
+ + +
+
ENERGIE
+ +
+ +
+ +
+
Bezug
+
Einspeisung
+
Leistung
+
Heute
+
+ +
+ +
+
+ +
+ +
+ +
+
Wasser
+
Stand
+
seit Ablesung
+
+
+ +
+
+ + +
+
🎛 KOMMANDO
+ +
LICHT
+
+ +
WLED BAD
+
+
+ +
+
+ Helligkeit + + +
+
+ Farbe + + +
+
+ +
ROLLADEN
+
+
+ alle Rollladen + + + + + +
+
+
+ +
STECKDOSEN
+
+ +
FUNKSTECKDOSEN
+
+
Funksteckdose 1
+
Saunalampe
+
KDY2
+
Poolpumpe
+
+ +
HAUS
+
+ + + +
+
+ + +
+
📊 STATUS
+ +
HEIZUNG
+
+ +
POOLPUMPE
+
+
– / –
+
+ SOLL min + + +
+
+ +
URLAUB
+
+ + AUS + + +
+
+ +
+ +
+ + +
+ + + + \ No newline at end of file diff --git a/lampe.png b/lampe.png new file mode 100644 index 0000000..a3e71e8 Binary files /dev/null and b/lampe.png differ diff --git a/lichtV2.htm b/lichtV2.htm new file mode 100644 index 0000000..78f785b --- /dev/null +++ b/lichtV2.htm @@ -0,0 +1,305 @@ + + + + + + + + + + + + + + + + + + + + + + + +TTh + + + + + + + + + + +
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Küche +
+ +
+
Esszimmer +
+ +
+
Wohnzimmer +
+ +
+
+ +
+
Arbeitszimmer +
+ +
+
Hauswirtschaftsraum +
+ +
+
Niko +
+ +
+
Dusche +
+ +
+
Abstellraum +
+ +
+
Schalfzimmer +
+ +
+
Noah +
+ +
+
+ Badezimmer + +
+ +
+
+
+ +
+
+
+

+ +
+
+

+ +
Mika +
+ +
+
Kickertaste +
+ +
+
+
+
+
+ + +
+ + + + + + diff --git a/licht_state.html b/licht_state.html new file mode 100644 index 0000000..af65a0e --- /dev/null +++ b/licht_state.html @@ -0,0 +1,15 @@ +

:="Licht Küche": +

:="Licht Essen": +

:="Licht Wohnen 1": +

:="Licht Wohnen 2": +

:="Licht Arbeiten": +

:="Licht HWR": +

:="Licht Niko": +

:="Licht Dusche": +

:="Licht Abstellraum": +

:="Licht Eltern": +

:="Licht Noah": +

:="Licht Bad 2": +

:="Licht Kind 3": +

:="Licht Deele": +

\ No newline at end of file diff --git a/rolladen.htm b/rolladen.htm new file mode 100644 index 0000000..b722e72 --- /dev/null +++ b/rolladen.htm @@ -0,0 +1,337 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TTh + + + + + + + + + + +
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
alle Rolladen +
+ + +
+
+ + +
+
Küche +
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
Esszimmer +
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
Wohnzimmer +
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
Arbeitszimmer +
+ + +
+
+ + +
+
Hauswirtschaftsraum +
+ + +
+
+ + +
+
Niko +
+ + +
+
+ + +
+
Dusche +
+ + +
+
+ + +
+
Schlafzimmer +
+ + +
+
+ + +
+
Noah +
+ + +
+
+ + +
+
Badezimmer +
+ + +
+
+ + +
+
Mika +
+ + +
+
+ + +
+
Urlaub +
+ + +
+
+ + +
+
+ +
+
+
+
+ + +

:="Jalousie_DB".Urlaub: + +

+ + + + + + \ No newline at end of file diff --git a/steckdosenV2.htm b/steckdosenV2.htm new file mode 100644 index 0000000..a47f249 --- /dev/null +++ b/steckdosenV2.htm @@ -0,0 +1,307 @@ + + + + + + + + + + + + + + + + + + + + + + + +TTh + + + + + + + + + + +
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Deele +
+ + +
+
+ + +
+
+ +
Wohnzimmer +
+ + +
+
+ + +
+
+ +
Esszimmer +
+ + +
+
+ + +
+
+
+
Arbeitszimmer +
+ + +
+
+ + +
+
+ +
Steckdose Noah +
+ + +
+
+ + +
+
+ +
Steckdose Mika +
+ + +
+
+ + +
+
+ +
Steckdose Niko +
+ + +
+
+ + +
+
+ +
Steckdose Eltern +
+ + +
+
+ + +
+
+ +
Funksteckdose 1 +
+ + +
+
+ + +
+
Saunalampe +
+ + +
+
+ + +
+
KDY2 +
+ + +
+
+ + +
+
Poolpumpe +
+ + +
+
+ + +
+
+
+
+
+ + + + + + +
+ + + + \ No newline at end of file diff --git a/steckdosen_state.html b/steckdosen_state.html new file mode 100644 index 0000000..9a4bfa0 --- /dev/null +++ b/steckdosen_state.html @@ -0,0 +1,9 @@ +

:="Steckdose Deele Garder": +

:="Steckdose Wohnen": +

:="Steckdose Essen": +

:="Steckdose Arbeiten": +

:="Steckdose Kind 1": +

:="Steckdose Kind 2": +

:="Steckdose Gäste": +

:="Steckdose Eltern": +

\ No newline at end of file diff --git a/stromzaehler.htm b/stromzaehler.htm new file mode 100644 index 0000000..e00058a --- /dev/null +++ b/stromzaehler.htm @@ -0,0 +1,386 @@ + + + + + TTh + + + + + + + + +
+
+
+
+
+
+
+
+

+
+
+

+

+

+

+

+

+

+

+

+

+

+

+
+
+ +
+
+
+
+ + + + + + + + + + + + +
+ + + \ No newline at end of file diff --git a/stromzaehler_state.html b/stromzaehler_state.html new file mode 100644 index 0000000..035fa0d --- /dev/null +++ b/stromzaehler_state.html @@ -0,0 +1,12 @@ +

:="Stromzähler".Stromzähler."Zählerstand Bezug":

+

:="Stromzähler".Stromzähler."Zählerstand Einspeisung":

+

:="Stromzähler".Stromzähler."aktuelle Leistung":

+

:="Stromzähler".Stromzähler.Aufzeichnung."täglicher Bezug":

+

:="Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[0]."Zählerstand Bezug":

+

:="Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[1]."Zählerstand Bezug":

+

:="Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[2]."Zählerstand Bezug":

+

:="Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[3]."Zählerstand Bezug":

+

:="Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[4]."Zählerstand Bezug":

+

:="Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[5]."Zählerstand Bezug":

+

:="Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[6]."Zählerstand Bezug":

+

:="Stromzähler".Stromzähler.Aufzeichnung.Zählerstand[7]."Zählerstand Bezug":

\ No newline at end of file diff --git a/zeit_state.html b/zeit_state.html new file mode 100644 index 0000000..77cb483 --- /dev/null +++ b/zeit_state.html @@ -0,0 +1,14 @@ + +

:="Aufgang_Untergang".aktuelle_CPU_Zeit.YEAR:

+

:="Aufgang_Untergang".aktuelle_CPU_Zeit.MONTH:

+

:="Aufgang_Untergang".aktuelle_CPU_Zeit.DAY:

+

:="Aufgang_Untergang".aktuelle_CPU_Zeit.HOUR:

+

:="Aufgang_Untergang".aktuelle_CPU_Zeit.MINUTE:

+

:="Aufgang_Untergang".aktuelle_CPU_Zeit.SECOND:

+

:="Aufgang_Untergang".Aufgang.HOUR:

+

:="Aufgang_Untergang".Aufgang.MINUTE:

+

:="Aufgang_Untergang".Untergang.HOUR:

+

:="Aufgang_Untergang".Untergang.MINUTE:

+

:="Aufgang_Untergang".Aufgang_offset:

+

:="Aufgang_Untergang".Untergang_offset:

+

:="Jalousie_DB".Urlaub: