Files

1035 lines
40 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_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] = '<span style="color:var(--v3-danger)"> ▲</span>'; }
else if (t < p - 0.1) { arrowState[h.i] = '<span style="color:var(--v3-accent)"> ▼</span>'; }
}
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();
}
})();