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