// ****************************** // tibberNextCheapest v2.0.0 // ****************************** // Copyright ©MCU // - Anpassung an 15-Minuten-Intervalle (96 Werte/Tag) // - numHours und cheapestArea weiterhin in Stunden, intern aber auf 15min-Bloecke umgerechnet // - robustere Fehlerbehandlung, Sortierung, Log-Ausgabe verbessert // ================== Konfiguration ================== const tibberInst = '0'; const tibberdataDP = '0_userdata.0.tibberdata'; const cheapestNextHoursDP = tibberdataDP + '.cheapestNextHours'; const cheapestNextStartTSDP = tibberdataDP + '.cheapestNextStartTS'; const cheapestNextStartDP = tibberdataDP + '.cheapestNextStart'; const cheapestSearchAreaDP = tibberdataDP + '.cheapestArea'; const startDP = tibberdataDP + '.startCalc'; const numHoursDP = tibberdataDP + '.numHours'; // ================== Datenpunkte ================== createStateAsync(cheapestNextHoursDP, { read: true, write: false, name: "Guenstigste X Stunden im Bereich", type: "string", def: "[]" }); createStateAsync(cheapestNextStartTSDP, { read: true, write: false, name: "Start [TS] der naechsten guenstigen X Stunden", type: "number", def: 0 }); createStateAsync(cheapestNextStartDP, { read: true, write: false, name: "Start [Text] der naechsten guenstigen X Stunden", type: "string", def: "" }); createStateAsync(cheapestSearchAreaDP, { read: true, write: true, name: "Bereich fuer Suche [h]", type: "number", unit: "Std.", def: 10 }); createStateAsync(startDP, { read: true, write: true, name: "Berechnung starten", type: "boolean", role: "button", def: false }); createStateAsync(numHoursDP, { read: true, write: true, name: "Zusammenhaengende Stunden [h]", type: "number", unit: "Std.", def: 2 }); // ================== Trigger ================== schedule('00 * * * *', getCheapest); // jede volle Stunde neu berechnen on({ id: [cheapestSearchAreaDP, numHoursDP], change: "any" }, getCheapest); on({ id: startDP, change: "any" }, obj => { if (obj.state.val) { getCheapest(); setState(startDP, false); } }); // ================== Hauptfunktion ================== function getCheapest() { const priceArr = ['PricesToday', 'PricesTomorrow']; let tibberData = []; const searchHours = parseFloat(getState(cheapestSearchAreaDP).val) || 10; const numHours = parseFloat(getState(numHoursDP).val) || 2; const searchSlots = Math.round(searchHours * 4); // 4 Viertelstunden pro Stunde const numSlots = Math.round(numHours * 4); for (let x = 0; x < priceArr.length; x++) { const tibberLink = $('tibberlink.' + tibberInst + '.Homes.*.' + priceArr[x] + '.json'); if (tibberLink[0] != undefined) { const tblLevel = levelObject(tibberLink[0]); const dpLeveltbl = getDPLevel(tblLevel, 4); const raw = getState(dpLeveltbl + '.json')?.val || '[]'; let tibberJSON = []; try { tibberJSON = JSON.parse(raw); } catch (e) { log(`⚠️ Fehler beim Parsen von ${priceArr[x]}`, 'warn'); continue; } for (let i = 0; i < tibberJSON.length; i++) { tibberData.push({ start: tibberJSON[i].startsAt, startTS: new Date(tibberJSON[i].startsAt).getTime(), value: tibberJSON[i].total }); } } } if (!tibberData.length) { log('⚠️ Keine Tibber-Daten verfuegbar.', 'warn'); return; } tibberData.sort((a, b) => a.startTS - b.startTS); const now = Date.now(); const nextIndex = tibberData.findIndex(e => e.startTS > now); const startIndex = nextIndex !== -1 ? nextIndex : 0; const subset = tibberData.slice(startIndex, startIndex + searchSlots); const cheapest = findCheapestSlots(subset, numSlots); if (!cheapest.length) { log('⚠️ Keine guenstigen Zeitraeume gefunden.', 'warn'); return; } setState(cheapestNextHoursDP, JSON.stringify(cheapest), true); setState(cheapestNextStartTSDP, cheapest[0].startTS, true); setState(cheapestNextStartDP, formatDate(cheapest[0].startTS, 'DD.MM.YYYY hh:mm:ss'), true); const avgPrice = (cheapest.reduce((a, b) => a + b.value, 0) / cheapest.length).toFixed(3); log(`💡 Guenstigste ${numHours}h (≈${numSlots * 15} min) starten um ${formatDate(cheapest[0].startTS, 'hh:mm')} – ∅ ${avgPrice} ct/kWh`); } // ================== Hilfsfunktionen ================== function findCheapestSlots(data, numSlots) { if (numSlots < 1 || numSlots > data.length) return []; let minPrice = Infinity; let result = []; for (let i = 0; i <= data.length - numSlots; i++) { const current = data.slice(i, i + numSlots); const sum = current.reduce((a, b) => a + b.value, 0); if (sum < minPrice) { minPrice = sum; result = current; } } return result; } function levelObject(id) { return id.split('.'); } function getDPLevel(idArr, level) { return idArr.slice(0, level + 1).join('.'); }