// ==================================================================== // 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(); } })();