Files
haus-steuerung/Script/tth_v4.js
T

1321 lines
47 KiB
JavaScript
Raw 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_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 = '<span class="v4-tile-name">' + l[0] + '</span><span class="v4-tile-orb"></span>';
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 =
'<span class="v4-heat-name">' + h.name + '</span>' +
'<span class="v4-heat-tempwrap"><span class="v4-heat-temp v4-heat-temp--normal" id="he_temp_' + h.i + '"></span>' +
'<span class="v4-heat-dot" id="he_dot_' + h.i + '"></span></span>' +
'<span class="v4-heat-bar-wrap" id="he_bar_' + h.i + '"></span>' +
'<span class="v4-heat-set v4-heat-set--min"><span class="v4-stepper"><button class="v4-stepper-btn" type="button" data-heat-step="min" data-heat-dir="-1" data-heat-i="' + h.i + '"></button><span class="v4-stepper-val" id="he_min_' + h.i + '"></span><button class="v4-stepper-btn" type="button" data-heat-step="min" data-heat-dir="1" data-heat-i="' + h.i + '">+</button></span></span>' +
'<span class="v4-heat-set v4-heat-set--max"><span class="v4-stepper"><button class="v4-stepper-btn" type="button" data-heat-step="max" data-heat-dir="-1" data-heat-i="' + h.i + '"></button><span class="v4-stepper-val" id="he_max_' + h.i + '"></span><button class="v4-stepper-btn" type="button" data-heat-step="max" data-heat-dir="1" data-heat-i="' + h.i + '">+</button></span></span>';
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
})();