Initial commit: Haussteuerung V2/V3 Web-Interface

This commit is contained in:
Thebille
2026-08-06 06:09:57 +02:00
commit 46c45518a3
46 changed files with 7285 additions and 0 deletions
+107
View File
@@ -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);
}
+349
View File
@@ -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();
}
})();
+1034
View File
File diff suppressed because it is too large Load Diff
+1320
View File
File diff suppressed because it is too large Load Diff