/** * Wärmepumpe: Instant values + Tages-COP + Gesamt-COP * - Intervall 10s * - Kein setObject (kein Erstellen von States) * - Safe setState wrapper; vermeidet warn stack traces * * Ziel-States (optional, anlegen für persistente Speicherung): * javascript.0.heizung.electricPower_W * javascript.0.heizung.thermalPower_W * javascript.0.heizung.COP * javascript.0.heizung.deltaT_K * javascript.0.heizung.flow_Lmin * javascript.0.heizung.massFlow_kgps * javascript.0.heizung.compressor_Hz * javascript.0.heizung.notes * javascript.0.heizung.dailyThermal_Wh * javascript.0.heizung.dailyElectrical_Wh * javascript.0.heizung.dailyCOP * javascript.0.heizung.totalThermal_Wh * javascript.0.heizung.totalElectrical_Wh * javascript.0.heizung.totalCOP */ 'use strict'; // -------- CONFIG ---------- const INTERVAL_MS = 10000; // 10s // Inputs (anpassen falls nötig) const INPUT_POWER = 'tuya.0.bfbe5d6d2134435cecpdau.103'; // W const INPUT_SRC = 'tuya.0.bf751cd35a131c1418tick.106'; // base64 block const INPUT_COMP = 'tuya.0.bf751cd35a131c1418tick.16'; // Hz // Indizes im Datenblock const IDX_RL = 6, IDX_VL = 7, IDX_FLOW = 9; // Outputs const OUT_POWER = 'javascript.0.heizung.electricPower_W'; const OUT_THERMAL = 'javascript.0.heizung.thermalPower_W'; const OUT_COP = 'javascript.0.heizung.COP'; const OUT_DT = 'javascript.0.heizung.deltaT_K'; const OUT_FLOW = 'javascript.0.heizung.flow_Lmin'; const OUT_MASS = 'javascript.0.heizung.massFlow_kgps'; const OUT_COMP_HZ = 'javascript.0.heizung.compressor_Hz'; const OUT_NOTES = 'javascript.0.heizung.notes'; const OUT_DAILY_THERMAL_WH = 'javascript.0.heizung.dailyThermal_Wh'; const OUT_DAILY_ELEC_WH = 'javascript.0.heizung.dailyElectrical_Wh'; const OUT_DAILY_COP = 'javascript.0.heizung.dailyCOP'; const OUT_TOTAL_THERMAL_WH = 'javascript.0.heizung.totalThermal_Wh'; const OUT_TOTAL_ELEC_WH = 'javascript.0.heizung.totalElectrical_Wh'; const OUT_TOTAL_COP = 'javascript.0.heizung.totalCOP'; // Physik / Konstanten const CP_WATER = 4186; // J/(kg*K) const INVALID_WORD = 0x8000; // Safe logging function safeLog(msg){ try { log('[WP] ' + msg); } catch(e) {} } function safeWarn(msg){ try { log('[WP][WARN] ' + msg); } catch(e) {} } // ----- safe wrappers to avoid noisy stack traces ----- function setStateSafe(id, val) { return new Promise(resolve => { try { setState(id, { val: val, ack: true }, (err) => { if (err) { try { safeWarn('setState callback error for ' + id + ': ' + (err && err.stack ? err.stack : err)); } catch(_) {} } resolve(); }); } catch (e) { try { safeWarn('setState threw for ' + id + ': ' + (e && e.stack ? e.stack : e)); } catch(_) {} resolve(); } }); } let safeQueue = Promise.resolve(); function enqueue(fn) { safeQueue = safeQueue.then(() => Promise.resolve().then(fn)).catch(e => { try { safeWarn('enqueue error: ' + (e && e.stack ? e.stack : e)); } catch(_) {} }).then(() => new Promise(r => setTimeout(r, 60))); return safeQueue; } // ----- helper promises for getState/getObject ----- function getStateP(id){ return new Promise(res => { try { getState(id, (e,s) => res(s ? s.val : null)); } catch(e) { res(null); } }); } function getObjectP(id){ return new Promise(res => { try { getObject(id, (e,o) => res(o || null)); } catch(e) { res(null); } }); } // write only if state exists (log missing once) const missingStates = new Set(); async function writeIfExists(id, value) { if (!id) return; return enqueue(async () => { const obj = await getObjectP(id); if (!obj) { if (!missingStates.has(id)) { missingStates.add(id); safeWarn('State not found (will not create): ' + id); } return; } // read current to avoid redundant writes const cur = await getStateP(id); if (typeof value === 'number' && typeof cur === 'number') { if (Math.abs(cur - value) < 1e-9) return; } else { if (cur === value) return; } await setStateSafe(id, value); }); } // ----- decode block / compute functions ----- function toSigned(v){ return v > 0x7FFF ? v - 0x10000 : v; } function decodeWords(b64){ try { if (!b64 || typeof b64 !== 'string') return null; const buf = Buffer.from(b64, 'base64'); const out = []; for (let i = 0; i + 1 < buf.length; i += 2) out.push(buf.readUInt16BE(i)); return out; } catch (e) { safeWarn('decodeWords failed: ' + (e && e.stack ? e.stack : e)); return null; } } function computeFromBlock(powerW, words){ const notes = []; if (!words || words.length <= IDX_FLOW) { notes.push('Block zu kurz'); return { thermalW: null, cop: null, deltaT: null, flowLpm: null, massKgPs: null, notes }; } const rlRaw = words[IDX_RL] & 0xFFFF; const vlRaw = words[IDX_VL] & 0xFFFF; const flowRaw = words[IDX_FLOW] & 0xFFFF; const rl = (rlRaw === INVALID_WORD) ? null : toSigned(rlRaw) / 10; const vl = (vlRaw === INVALID_WORD) ? null : toSigned(vlRaw) / 10; const flowLpm = (flowRaw === INVALID_WORD) ? null : flowRaw / 10; if (rl === null) notes.push('RL fehlt'); if (vl === null) notes.push('VL fehlt'); if (flowLpm === null) notes.push('Flow fehlt'); const deltaT = (rl !== null && vl !== null) ? (vl - rl) : null; let massFlow = (flowLpm !== null) ? (flowLpm / 60.0) : null; // kg/s if (massFlow !== null && (!Number.isFinite(massFlow) || massFlow <= 0)) { notes.push('Flow ungültig'); massFlow = null; } let thermalW = null; if (massFlow !== null && deltaT !== null) { thermalW = massFlow * CP_WATER * deltaT; // W if (!Number.isFinite(thermalW)) { thermalW = null; notes.push('ThermW nicht endlich'); } } let cop = null; if (thermalW !== null) { if (powerW === null || powerW === undefined || isNaN(powerW) || Number(powerW) <= 0) notes.push('elektrische Leistung fehlt oder <=0'); else { cop = thermalW / Number(powerW); if (!Number.isFinite(cop)) { cop = null; notes.push('COP nicht endlich'); } } } return { thermalW, cop, deltaT, flowLpm, massKgPs: massFlow, notes }; } // ----- accumulators for daily/total (in Wh) ----- let dailyThermalWh = 0; let dailyElecWh = 0; let totalThermalWh = 0; let totalElecWh = 0; let lastTimestamp = Date.now(); let currentDay = (new Date()).toISOString().slice(0,10); // bootstrap from existing states (if present) async function bootstrapAccumulators() { try { const dT = await getStateP(OUT_DAILY_THERMAL_WH); const dE = await getStateP(OUT_DAILY_ELEC_WH); const tT = await getStateP(OUT_TOTAL_THERMAL_WH); const tE = await getStateP(OUT_TOTAL_ELEC_WH); dailyThermalWh = (typeof dT === 'number' && Number.isFinite(dT)) ? Number(dT) : 0; dailyElecWh = (typeof dE === 'number' && Number.isFinite(dE)) ? Number(dE) : 0; totalThermalWh = (typeof tT === 'number' && Number.isFinite(tT)) ? Number(tT) : 0; totalElecWh = (typeof tE === 'number' && Number.isFinite(tE)) ? Number(tE) : 0; safeLog(`bootstrapped: dailyThermalWh=${dailyThermalWh}, dailyElecWh=${dailyElecWh}, totalThermalWh=${totalThermalWh}, totalElecWh=${totalElecWh}`); } catch (e) { safeWarn('bootstrap EX: ' + (e && e.stack ? e.stack : e)); } } // midnight rollover async function midnightRolloverIfNeeded(){ const today = (new Date()).toISOString().slice(0,10); if (today !== currentDay) { safeLog(`Midnight rollover ${currentDay} -> ${today}`); // move daily into total totalThermalWh += dailyThermalWh; totalElecWh += dailyElecWh; // persist totals if states exist await writeIfExists(OUT_TOTAL_THERMAL_WH, Math.round(totalThermalWh * 1000) / 1000); await writeIfExists(OUT_TOTAL_ELEC_WH, Math.round(totalElecWh * 1000) / 1000); // compute/store total COP if (totalElecWh > 0) { await writeIfExists(OUT_TOTAL_COP, Math.round((totalThermalWh / totalElecWh) * 100) / 100); } else { await writeIfExists(OUT_TOTAL_COP, null); } // reset daily dailyThermalWh = 0; dailyElecWh = 0; await writeIfExists(OUT_DAILY_THERMAL_WH, 0); await writeIfExists(OUT_DAILY_ELEC_WH, 0); await writeIfExists(OUT_DAILY_COP, null); currentDay = today; } } // ----- main cycle ----- async function runCycle(){ try { await midnightRolloverIfNeeded(); // read inputs const rawPower = await getStateP(INPUT_POWER).catch(()=>null); const powerW = (rawPower === null || rawPower === undefined) ? null : Number(rawPower); const rawComp = await getStateP(INPUT_COMP).catch(()=>null); const compVal = (rawComp === null || rawComp === undefined) ? null : Number(rawComp); const src = await getStateP(INPUT_SRC).catch(()=>null); if (!src) { await writeIfExists(OUT_NOTES, 'SRC fehlt'); safeLog('SRC fehlt — skipping'); return; } const words = decodeWords(src); if (!words) { await writeIfExists(OUT_NOTES, 'Decode Fehler'); safeWarn('decode invalid'); return; } const res = computeFromBlock(powerW, words); // write instantaneous values (if states exist) await writeIfExists(OUT_POWER, powerW == null ? null : Math.round(powerW)); await writeIfExists(OUT_COMP_HZ, compVal == null ? null : Math.round(compVal * 10) / 10); await writeIfExists(OUT_THERMAL, res.thermalW == null ? null : Math.round(res.thermalW)); await writeIfExists(OUT_COP, res.cop == null ? null : Math.round(res.cop * 100) / 100); await writeIfExists(OUT_DT, res.deltaT == null ? null : Math.round(res.deltaT * 10) / 10); await writeIfExists(OUT_FLOW, res.flowLpm == null ? null : Math.round(res.flowLpm * 10) / 10); await writeIfExists(OUT_MASS, res.massKgPs == null ? null : Math.round(res.massKgPs * 1000) / 1000); await writeIfExists(OUT_NOTES, res.notes.length ? res.notes.join('; ') : 'ok'); // integrate energies (Wh) const now = Date.now(); let dtSec = (now - lastTimestamp) / 1000.0; if (dtSec <= 0 || dtSec > 600) dtSec = INTERVAL_MS / 1000.0; // fallback lastTimestamp = now; if (res.thermalW !== null && Number.isFinite(res.thermalW)) { dailyThermalWh += Number(res.thermalW) * dtSec / 3600.0; } if (powerW !== null && Number.isFinite(powerW) && powerW > 0) { dailyElecWh += Number(powerW) * dtSec / 3600.0; } // persist daily accumulators await writeIfExists(OUT_DAILY_THERMAL_WH, Math.round(dailyThermalWh * 1000) / 1000); await writeIfExists(OUT_DAILY_ELEC_WH, Math.round(dailyElecWh * 1000) / 1000); // daily COP if (dailyElecWh > 0) { const dailyCop = dailyThermalWh / dailyElecWh; await writeIfExists(OUT_DAILY_COP, Math.round(dailyCop * 100) / 100); } else { await writeIfExists(OUT_DAILY_COP, null); } // totals including today's running const totalNowThermal = totalThermalWh + dailyThermalWh; const totalNowElec = totalElecWh + dailyElecWh; if (totalNowElec > 0) { await writeIfExists(OUT_TOTAL_COP, Math.round((totalNowThermal / totalNowElec) * 100) / 100); await writeIfExists(OUT_TOTAL_THERMAL_WH, Math.round(totalNowThermal * 1000) / 1000); await writeIfExists(OUT_TOTAL_ELEC_WH, Math.round(totalNowElec * 1000) / 1000); } else { await writeIfExists(OUT_TOTAL_COP, null); } // neat log const qLog = res.thermalW == null ? 'n/a' : Math.round(res.thermalW) + 'W'; const pLog = powerW == null ? 'n/a' : Math.round(powerW) + 'W'; const copLog = res.cop == null ? 'n/a' : (Math.round(res.cop * 100) / 100).toFixed(2); const dtLog = res.deltaT == null ? 'n/a' : (Math.round(res.deltaT * 10) / 10).toFixed(1); const flowLog = res.flowLpm == null ? 'n/a' : (Math.round(res.flowLpm * 10) / 10).toFixed(1); const compLog = compVal == null ? 'n/a' : (Math.round(compVal * 10) / 10).toFixed(1) + ' Hz'; const dailyCopNow = (dailyElecWh > 0) ? (Math.round((dailyThermalWh / dailyElecWh) * 100) / 100).toFixed(2) : 'n/a'; safeLog(`P=${pLog}, Q=${qLog}, COP=${copLog}, ΔT=${dtLog}, Flow=${flowLog} L/min, Comp=${compLog}, dailyCOP=${dailyCopNow}`); } catch (e) { safeWarn('runCycle EX: ' + (e && e.stack ? e.stack : e)); } } // ----- init & start ----- (async () => { await bootstrapAccumulators(); lastTimestamp = Date.now(); currentDay = (new Date()).toISOString().slice(0,10); await runCycle(); const timer = setInterval(runCycle, INTERVAL_MS); onStop(() => { try { clearInterval(timer); safeLog('stopped'); } catch(e) {} }); })();