// ==================================================================== // 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 })();