/************************************************************ * EVCC Optimizer → Zendure Speicher (Battery 2 + 3) * - Laden / Entladen (true/false) * - Diagramm JSON für VIS ECharts * (grün=laden, rot=entladen, blau=SOC, orange=Netzpreis) * - Zeitauflösung: 15 Minuten * - Netzpreis in ct/kWh, 1 Nachkommastelle * - Step-Linie, keine Punkte ************************************************************/ /* =================== KONFIG =================== */ const EVCC_BATTERIES_DP = 'evcc.0.status.Evopt.Res.batteries'; const EVCC_TIME_SERIES_DP = 'evcc.0.status.Evopt.Req.time_series'; // Ergebnis-Datenpunkte const DP_CHARGE = '0_userdata.0.EVCC.Zendure.Laden_Optimal'; const DP_DISCHARGE = '0_userdata.0.EVCC.Zendure.Entladen_Optimal'; const DP_CHART_JSON = '0_userdata.0.EVCC.Zendure.Chart_JSON'; // Leistungsschwellwerte (W) const CHARGE_THRESHOLD = 50; const DISCHARGE_THRESHOLD = 50; /* =================== STATES ANLEGEN =================== */ createState(DP_CHARGE, false, { type: 'boolean', role: 'indicator', name: 'EVCC Zendure Laden optimal' }); createState(DP_DISCHARGE, false, { type: 'boolean', role: 'indicator', name: 'EVCC Zendure Entladen optimal' }); createState(DP_CHART_JSON, '', { type: 'string', role: 'json', name: 'EVCC Zendure Optimizer Chart' }); /* =================== HAUPTFUNKTION =================== */ function processEVCCZendure() { const batteries = getState(EVCC_BATTERIES_DP)?.val; const timeSeries = getState(EVCC_TIME_SERIES_DP)?.val; if (!batteries || !Array.isArray(batteries) || !timeSeries) { log('❌ EVCC JSON leer oder kein Array', 'warn'); return; } if (!batteries[1] || !batteries[2]) { log('❌ Batterie 2 oder 3 nicht vorhanden', 'warn'); return; } const zendure = [batteries[1], batteries[2]]; const slots = zendure[0].charging_power.length; let labels = [], chargeArr = [], dischargeArr = [], socArr = [], priceArr = []; let chargeNow = false, dischargeNow = false; const now = new Date(); // Runde Minuten auf nächstes 15-Minuten-Intervall const roundedMinutes = Math.floor(now.getMinutes() / 15) * 15; now.setMinutes(roundedMinutes, 0, 0); for (let i = 0; i < slots; i++) { const chargeW = zendure.reduce((sum, b) => sum + (b.charging_power[i] || 0), 0); const dischargeW = zendure.reduce((sum, b) => sum + (b.discharging_power[i] || 0), 0); const socWh = zendure.reduce((sum, b) => sum + (b.state_of_charge[i] || 0), 0); // Netzpreis in ct/kWh (p_N in €/Wh -> *1000 €/kWh -> *100 ct/kWh) const price = parseFloat(((timeSeries.p_N[i] || 0) * 100000).toFixed(1)); // Zeitlabel (15-Minuten Raster) const t = new Date(now.getTime() + i * 15 * 60000); labels.push(`${t.getHours().toString().padStart(2,'0')}:${t.getMinutes().toString().padStart(2,'0')}`); chargeArr.push(Math.round(chargeW)); dischargeArr.push(Math.round(dischargeW)); socArr.push((socWh/1000).toFixed(2)); // kWh priceArr.push(price); if (i === 0) { chargeNow = chargeW > CHARGE_THRESHOLD; dischargeNow = dischargeW > DISCHARGE_THRESHOLD; } } /* ===== TRUE / FALSE STATES ===== */ setState(DP_CHARGE, chargeNow, true); setState(DP_DISCHARGE, dischargeNow, true); /* ===== ECHARTS JSON ===== */ const chartJSON = { tooltip: { trigger: 'axis', formatter: function(params) { let s = params[0].axisValue + '
'; params.forEach(p => { if(p.seriesName === 'Netzpreis'){ s += `${p.seriesName}: ${p.data.toFixed(1)} ct/kWh
`; } else { s += `${p.seriesName}: ${p.data}
`; } }); return s; } }, legend: { data: ['Entladen','Laden','SOC','Netzpreis'], textStyle: { color:'#ffffff' } }, grid: { left:'5%', right:'6%', bottom:'8%', containLabel:true }, xAxis: { type:'category', data: labels, axisLabel: { color:'#ffffff' }, axisLine: { lineStyle: { color:'#ffffff' } } }, yAxis: [ { type:'value', name:'W', nameTextStyle: { color:'#ffffff' }, axisLabel: { color:'#ffffff' }, splitLine: { lineStyle: { color:'#888888', width:1 } } }, { type:'value', name:'kWh', position:'right', nameTextStyle: { color:'#ffffff' }, axisLabel: { color:'#ffffff' }, splitLine: { lineStyle: { color:'#888888', width:1 } } }, { type:'value', name:'ct/kWh', position:'right', offset:60, top:30, nameTextStyle: { color:'#ffffff' }, axisLabel: { color:'#ffffff' }, splitLine: { lineStyle: { color:'#888888', width:1 } } } ], series:[ { name:'Entladen', type:'bar', data: dischargeArr, itemStyle: { color:'#ff9800' }, barWidth: 8 }, { name:'Laden', type:'bar', data: chargeArr, itemStyle: { color:'#4caf50' }, barWidth: 8 }, { name:'SOC', type:'line', yAxisIndex:1, smooth:true, data: socArr, lineStyle: { color:'#2196f3', width:2 }, itemStyle: { color:'#2196f3' }, symbol: 'dot' }, { name:'Netzpreis', type:'line', yAxisIndex:2, step: 'start', smooth:false, data: priceArr, lineStyle: { color:'#3F51B5', width:2 }, itemStyle: { color:'#3F51B5' }, symbol: 'none' } ] }; setState(DP_CHART_JSON, JSON.stringify(chartJSON), true); log(`✅ EVCC Zendure aktualisiert → Laden=${chargeNow} | Entladen=${dischargeNow}`, 'info'); } /* =================== TRIGGER =================== */ on({ id: EVCC_BATTERIES_DP, change: 'ne' }, processEVCCZendure); schedule('0,15,30,45 * * * *', processEVCCZendure); /* =================== INITIAL =================== */ processEVCCZendure();