350 lines
12 KiB
JavaScript
350 lines
12 KiB
JavaScript
// ====================================================================
|
||
// 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();
|
||
}
|
||
})();
|