[Modul] PVBatteryEconomics - Sollte ich in eine Batterie investieren?

Hallo zusammen,

ich möchte mein neues Modul PVBatteryEconomics vorstellen.

Damit lässt sich auf Basis eurer stündlichen Zählerdaten abschätzen, wie sich ein Batteriespeicher wirtschaftlich auswirkt.

Was macht das Modul?

Das Modul vergleicht zwei Szenarien:

  • ohne Batterie
  • mit Batterie

Ausgegeben werden unter anderem:

  • Netzbezug / Einspeisung
  • Kosten ohne Batterie
  • Kosten mit Batterie
  • Ersparnis
  • Amortisationszeit
  • äquivalente Vollzyklen

Zusätzlich gibt es Batterie-Kennwerte:

  • in Batterie geladen (kWh)
  • aus Batterie an Last abgegeben (kWh)
  • Batterieverluste (kWh)

Debug-Transparenz

Für Analyse und Plausibilitätscheck schreibt das Modul im Debug:

  • DailyValues (Tagessummen)
  • MonthlyValues (Monatssummen)

jeweils inkl. Import/Export, Kosten und Ersparnis.

Voraussetzungen

  • Symcon 8.0+
  • stündlich aggregierte Zählerdaten für:
    • Netzbezug
    • Netzeinspeisung

Feedback

Ich freue mich über Rückmeldungen, Verbesserungsvorschläge und reale Vergleichswerte aus euren Anlagen. :slight_smile:

8 „Gefällt mir“

Herzlichen Dank.
Kommt mir gerade recht. Denke über eine Batterie nach!
scheinbar alles plausibel…:grinning_face:

Das war auch meine Intention :grin:
Bei uns geht es um eine Erweiterung. Da lag es nahe, gleich ein kleines Modul draus zu machen.
So kann man prima verschiedene Szenarien - auch mit mehreren Instanzen - leicht durchspielen.

Bei der Armortisationszeit muss man die entgangene Einspeisevergütung mit beachten.

Du hast recht: Bei der Amortisationsbetrachtung muss die entgangene Einspeisevergütung berücksichtigt werden. Inhaltlich wurde es bislang schon berücksichtigt, aber ich habe es jetzt noch erweitert, so dass es transparenter wird.

Netto-Vorteil = vermiedene Bezugskosten – entgangene Einspeisevergütung

bzw. auf kWh-Basis:

Netto-Vorteil = (vermiedener Netzbezug * Bezugspreis) - (weniger Einspeisung * Einspeisevergütung)

Und daraus ergibt sich dann :

Amortisationszeit = Investition / Netto-Vorteil

Ich habe das Modul mal entsprechend angepasst. Damit ist die Rechnung nun transparenter und nachvollziehbar.

Auch die Debugs habe ich noch mal etwas aufgedröselt. Sie werden jetzt pro Tag und Monat ausgegeben. Auch das hilft bei der Analyse.

Ich betreibe seit Ende 2019 einen BYD HV mit einer Kapazität von 7,7 kWh.

Zur genauen Erfassung der ein und ausgespeicherten Energie habe ich extra einen
4-Quadrantenzähler von ABB gekauft, und vor dem Batteriewechselrichter angeschlossen.

Das Ergebnis meiner Messung ist ein Wirkungsgrad von 80%
20% der Energie fallen leider den Wandlungsverlusten zum Opfer.

Mein System ist AC angebunden, ein reines DC System ist da etwas besser.

Ähnlich bei mir: seit 5 Jahren BYD HVM am SMA Batteriewechselrichter. Erst mit 3 Modulen, dann mit vier und jetzt bald mit 5 :slight_smile:

Der Wirkungsgrad ist leicht besser

Mir ist es übrigens heute gelungen, dank KI und Modbus auf die Batterie zuzugreifen. Stelle ich demnächst mal vor. Die Zellspannungen sind interessant.

1 „Gefällt mir“

Hi, sowas ähnliches hatte ich mir vor einiger Zeit mit der Ki gebaut.

ich wollte aber wissen ob es sich lohnt, noch eine Batterie hinzustellen (schon 2x 15kWh vorhanden)
wie sich herausgestellt hat, NEIN (zumindest im aktuellen Zustand)

meine Logik dahinter war:

An welchem Tag im Jahr habe ich eingespeist UND war am nächsten früh die Batterie am min SoC angekommen (denn nur in diesem Fall hätte ich meiner Meinung nach mit einem größeren Akku etwas sparen können)

das Script listet genau die Tage auf:

=== Analyse 2025-01-01 bis 2025-12-31 (SoC≤Limit±3%, BEIDE Bedingungen) ===
Logging: Einspeisung=AKTIV SOC=AKTIV Limit=AKTIV
Monat laden: 2025-01
Monat laden: 2025-02
Monat laden: 2025-03
Monat laden: 2025-04
Monat laden: 2025-05
Monat laden: 2025-06
Monat laden: 2025-07
Monat laden: 2025-08
Monat laden: 2025-09
Monat laden: 2025-10
Monat laden: 2025-11
Monat laden: 2025-12
Top 15 Matches (Einspeisung>3kWh UND SoC≤Limit±3%):
Tag | E(kWh) | heute(lim%) | morgen(lim%)
2025-02-03 | 9,80 | JA(50%) | NEIN(50%)
2025-11-22 | 9,70 | NEIN(40%) | JA(60%)

Summary 2025-01-01 bis 2025-12-31: matches=2, Potenzial_kWh=19.50
Kosten: vermieden=7.41€, vergütung=1.46€, netto=5.95€
Amortisation (ohne Hochrechnung): 218.58 Jahre
→ ZEIGT: Zusätzlicher Akku würde 19.5kWh/Jahr speichern!

im Script habe ich angegeben welche Kosten alles eine Rolle spielen, bei der Batt bin ich mit 1300 Euro rangegangen (was den Kosten entspricht wenn ich ihn selbst baue)

hier mein Script, evtl findest du noch ein paar sinvolle Dinge für dein Modul falls du es erweitern willst:

<?php
// =================== Konfiguration ===================
$archiveID       = 15951;
$pvIDs           = [25769, 52353, 21906]; // PV: Garage Nord, Ost, West
$varEins         = 28197;                 // Einspeisung (kWh, zählt am Tag hoch)
$varSOC          = 31049;                 // Batterie SoC (%), geloggt bei Änderung
$varMinSOCLimit  = 36308;                 // Min-SoC-Limit (%), manueller Untergrenzwert

// Analysezeitraum 2025 (bis 02.12.2025)
$rangeLabel = '2025-01-01 bis 2025-12-31';
$t0 = strtotime("2025-01-01 00:00:00");
$t1 = strtotime("2025-12-31 23:59:59");

// Wirtschaftlichkeit
$preisBezug      = 0.38;   // €/kWh
$preisEinspeise  = 0.075;  // €/kWh
$invest          = 1300.0; // €

// Fenster-/Datenqualität
$fallbackEveningHour = 18; // Fallback PV-Stop
$fallbackMorningHour = 10; // Fallback PV-Start
$socTolerance = 3.0;       // ±3% Toleranz um Limit
$minSocPoints = 1;         // min Messpunkte (reduziert wg. Toleranz)
$showLimitPerMatch = true; // ← NEU: Limits in Top-15 anzeigen

// =================== Hilfsfunktionen ===================
function getLogsSorted($archiveID,$var,$t0,$t1){
  $a=@AC_GetLoggedValues($archiveID,$var,$t0,$t1,0);
  if(!is_array($a)) $a=[];
  usort($a,function($x,$y){return $x['TimeStamp']<=>$y['TimeStamp'];});
  return $a;
}
function socClean($logs){
  $o=[]; foreach($logs as $r){ $v=$r['Value']; if(is_numeric($v)&&$v>=0&&$v<=100) $o[]=$r; } return $o;
}
function dayKey($ts){ return date('Y-m-d',$ts); }
function sumPositiveDeltasPerDay($logs){
  $sum=0.0;
  if(count($logs)==1 && is_numeric($logs[0]['Value']) && $logs[0]['Value']>0) return $logs[0]['Value'];
  for($i=1;$i<count($logs);$i++){
    $d=$logs[$i]['Value']-$logs[$i-1]['Value'];
    if(is_numeric($d) && $d>0) $sum+=$d;
  }
  return $sum;
}
function monthStartTS($y,$m){ return strtotime(sprintf("%04d-%02d-01 00:00:00",$y,$m)); }
function monthEndTS($y,$m){ return strtotime(date('Y-m-t 23:59:59', monthStartTS($y,$m))); }
function pvStopOnDay($archiveID,$pvIDs,$dayStart,$dayEnd){
  $lastIncTS=null;
  foreach($pvIDs as $vid){
    $L=getLogsSorted($archiveID,$vid,$dayStart,$dayEnd);
    for($i=1;$i<count($L);$i++){
      $inc=$L[$i]['Value']-$L[$i-1]['Value'];
      if(is_numeric($inc)&&$inc>0) $lastIncTS = max($lastIncTS??0,$L[$i]['TimeStamp']);
    }
  }
  return $lastIncTS;
}
function pvStartNextDay($archiveID,$pvIDs,$nextStart,$nextEnd){
  $firstIncTS=null;
  foreach($pvIDs as $vid){
    $L=getLogsSorted($archiveID,$vid,$nextStart,$nextEnd);
    if(count($L)>0){
      $base=$L[0]['Value'];
      for($i=1;$i<count($L);$i++){
        if(is_numeric($L[$i]['Value']) && $L[$i]['Value']>$base){
          $firstIncTS = min($firstIncTS??PHP_INT_MAX,$L[$i]['TimeStamp']);
          break;
        }
      }
    }
  }
  return $firstIncTS;
}
function socHitLimitInRange($logs,$tA,$tB,$limit,&$count,&$minHit){
  $count=0; $minHit=null; $hits=[];
  foreach($logs as $r){
    $ts=$r['TimeStamp']; if($ts<$tA||$ts>$tB) continue;
    $v=$r['Value']; if(!is_numeric($v)) continue;
    $count++;
    if(abs($v - $limit) <= 3.0) { // ±3% Toleranz
      $hits[]=$v;
      if($minHit===null || $v<$minHit) $minHit=$v;
    }
  }
  return count($hits)>0;
}

// =================== Logging-Check ===================
$okE = @AC_GetLoggingStatus($archiveID,$varEins);
$okS = @AC_GetLoggingStatus($archiveID,$varSOC);
$okL = @AC_GetLoggingStatus($archiveID,$varMinSOCLimit);
echo "=== Analyse $rangeLabel (SoC≤Limit±3%, BEIDE Bedingungen) ===\n";
echo "Logging: Einspeisung=".($okE?'AKTIV':'INAKTIV')." SOC=".($okS?'AKTIV':'INAKTIV')." Limit=".($okL?'AKTIV':'INAKTIV')."\n";
if(!$okE||!$okS||!$okL){ die("Abbruch: fehlendes Logging.\n"); }

// =================== Monats-Batches ===================
$y0=intval(date('Y',$t0)); $y1=intval(date('Y',$t1));
$m0=intval(date('n',$t0)); $m1=intval(date('n',$t1));
$lastKnownLimit = 10;
$E_day=[]; $limitPerDay=[]; $socLogsByMonth=[];

for($y=$y0;$y<=$y1;$y++){
  $mStart=($y==$y0)?$m0:1; $mEnd=($y==$y1)?$m1:12;
  for($m=$mStart;$m<=$mEnd;$m++){
    $ms=monthStartTS($y,$m); $me=monthEndTS($y,$m);
    if($ms<$t0) $ms=$t0; if($me>$t1) $me=$t1;
    echo "Monat laden: ".date('Y-m',$ms)."\n";

    $logsE = getLogsSorted($archiveID,$varEins,$ms,$me);
    $byDayE = [];
    foreach($logsE as $r){ $byDayE[dayKey($r['TimeStamp'])][]=$r; }
    foreach($byDayE as $d=>$arr){ $E_day[$d] = sumPositiveDeltasPerDay($arr); }

    $logsL = getLogsSorted($archiveID,$varMinSOCLimit,$ms,$me);
    $lastPerDay=[];
    foreach($logsL as $r){ $lastPerDay[dayKey($r['TimeStamp'])]=$r['Value']; }
    ksort($lastPerDay);
    for($ts=$ms;$ts<=$me;$ts+=86400){
      $d=dayKey($ts);
      if(isset($lastPerDay[$d])) $lastKnownLimit=$lastPerDay[$d];
      $limitPerDay[$d]=$lastKnownLimit;
    }

    $socLogsByMonth[$m] = socClean(getLogsSorted($archiveID,$varSOC,$ms,$me));
  }
}

// =================== Analyse pro Tag (BEIDE Bedingungen) ===================
$allDays=[]; for($ts=$t0;$ts<=$t1;$ts+=86400){ $allDays[]=dayKey($ts); }
$matches=[];
for($i=0;$i<count($allDays);$i++){
  $d = $allDays[$i];
  $dayStart = strtotime("$d 00:00:00");
  $dayEnd   = strtotime("$d 23:59:59");
  $E = $E_day[$d] ?? 0.0;
  
  // BEDINGUNG 1: Einspeisung > 0
  if($E<=3.0) continue;

  $pvStop = pvStopOnDay($archiveID,$pvIDs,$dayStart,$dayEnd);
  if($pvStop===null) $pvStop = strtotime("$d ".$fallbackEveningHour.":00:00");

  // BEDINGUNG 2: SoC ≤ Limit±3% (heute Abend ODER morgen früh)
  $hitToday=false; $hitTomorrow=false; $eveMin=null; $morMin=null;
  
  // Abend-Check (heute nach PV-Stop)
  $m = intval(date('n',$dayStart));
  $socMonth = $socLogsByMonth[$m] ?? [];
  $cntE=0; 
  $limToday = $limitPerDay[$d] ?? 10;
  $hitToday = socHitLimitInRange($socMonth, $pvStop, $dayEnd, $limToday, $cntE, $eveMin);

  // Morgen-Check (nächster Tag bis PV-Start)
  if($i+1 < count($allDays)){
    $dNext = $allDays[$i+1];
    $nStart=strtotime("$dNext 00:00:00");
    $nEnd  =strtotime("$dNext 23:59:59");
    $pvStart = pvStartNextDay($archiveID,$pvIDs,$nStart,$nEnd);
    if($pvStart===null) $pvStart = strtotime("$dNext ".$fallbackMorningHour.":00:00");
    
    $m2=intval(date('n',strtotime($dNext)));
    $socMonth2 = $socLogsByMonth[$m2] ?? [];
    $cntM=0;
    $limNext = $limitPerDay[$dNext] ?? $limToday;
    $hitTomorrow = socHitLimitInRange($socMonth2, $nStart, $pvStart, $limNext, $cntM, $morMin);
  }

  // BEIDE Bedingungen erfüllt?
  if($hitToday || $hitTomorrow){
    $matches[] = [
      'day'=>$d,'E'=>$E,
      'hitToday'=>$hitToday,'eveMin'=>$eveMin,'limToday'=>$limToday,
      'hitTomorrow'=>$hitTomorrow,'morMin'=>$morMin,'limNext'=>$limNext??$limToday
    ];
  }
}

// =================== Ausgabe ===================
usort($matches,function($a,$b){ return $b['E'] <=> $a['E']; });
echo "Top 15 Matches (Einspeisung>3kWh UND SoC≤Limit±3%):\n";
echo "Tag          | E(kWh) | heute(lim%) | morgen(lim%)\n";
for($i=0;$i<min(15,count($matches));$i++){
  $m=$matches[$i];
  printf("%s | %6.2f | %s(%.0f%%) | %s(%.0f%%)\n",
     $m['day'],$m['E'],
     ($m['hitToday']?'JA':'NEIN'),$m['limToday'],
     ($m['hitTomorrow']?'JA':'NEIN'),$m['limNext']);
}

$sumE=0.0; foreach($matches as $m){ $sumE+=$m['E']; }
$vermieden = $sumE*$preisBezug;
$verguet   = $sumE*$preisEinspeise;
$netto     = $vermieden - $verguet;

echo "\nSummary $rangeLabel: matches=".count($matches).", Potenzial_kWh=".number_format($sumE,2)."\n";
echo "Kosten: vermieden=".number_format($vermieden,2)."€, vergütung=".number_format($verguet,2)."€, netto=".number_format($netto,2)."€\n";
if($netto>0){
  $amort = $invest/$netto;
  echo "Amortisation (ohne Hochrechnung): ".number_format($amort,2)." Jahre\n";
}else{
  echo "Amortisation: nicht sinnvoll (netto <= 0)\n";
}
echo "→ ZEIGT: Zusätzlicher Akku würde ".number_format($sumE,1)."kWh/Jahr speichern!\n";
?>

das Script durchsucht rückwirkend den angegebenen Zeitraum im Archiv und holt sich alles, dauert also einen Moment

Auch ein interessanter Ansatz. Es führen halt verschiedene Wege nach Rom :slight_smile:

Für das Modul selbst belasse ich es lieber vorerst bei der stundenbasierten Simulation, damit die Bedienung und vor allem die Interpretation der Ergebnisse nicht zu komplex werden.

Was mir aufgefallen ist, dass die Berechnung der Amortisationszeit wahrscheinlich von 1 Jahr Daten/Simulation ausgeht und nicht den konfigurierten Zeitraum nimmt. Zumindest kann ich sie ca. vierteln, wenn ich vier Jahre Daten in die Sim packe

Du hast Recht, bei Auswertungszeiten ungleich einem Jahr wurde die Amortisationszeit nicht korrekt behandelt.

Ich habe eine neue Beta abgestellt, die auch abweichende Auswertungszeiten korrekt berücksichtigt.

Hallo Burkhard,

dein Modul kommt leider zu spät für mich, ich habe vor Jahren eine Anker E1600 und 2x400W Solarzellen gekauft und aufgezeichent. Es lohnt sich nach Jahren.
Jetzt sind die Preise teilweise deutlich günstiger, und man müsste mal was großes machen. :grinning_face:

Thomas

Ja, mit wenig Einsatz kann man schon eine Menge erreichen - zumindest wenn man es selber machen kann. Da hast du doch die besten Voraussetzungen :slight_smile:

Kurz vor der Rente, fehlt es an etwas … :joy:

Thomas - der das WoMo fertig macht. (und das bei den Diesel Preisen… :rofl:)

ps auch da macht die Sonne beide Batterien voll, für 12V und 230V

Das wäre toll!
Offtopic: Ich dachte man kann den HVM nur die ersten beiden Jahre erweitern?

Momentan funktioniert es mit der aktuellen Modbusimplementierung noch nicht. Aber @paresy hat schon etwas in Vorbereitung. Ich hoffe, dass es dann klappt.

Das war auch mein Kenntnisstand bislang. Ich hatte auch nach 2,5 Jahren schon mal ohne Probleme ein Modul erweitert.
Zuletzt hatte ich es aber nirgends mehr gelesen und so habe ich es einfach mal probiert. Mein Eindruck nach ein paar Tages ist gut.

Was muss ich denn tun, um diese Version zu laden? Aktuell scheint bei mir im module Verzeichnis die 1.03 vom 17.3. installiert zu sein. Auf git sehe ich die 1.04 mit den Änderungen. Über den Store funktioniert es nicht, da hier keine Beta angeboten wird.

Ich habe die 1.04 jetzt in den Store als Beta hochgeladen.