// ====================================================================
// tth_v4.js – indexV4.htm "HAUS-BRÜCKE" Kommandozentrale
//
// - Basiert auf der V3-Web-API-Logik (tth_v3_api.js)
// - Futuristisches HUD-Design, Boot-Sequence, Toasts, Touch-Tiles
// - Kommunikation mit der S7-1500 ueber /api/jsonrpc
// ====================================================================
(function () {
'use strict';
// Preview-Modus: window.V4_PREVIEW = true in indexV4_preview.htm gesetzt
var PREVIEW = !!(window.V4_PREVIEW);
// ====== KONFIG ======
var CFG = {
url: '/api/jsonrpc',
user: '111',
password: '1q2w3e$R',
readInterval: 500
};
var ANON = !CFG.user;
var token = null;
var rid = 0;
// ====== Boot-Sequence ======
var bootEls = {
sps: document.getElementById('boot_sps'),
energie: document.getElementById('boot_energie'),
kommando: document.getElementById('boot_kommando'),
status: document.getElementById('boot_status'),
bar: document.getElementById('boot_bar'),
done: document.getElementById('boot_done'),
wrap: document.getElementById('v4_boot')
};
function bootSet(el, text, ok) {
if (!el) { return; }
el.textContent = text;
el.classList.toggle('v4-boot-ok', !!ok);
}
function runBoot() {
var steps = [
{ el: bootEls.sps, label: 'OK' },
{ el: bootEls.energie, label: 'OK' },
{ el: bootEls.kommando, label: 'OK' },
{ el: bootEls.status, label: 'OK' }
];
var i = 0;
function next() {
if (i < steps.length) {
var s = steps[i];
bootSet(s.el, s.label, true);
bootEls.bar.style.width = ((i + 1) / steps.length * 100) + '%';
i++;
setTimeout(next, 350);
} else {
bootEls.bar.style.width = '100%';
bootEls.done.classList.add('v4-boot-done--show');
setTimeout(function () {
bootEls.wrap.classList.add('v4-boot--hidden');
setTimeout(function () { bootEls.wrap.style.display = 'none'; }, 600);
}, 600);
}
}
next();
}
// ====== Status ======
var statusOrb = document.getElementById('api_status_orb');
var statusText = document.getElementById('api_status');
function setStatus(connected, text) {
if (statusText) { statusText.textContent = text; }
if (statusOrb) { statusOrb.classList.toggle('v4-status-orb--on', connected); }
}
// ====== Toasts ======
var toastWrap = document.getElementById('v4_toasts');
function toast(msg, kind) {
if (!toastWrap) { return; }
var el = document.createElement('div');
el.className = 'v4-toast' + (kind === 'ok' ? ' v4-toast--ok' : kind === 'err' ? ' v4-toast--err' : '');
el.textContent = msg;
toastWrap.appendChild(el);
setTimeout(function () {
el.classList.add('v4-toast--out');
setTimeout(function () { el.remove(); }, 300);
}, 2500);
}
// ====== Preview: Dummy-Daten ======
function makePreviewValues() {
var m = {};
var now = new Date();
// Uhr
m[ZEIT.y] = now.getFullYear();
m[ZEIT.mo] = now.getMonth() + 1;
m[ZEIT.d] = now.getDate();
m[ZEIT.h] = now.getHours();
m[ZEIT.mi] = now.getMinutes();
m[ZEIT.s] = now.getSeconds();
m[ZEIT.aufH] = 6; m[ZEIT.aufMi] = 12;
m[ZEIT.untH] = 20; m[ZEIT.untMi] = 48;
m[ZEIT.urlaub] = false;
// Strom
m[METER.bezug] = 12543.2;
m[METER.einsp] = 8421.7;
m[METER.leist] = 300 + Math.sin(Date.now() / 2000) * 1500;
m[METER.tag] = 18.4;
var base = 12543.2;
for (var i = 0; i < 8; i++) {
base -= 15 + i * 2.3;
m[METER.hist[i]] = base;
}
// Wasser
m[WATER.stand] = 247300;
m[WATER.seit] = 120;
m[WATER.wtemp] = 18;
m[WATER.tag] = 160;
base = 247300;
for (var j = 0; j < 8; j++) {
base -= 130 + j * 20;
m[WATER.hist[j]] = base;
}
// Licht: jeder 2. an
LIGHTS.forEach(function (l, idx) { m[l[1]] = (idx % 3 === 0); });
// Heizung
HEAT.forEach(function (h) {
m[h.temp] = 19 + (h.i % 4) * 0.8;
m[h.act] = (h.i % 3 === 0);
m[h.min] = 19;
m[h.max] = 23;
});
// Steckdosen: false = AN
SOCKETS.forEach(function (s, idx) { m[s[1]] = (idx % 2 === 0) ? false : true; });
// Pool
m[POOL.sek] = 4 * 3600 + 12 * 60;
m[POOL.max] = 5 * 3600;
m[METER.wemos] = '';
return m;
}
// ====== Robustheit: Backoff, Mutex, Token-Reset ======
var pollBusy = false;
var backoffMs = CFG.readInterval;
var MAX_BACKOFF = 10000;
var wemosCounter = 0;
function resetBackoff() { backoffMs = CFG.readInterval; }
function increaseBackoff() { backoffMs = Math.min(MAX_BACKOFF, backoffMs * 2); }
// ====== Low-Level API ======
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) })
.catch(function (e) { token = null; throw e; }) // Netzwerkfehler -> Token verwerfen
.then(function (r) {
if (r.status === 401 || r.status === 403) { token = null; throw new Error('unauthorized'); }
return r.json().catch(function () { throw new Error('leere Antwort'); });
})
.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');
});
}
function withAuth(fn) {
if (ANON) { return fn(); }
return (token ? Promise.resolve() : login())
.then(fn)
.catch(function () { return login().then(fn); });
}
function readMany(vars) {
if (PREVIEW) {
var pv = makePreviewValues();
var out = {};
vars.forEach(function (v) { out[v] = pv[v] != null ? pv[v] : null; });
return Promise.resolve(out);
}
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;
});
});
}
function write(varName, value) {
varName = String(varName).trim();
if (PREVIEW) {
// Schreibvorgang simulieren: Zielvariable im Dummy-Speicher togglen
var key = varName;
var cur = lastValues[key];
if (typeof value === 'boolean' && typeof cur === 'boolean') {
lastValues[key] = value;
}
render(lastValues);
return Promise.resolve({ jsonrpc: '2.0', id: 0, result: true });
}
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, r.error);
}
return r;
}).catch(function (e) { console.warn('Write-Ausnahme', varName, e); });
}
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;
}
function readWemos() {
if (PREVIEW) {
setText('sz_wemos', 'PREVIEW MODUS – keine SPS');
return Promise.resolve();
}
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();
setText('sz_wemos', txt === '' ? '–' : txt);
}).catch(function () {});
}
// ====== Variablen-Zuordnung ======
var LIGHTS = [
['Küche', '"Licht Küche"', '"Lichtmerker Küche"'],
['Esszimmer', '"Licht Essen"', '"Lichtmerker Essen"'],
['Wohnen 1', '"Licht Wohnen 1"', '"Lichtmerker Wohnen 1"'],
['Wohnen 2', '"Licht Wohnen 2"', '"Lichtmerker Wohnen 2"'],
['Arbeiten', '"Licht Arbeiten"', '"Lichtmerker Arbeiten"'],
['HWR', '"Licht HWR"', '"Lichtmerker HWR"'],
['Niko', '"Licht Niko"', '"Lichtmerker Niko"'],
['Dusche', '"Licht Dusche"', '"Lichtmerker Dusche"'],
['Abstellraum', '"Licht Abstellraum"', '"Lichtmerker Abstellraum"'],
['Eltern', '"Licht Eltern"', '"Lichtmerker Eltern"'],
['Noah', '"Licht Noah"', '"Lichtmerker Noah"'],
['Bad', '"Licht Bad 2"', '"Lichtmerker Bad"'],
['Mika', '"Licht Kind 3"', '"Lichtmerker Kind 3"'],
['Kicker', '"Licht Deele"', '"Kickertaste"']
];
var HEAT = [
{ i: 1, name: 'Wohnzimmer', temp: '"Temp Wohnen"', act: '"Heizung Essen Küche Wohn"', min: '"Einstellungen".Heizung."Heizung Wohnen min"', max: '"Einstellungen".Heizung."Heizung Wohnen max"' },
{ i: 2, name: 'Arbeiten', temp: '"Temp Arbeiten"', act: '"Heizung Arbeiten"', min: '"Einstellungen".Heizung."Heizung Arbeiten min"', max: '"Einstellungen".Heizung."Heizung Arbeiten max"' },
{ i: 3, name: 'HWR', temp: '"Temp HWR"', act: '"Heizung HWR"', min: '"Einstellungen".Heizung."Heizung HWR min"', max: '"Einstellungen".Heizung."Heizung HWR max"' },
{ i: 4, name: 'Bad oben', temp: '"Temp Bad"', act: '"Heizung Bad"', min: '"Einstellungen".Heizung."Heizung Bad min"', max: '"Einstellungen".Heizung."Heizung Bad max"' },
{ i: 5, name: 'Eltern', temp: '"Temp Eltern"', act: '"Heizung Eltern"', min: '"Einstellungen".Heizung."Heizung Eltern min"', max: '"Einstellungen".Heizung."Heizung Eltern max"' },
{ i: 6, name: 'Noah', temp: '"Temp Noah"', act: '"Heizung Noah"', min: '"Einstellungen".Heizung."Heizung Kind1 min"', max: '"Einstellungen".Heizung."Heizung Kind1 max"' },
{ i: 7, name: 'Mika', temp: '"Temp Mika"', act: '"Heizung Mika"', min: '"Einstellungen".Heizung."Heizung Kind2 min"', max: '"Einstellungen".Heizung."Heizung Kind2 max"' },
{ i: 8, name: 'Niko', temp: '"Temp Niko"', act: '"Heizung Niko"', min: '"Einstellungen".Heizung."Heizung Gäste min"', max: '"Einstellungen".Heizung."Heizung Gäste max"' },
{ i: 9, name: 'Dusche', temp: '"Temp Dusche"', act: '"Heizung Gäste Dusche"', min: '"Einstellungen".Heizung."Heizung Dusche min"', max: '"Einstellungen".Heizung."Heizung Dusche max"' }
];
var SOCKETS = [
['Deele', '"Steckdose Deele Garder"'],
['Wohnzimmer', '"Steckdose Wohnen"'],
['Esszimmer', '"Steckdose Essen"'],
['Arbeiten', '"Steckdose Arbeiten"'],
['Noah', '"Steckdose Kind 1"'],
['Mika', '"Steckdose Kind 2"'],
['Niko', '"Steckdose Gäste"'],
['Eltern', '"Steckdose Eltern"']
];
var ROLLADEN = [
['Küche', '"Rolladenmerker Küc l auf"', '"Rolladenmerker Küc l ab"', '"Rolladenmerker Küc r auf"', '"Rolladenmerker Küc r ab"'],
['Esszimmer', '"Rolladenmerker Ess l auf"', '"Rolladenmerker Ess l ab"', '"Rolladenmerker Ess m auf"', '"Rolladenmerker Ess m ab"', '"Rolladenmerker Ess r auf"', '"Rolladenmerker Ess r ab"'],
['Wohnzimmer', '"Rolladenmerker Woh l auf"', '"Rolladenmerker Woh l ab"', '"Rolladenmerker Woh r auf"', '"Rolladenmerker Woh r ab"', '"Rolladenmerker Wohtü auf"', '"Rolladenmerker Wohtü ab"'],
['Arbeiten', '"Rolladenmerker Arbei auf"', '"Rolladenmerker Arbei ab"'],
['HWR', '"Rolladenmerker HWR auf"', '"Rolladenmerker HWR ab"'],
['Niko', '"Rolladenmerker Niko auf"', '"Rolladenmerker Niko ab"'],
['Dusche', '"Rolladenmerker Dusche au"', '"Rolladenmerker Dusche ab"'],
['Eltern', '"Rolladenmerker Elter auf"', '"Rolladenmerker Elter ab"'],
['Noah', '"Rolladenmerker Noah auf"', '"Rolladenmerker Noah ab"'],
['Bad', '"Rolladenmerker Bad auf"', '"Rolladenmerker Bad ab"'],
['Mika', '"Rolladenmerker Mika auf"', '"Rolladenmerker Mika ab"']
];
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'
};
function allReadVars() {
var v = [];
LIGHTS.forEach(function (l) { v.push(l[1]); });
HEAT.forEach(function (h) { v.push(h.temp, h.act); });
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;
}
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 tempClass(t) {
if (t === null) { return 'v4-heat-temp--normal'; }
if (t >= 24) { return 'v4-heat-temp--hot'; }
if (t >= 22) { return 'v4-heat-temp--warm'; }
return 'v4-heat-temp--normal';
}
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');
}
// ====== UI aufbauen ======
function buildLightTiles() {
var wrap = document.getElementById('v4_light_tiles');
if (!wrap) { return; }
LIGHTS.forEach(function (l, idx) {
var tile = document.createElement('div');
tile.className = 'v4-tile';
tile.id = 'li_tile_' + idx;
tile.innerHTML = '' + l[0] + '';
tile.addEventListener('click', function (ev) {
// Ripple
var rect = tile.getBoundingClientRect();
var r = document.createElement('span');
r.className = 'v4-tile-ripple';
var size = Math.max(rect.width, rect.height);
r.style.width = r.style.height = size + 'px';
r.style.left = (ev.clientX - rect.left - size / 2) + 'px';
r.style.top = (ev.clientY - rect.top - size / 2) + 'px';
tile.appendChild(r);
setTimeout(function () { r.remove(); }, 500);
write(l[2], true).then(function (res) {
flash(tile, res);
toast(l[0] + ': geschaltet', res && !res.error ? 'ok' : 'err');
});
});
wrap.appendChild(tile);
});
}
function buildRolladen() {
var wrap = document.getElementById('v4_rolladen');
if (!wrap) { return; }
ROLLADEN.forEach(function (r) {
var row = document.createElement('div');
row.className = 'v4-roll-row';
var name = document.createElement('span');
name.className = 'v4-roll-name';
name.textContent = r[0];
var btns = document.createElement('span');
btns.className = 'v4-roll-btns';
// auf/ab Paare
for (var i = 1; i < r.length; i += 2) {
var up = document.createElement('button');
up.className = 'v4-btn v4-btn--on';
up.type = 'button';
up.textContent = '↑';
up.title = 'hoch';
up.setAttribute('data-var', r[i]);
up.setAttribute('data-value', 'true');
var down = document.createElement('button');
down.className = 'v4-btn v4-btn--off';
down.type = 'button';
down.textContent = '↓';
down.title = 'runter';
down.setAttribute('data-var', r[i + 1]);
down.setAttribute('data-value', 'true');
btns.appendChild(up);
btns.appendChild(down);
}
row.appendChild(name);
row.appendChild(btns);
wrap.appendChild(row);
});
}
function buildSockets() {
var wrap = document.getElementById('v4_sockets');
if (!wrap) { return; }
SOCKETS.forEach(function (s, idx) {
var row = document.createElement('div');
row.className = 'v4-socket-row';
var name = document.createElement('span');
name.className = 'v4-socket-name';
name.textContent = s[0];
var orb = document.createElement('span');
orb.className = 'v4-socket-orb';
orb.id = 'sd_orb_' + idx;
orb.title = 'an/aus';
orb.addEventListener('click', function () {
// Status false/0 = AN, true/1 = AUS -> umschalten
var cur = lastValues[s[1]];
var isOn = (cur === false || cur === 0);
write(s[1], isOn ? true : false).then(function (res) {
flash(orb, res);
toast(s[0] + ': ' + (isOn ? 'AUS' : 'AN'), res && !res.error ? 'ok' : 'err');
});
});
row.appendChild(name);
row.appendChild(orb);
wrap.appendChild(row);
});
}
function buildHeat() {
var wrap = document.getElementById('v4_heat');
if (!wrap) { return; }
HEAT.forEach(function (h) {
var row = document.createElement('div');
row.className = 'v4-heat-row';
row.id = 'he_row_' + h.i;
row.innerHTML =
'' + h.name + '' +
'–' +
'' +
'' +
'–' +
'–';
wrap.appendChild(row);
});
}
// Plausibilitaet min < max ueber die ganze Heizungszeile
function heatPlausible(i, kind, newVal) {
var mn = num(lastValues[HEAT[i - 1].min]);
var mx = num(lastValues[HEAT[i - 1].max]);
if (kind === 'min') { mn = newVal; } else { mx = newVal; }
if (mn !== null && mx !== null && mn >= mx) {
toast('Min-Temperatur muss kleiner sein als Max.', 'err');
return false;
}
return true;
}
// ====== Rendern ======
var powerHistory = [];
var gaugeTarget = 0, gaugeShown = 0;
var prevTemps = {}, arrowState = {};
var lastValues = {};
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-Tiles
LIGHTS.forEach(function (l, idx) {
var tile = document.getElementById('li_tile_' + idx);
if (tile) { tile.classList.toggle('v4-tile--on', on(m[l[1]])); }
});
// 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] = ' ▲'; }
else if (t < p - 0.1) { arrowState[h.i] = ' ▼'; }
}
prevTemps[h.i] = t;
tEl.textContent = t.toFixed(1) + '°' + (arrowState[h.i] || '');
}
tEl.className = 'v4-heat-temp ' + tempClass(t);
}
var dot = document.getElementById('he_dot_' + h.i);
if (dot) { dot.classList.toggle('v4-heat-dot--on', on(m[h.act])); }
// SOLL-Stepper-Werte anzeigen
var minEl = document.getElementById('he_min_' + h.i);
var maxEl = document.getElementById('he_max_' + h.i);
var mnVal = num(m[h.min]), mxVal = num(m[h.max]);
if (minEl) { minEl.textContent = (mnVal === null ? '–' : mnVal.toFixed(1) + '°'); }
if (maxEl) { maxEl.textContent = (mxVal === null ? '–' : mxVal.toFixed(1) + '°'); }
// Temp-Bar: IST vs. SOLL-Band
var barWrap = document.getElementById('he_bar_' + h.i);
if (barWrap) {
barWrap.innerHTML = '';
var mn = num(m[h.min]), mx = num(m[h.max]);
if (t !== null && mn !== null && mx !== null && mx > mn) {
var lo = 10, hi = 30; // Skala 10..30°C
var pct = Math.max(0, Math.min(100, (t - lo) / (hi - lo) * 100));
var bar = document.createElement('div');
bar.className = 'v4-heat-bar';
bar.style.width = pct + '%';
barWrap.appendChild(bar);
var band = document.createElement('div');
band.className = 'v4-heat-band';
var bLo = Math.max(0, Math.min(100, (mn - lo) / (hi - lo) * 100));
var bHi = Math.max(0, Math.min(100, (mx - lo) / (hi - lo) * 100));
band.style.left = bLo + '%';
band.style.width = Math.max(2, bHi - bLo) + '%';
barWrap.appendChild(band);
}
}
});
// Steckdosen (Status false/0 = AN)
SOCKETS.forEach(function (s, idx) {
var orb = document.getElementById('sd_orb_' + idx);
if (orb) {
var val = m[s[1]];
var known = (val !== null && val !== undefined);
orb.classList.toggle('v4-socket-orb--on', known && (val === false || val === 0));
}
});
// Poolpumpe
var poolSek = num(m[POOL.sek]);
var poolMax = num(m[POOL.max]);
// SOLL min im Eingabefeld anzeigen (Sekunden -> Minuten)
var poolInput = document.getElementById('sd_pool_max_input');
if (poolInput && document.activeElement !== poolInput && poolMax !== null) {
poolInput.value = Math.round(poolMax / 60);
}
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('v4-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('v4-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');
addPowerSample(lei);
gaugeTarget = (lei === null ? 0 : lei);
// Wasser
var wStand = num(m[WATER.stand]), wSeit = num(m[WATER.seit]);
var wTemp = num(m[WATER.wtemp]);
setText('hdr_wtemp', (wTemp === null ? '–' : wTemp.toFixed(0) + ' °C'));
setText('hdr_wstand', (wStand === null ? '–' : wStand.toFixed(0) + ' L'));
setText('hdr_wseit', (wSeit === null ? '–' : wSeit.toFixed(0) + ' L'));
// 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]));
setText('sun_down', pad(m[ZEIT.untH]) + ':' + pad(m[ZEIT.untMi]));
var urlaub = on(m[ZEIT.urlaub]);
var udot = document.getElementById('roll_urlaub_dot');
if (udot) { udot.classList.toggle('v4-urlaub-indicator--on', urlaub); }
setText('roll_urlaub_text', urlaub ? 'AN' : 'AUS');
}
// ====== Canvas ======
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 };
}
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();
}
// HUD-Dial (Halbkreis-Tacho)
function drawGauge(value) {
var canvas = document.getElementById('v4_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 cx = w / 2;
var cy = h - 20;
var R = Math.min(w / 2 - 20, h - 30);
// Bogen-Hintergrund
ctx.strokeStyle = 'rgba(0, 229, 255, 0.15)';
ctx.lineWidth = 14;
ctx.beginPath();
ctx.arc(cx, cy, R, Math.PI, 0);
ctx.stroke();
// Skala
ctx.strokeStyle = 'rgba(0, 229, 255, 0.4)';
ctx.lineWidth = 1;
for (var t = -8000; t <= 8000; t += 1000) {
var ang = Math.PI + (t - MIN) / (MAX - MIN) * Math.PI;
var x1 = cx + Math.cos(ang) * (R - 8);
var y1 = cy + Math.sin(ang) * (R - 8);
var x2 = cx + Math.cos(ang) * (R + 8);
var y2 = cy + Math.sin(ang) * (R + 8);
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
}
// Aktiver Bogen (grün links = Einspeisung, rot rechts = Bezug)
var frac = (v - MIN) / (MAX - MIN);
var col = v < 0 ? '0,255,156' : '255,59,92';
ctx.save();
ctx.strokeStyle = 'rgba(' + col + ',0.9)';
ctx.lineWidth = 14;
ctx.shadowColor = 'rgba(' + col + ',0.6)';
ctx.shadowBlur = 12;
ctx.beginPath();
if (v >= 0) {
ctx.arc(cx, cy, R, Math.PI, Math.PI + frac * Math.PI);
} else {
ctx.arc(cx, cy, R, Math.PI + frac * Math.PI, 0);
}
ctx.stroke();
ctx.restore();
// Nadel
var angN = Math.PI + frac * Math.PI;
var nx = cx + Math.cos(angN) * (R - 20);
var ny = cy + Math.sin(angN) * (R - 20);
ctx.save();
ctx.strokeStyle = 'rgba(' + col + ',1)';
ctx.lineWidth = 2;
ctx.shadowColor = 'rgba(' + col + ',0.8)';
ctx.shadowBlur = 8;
ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(nx, ny); ctx.stroke();
ctx.restore();
// Digitalwert
ctx.textAlign = 'center';
ctx.fillStyle = 'rgba(' + col + ',1)';
ctx.font = 'bold 26px Consolas, monospace';
ctx.shadowColor = 'rgba(' + col + ',0.6)';
ctx.shadowBlur = 10;
ctx.fillText(Math.round(v) + ' W', cx, cy - R + 30);
ctx.shadowBlur = 0;
ctx.fillStyle = 'rgba(230,247,255,0.6)';
ctx.font = '12px Consolas, monospace';
ctx.fillText('LEISTUNG', cx, cy - R + 48);
}
// Weiche Kurve
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 drawPowerChart() {
var canvas = document.getElementById('v4_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(0, 10, 20, 0.6)';
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(0, 229, 255, 0.9)';
ctx.font = 'bold 12px Consolas, monospace';
ctx.fillText('LEISTUNG – 5 MIN', 12, 14);
if (!powerHistory.length) {
ctx.fillStyle = 'rgba(230,247,255,0.5)';
ctx.font = '12px Consolas, monospace';
ctx.fillText('warte auf Daten…', 12, padT + plotH / 2);
return;
}
var WINDOW = 5 * 60 * 1000;
var t1 = now, t0 = now - WINDOW;
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(0, 229, 255, 0.08)';
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();
}
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));
// Fläche
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,59,92,0.35)');
grad.addColorStop(Math.max(0, zeroFrac - 0.001), 'rgba(255,59,92,0.05)');
grad.addColorStop(Math.min(1, zeroFrac + 0.001), 'rgba(0,255,156,0.05)');
grad.addColorStop(1, 'rgba(0,255,156,0.35)');
ctx.fillStyle = grad;
ctx.fill();
// Nulllinie
if (minV < 0 && maxV > 0) {
ctx.save();
ctx.setLineDash([4, 4]);
ctx.strokeStyle = 'rgba(230,247,255,0.3)';
ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(padL, zeroY); ctx.lineTo(padL + plotW, zeroY); ctx.stroke();
ctx.restore();
}
// Linie
ctx.save();
ctx.beginPath();
smoothCurve(ctx, pts);
ctx.strokeStyle = 'rgba(0, 229, 255, 0.95)';
ctx.lineWidth = 2.4;
ctx.lineJoin = 'round';
ctx.shadowColor = 'rgba(0, 229, 255, 0.55)';
ctx.shadowBlur = 8;
ctx.stroke();
ctx.restore();
// Y-Beschriftung
ctx.fillStyle = 'rgba(230,247,255,0.55)';
ctx.font = '10px Consolas, monospace';
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
var lp = pts[pts.length - 1];
var col = lp.v < 0 ? '0,255,156' : '255,59,92';
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();
// Wert-Pille
var label = 'akt. Leistung: ' + Math.round(lp.v) + ' W';
ctx.font = 'bold 11px Consolas, monospace';
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);
}
function drawDailyChart() {
var canvas = document.getElementById('v4_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(0, 10, 20, 0.6)'; ctx.fillRect(0, 0, w, h);
var padL = 44, padR = 12, padT = 22, padB = 28;
var plotW = w - padL - padR, plotH = h - padT - padB;
var bez = num(lastValues[METER.bezug]);
var hist = METER.hist.map(function (x) { return num(lastValues[x]); });
var seq = [bez].concat(hist);
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));
}
dailyValues.reverse();
var tagVal = num(lastValues[METER.tag]) || 0;
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); }
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 dd = new Date(heute.getFullYear(), heute.getMonth(), heute.getDate() - (7 - i));
dayLabels.push(wochentage[dd.getDay()]);
}
ctx.textAlign = 'left'; ctx.fillStyle = 'rgba(0, 229, 255, 0.9)';
ctx.font = 'bold 12px Consolas, monospace';
ctx.fillText('TAGESBEZUG – 8 TAGE', 12, 14);
ctx.strokeStyle = 'rgba(0, 229, 255, 0.08)'; 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();
}
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);
var grad = ctx.createLinearGradient(0, y1, 0, y0);
if (isToday) {
grad.addColorStop(0, 'rgba(255,179,0,0.95)');
grad.addColorStop(1, 'rgba(255,179,0,0.25)');
} else {
grad.addColorStop(0, 'rgba(0,255,156,0.95)');
grad.addColorStop(1, 'rgba(0,255,156,0.25)');
}
ctx.fillStyle = grad;
roundRect(ctx, bx, y1, barW, bh, 4);
ctx.fill();
ctx.fillStyle = isToday ? 'rgba(255,179,0,0.95)' : 'rgba(230,247,255,0.85)';
ctx.font = 'bold 10px Consolas, monospace';
ctx.textAlign = 'center';
ctx.fillText(dailyValues[i].toFixed(2), x, y1 - 5);
ctx.fillStyle = isToday ? 'rgba(255,179,0,0.95)' : 'rgba(230,247,255,0.65)';
ctx.font = 'bold 10px Consolas, monospace';
ctx.textAlign = 'center';
ctx.fillText(dayLabels[i], x, h - 8);
}
var tagY = Y(tagVal);
ctx.save();
ctx.strokeStyle = 'rgba(0, 229, 255, 0.95)'; ctx.lineWidth = 2.5;
ctx.shadowColor = 'rgba(0, 229, 255, 0.55)'; ctx.shadowBlur = 8;
ctx.beginPath(); ctx.moveTo(padL, tagY); ctx.lineTo(padL + plotW, tagY); ctx.stroke();
ctx.restore();
ctx.fillStyle = 'rgba(230,247,255,0.55)'; ctx.font = '10px Consolas, monospace';
ctx.textAlign = 'right';
ctx.fillText(maxV.toFixed(1) + ' kWh', padL - 6, padT + 8);
ctx.fillText(minV.toFixed(1) + ' kWh', padL - 6, padT + plotH);
var label = 'täglicher Bezug: ' + tagVal.toFixed(2) + ' kWh';
ctx.font = 'bold 11px Consolas, monospace';
var tw = ctx.measureText(label).width, bw = tw + 14, bx = w - padR - bw, by = 4, bh = 17;
ctx.fillStyle = 'rgba(0,229,255,0.16)'; roundRect(ctx, bx, by, bw, bh, 9); ctx.fill();
ctx.fillStyle = 'rgba(0,229,255,1)'; ctx.textAlign = 'left';
ctx.fillText(label, bx + 7, by + 12);
}
function drawWaterChart() {
var canvas = document.getElementById('v4_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(0, 10, 20, 0.6)'; ctx.fillRect(0, 0, w, h);
var padL = 50, padR = 16, padT = 22, padB = 28;
var plotW = w - padL - padR, plotH = h - padT - padB;
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));
}
dailyValues.reverse();
var maxV = Math.max.apply(null, dailyValues);
var minV = 0;
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); }
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 dd = new Date(heute.getFullYear(), heute.getMonth(), heute.getDate() - (dailyValues.length - 1 - i));
dayLabels.push(wochentage[dd.getDay()]);
}
ctx.textAlign = 'left'; ctx.fillStyle = 'rgba(0, 229, 255, 0.9)';
ctx.font = 'bold 12px Consolas, monospace';
ctx.fillText('WASSER – 7 TAGE', 12, 14);
ctx.strokeStyle = 'rgba(0, 229, 255, 0.08)'; 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();
}
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(0,229,255,0.95)');
grad.addColorStop(1, 'rgba(0,229,255,0.25)');
} else {
grad.addColorStop(0, 'rgba(0,229,255,0.75)');
grad.addColorStop(1, 'rgba(0,229,255,0.15)');
}
ctx.fillStyle = grad;
roundRect(ctx, bx, y1, barW, bh, 4);
ctx.fill();
ctx.fillStyle = isToday ? 'rgba(0,229,255,0.95)' : 'rgba(230,247,255,0.85)';
ctx.font = 'bold 10px Consolas, monospace';
ctx.textAlign = 'center';
ctx.fillText(Math.round(dailyValues[i]), x, y1 - 5);
ctx.fillStyle = isToday ? 'rgba(0,229,255,0.95)' : 'rgba(230,247,255,0.65)';
ctx.font = 'bold 10px Consolas, monospace';
ctx.textAlign = 'center';
ctx.fillText(dayLabels[i], x, h - 8);
}
var tagY = Y(tagVal);
ctx.save();
ctx.strokeStyle = 'rgba(255,179,0,0.95)'; ctx.lineWidth = 2.5;
ctx.setLineDash([6, 4]);
ctx.shadowColor = 'rgba(255,179,0,0.55)'; ctx.shadowBlur = 8;
ctx.beginPath(); ctx.moveTo(padL, tagY); ctx.lineTo(padL + plotW, tagY); ctx.stroke();
ctx.restore();
ctx.fillStyle = 'rgba(230,247,255,0.55)'; ctx.font = '10px Consolas, monospace';
ctx.textAlign = 'right';
ctx.fillText(Math.round(maxV) + ' L', padL - 6, padT + 8);
ctx.fillText(Math.round(minV) + ' L', padL - 6, padT + plotH);
var label = 'täglicher Bezug: ' + Math.round(tagVal) + ' L';
ctx.font = 'bold 11px Consolas, monospace';
var tw = ctx.measureText(label).width, bw = tw + 14, bx = w - padR - bw, by = 4, bh = 17;
ctx.fillStyle = 'rgba(255,179,0,0.16)'; roundRect(ctx, bx, by, bw, bh, 9); ctx.fill();
ctx.fillStyle = 'rgba(255,179,0,1)'; ctx.textAlign = 'left';
ctx.fillText(label, bx + 7, by + 12);
}
// ====== Animation ======
function startChartAnimation() {
var last = 0;
function frame(ts) {
requestAnimationFrame(frame);
if (ts - last < 33) { return; }
last = ts;
gaugeShown += (gaugeTarget - gaugeShown) * 0.18;
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;
}
function flash(el, r) {
if (!el) { return; }
var cls = (r && !r.error) ? 'v4-flash-ok' : 'v4-flash-err';
el.classList.remove('v4-flash-ok', 'v4-flash-err');
void el.offsetWidth;
el.classList.add(cls);
setTimeout(function () { el.classList.remove(cls); }, 600);
}
function onClick(ev) {
var btn = ev.target.closest ? ev.target.closest('button') : null;
// Mehrfach-Schreibvorgang
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 });
toast('Schnellbefehl ausgeführt', allOk ? 'ok' : 'err');
});
return;
}
// 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;
}
// Heizung: Stepper (+/-) – Wert um 0.5°C aendern und schreiben
var hs = ev.target.closest ? ev.target.closest('[data-heat-step]') : null;
if (hs) {
var hI = parseInt(hs.getAttribute('data-heat-i'), 10);
var hStep = hs.getAttribute('data-heat-step');
var hDir = parseInt(hs.getAttribute('data-heat-dir'), 10);
var hObj = HEAT[hI - 1];
if (!hObj) { return; }
var cur = num(lastValues[hObj[hStep]]);
if (cur === null) { cur = 20; }
var newVal = Math.round((cur + hDir * 0.5) * 10) / 10;
if (!heatPlausible(hI, hStep, newVal)) { return; }
write(hObj[hStep], newVal).then(function (r) {
flash(btn, r);
toast(hObj.name + ' ' + hStep + ': ' + newVal.toFixed(1) + '°C', r && !r.error ? 'ok' : 'err');
setTimeout(refreshSettings, 300);
});
return;
}
// Poolpumpe
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) { toast('Bitte eine gültige Zahl eingeben.', 'err'); return; }
write(pm.getAttribute('data-var'), Math.round(pMin * 60)).then(function (r) {
flash(btn, r);
toast('Poolpumpe SOLL gesetzt', r && !r.error ? 'ok' : 'err');
readMany([POOL.max]).then(function (m) { merge(m); render(lastValues); }).catch(function () {});
});
return;
}
// WLED
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); toast('WLED Helligkeit', r && !r.error ? 'ok' : 'err'); });
} 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); toast('WLED Farbe', r && !r.error ? 'ok' : 'err'); });
}
return;
}
}
// ====== Polling ======
var FAST_VARS = [METER.leist];
var ALL_VARS = null;
var SET_VARS = null;
var SLOW_EVERY = 4;
var tickCount = 0;
function merge(m) { Object.keys(m).forEach(function (k) { lastValues[k] = m[k]; }); }
function refreshSettings() {
readMany(SET_VARS).then(function (m) { merge(m); render(lastValues); }).catch(function () {});
}
function tick() {
if (pollBusy) { return; } // Mutex: kein überlappender Request
pollBusy = true;
tickCount++;
var isFull = (tickCount % SLOW_EVERY === 1);
var vars = isFull ? ALL_VARS : FAST_VARS;
readMany(vars).then(function (m) {
resetBackoff();
merge(m);
render(lastValues);
setStatus(true, 'ONLINE');
// readWemos nur jeden 10. vollen Zyklus, um Request-Last zu reduzieren
if (isFull) {
wemosCounter++;
if (wemosCounter >= 10) {
wemosCounter = 0;
readWemos();
}
}
pollBusy = false;
}).catch(function () {
increaseBackoff(); // exponentieller Backoff bei Fehler
setStatus(false, 'RECONNECT IN ' + Math.round(backoffMs / 1000) + 's');
pollBusy = false;
});
}
// Polling mit dynamischem Intervall (Backoff)
function scheduleNext() {
setTimeout(function () {
tick();
scheduleNext();
}, backoffMs);
}
// ====== Scanline-Toggle ======
var scanEl = document.getElementById('v4_scanlines');
var scanBtn = document.getElementById('scan_toggle');
if (scanBtn && scanEl) {
scanBtn.addEventListener('click', function () {
var on = scanEl.classList.toggle('v4-scanlines--on');
try { localStorage.setItem('v4_scanlines', on ? '1' : '0'); } catch (e) {}
});
try {
if (localStorage.getItem('v4_scanlines') === '1') {
scanEl.classList.add('v4-scanlines--on');
}
} catch (e) {}
}
// ====== Start ======
document.addEventListener('click', onClick);
buildLightTiles();
buildRolladen();
buildSockets();
buildHeat();
ALL_VARS = allReadVars();
SET_VARS = settingsVars();
if (PREVIEW) {
// Sofort Dummy-Daten laden, kein Login/Polling
var pv = makePreviewValues();
Object.keys(pv).forEach(function (k) { lastValues[k] = pv[k]; });
render(lastValues);
setStatus(true, 'PREVIEW');
setText('sz_wemos', 'PREVIEW MODUS – keine SPS');
// Dummy-Leistung zyklisch variieren
setInterval(function () {
lastValues[METER.leist] = 300 + Math.sin(Date.now() / 2000) * 1500;
render(lastValues);
}, 500);
runBoot();
startChartAnimation();
return;
}
runBoot();
startChartAnimation();
// Erstes volles Lesen, dann zyklisch
readMany(ALL_VARS).then(function (m) {
merge(m);
render(lastValues);
setStatus(true, 'ONLINE');
readWemos();
// Heizungs-SOLL min/max einmalig beim Start lesen (werden sonst nie angezeigt)
refreshSettings();
}).catch(function () {
setStatus(false, 'OFFLINE');
});
scheduleNext(); // dynamisches Polling mit Backoff statt setInterval
})();