Files

350 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ====================================================================
// 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();
}
})();