Berechnungsreferenz
eedc Berechnungsreferenz
Abschnitt betitelt „eedc Berechnungsreferenz“Version 4.0 | Stand: 2026-07-25
Dieses Dokument beschreibt alle Berechnungsketten im eedc-System: von den Eingabefeldern über die Berechnungslogik bis zur Anzeige im Frontend. Es dient als Referenz zur Fehlersuche und zum Verständnis der Datenflüsse.
Fachliche Fassung für Anwender: Die Wärmepumpen-, Klimaanlagen- und Heizstab-Formeln aus §3.5 sind in Wärme & Klima ohne Formelzeichen beschrieben — samt der Frage, warum eine Arbeitszahl verschwindet.
Inhaltsverzeichnis
Abschnitt betitelt „Inhaltsverzeichnis“- Datenmodell (3 Schichten)
- Konstanten
- Berechnungsketten nach Thema
- Prognosen (Aussichten)
- Tarif-System (Spezialtarife)
- Investitionstyp-spezifische Berechnungen (ROI-Dashboard) 6b. Energieprofil-Berechnungen (Tages-Aggregation)
- Debugging-Leitfaden
1. Datenmodell (3 Schichten)
Abschnitt betitelt „1. Datenmodell (3 Schichten)“Schicht 1: Rohdaten (Eingabe)
Abschnitt betitelt „Schicht 1: Rohdaten (Eingabe)“| Tabelle | Felder | Quelle | Beschreibung |
|---|---|---|---|
Monatsdaten | einspeisung_kwh, netzbezug_kwh | Zählerwerte (manuell/HA) | Anlagen-Energiebilanz |
InvestitionMonatsdaten | verbrauch_daten (JSON) | Manuell/Wizard/HA | Pro Komponente: PV-Erzeugung, Speicher, WP, E-Auto, etc. |
Strompreis | netzbezug_arbeitspreis_cent_kwh, einspeiseverguetung_cent_kwh, grundpreis_euro_monat, verwendung | Manuell | Tarife mit Gültigkeitszeitraum |
Investition | anschaffungskosten_gesamt, parameter (JSON) | Manuell | Kosten, technische Parameter |
Anlage | leistung_kwp, steuerliche_behandlung, ust_satz_prozent | Manuell | Anlage-Stammdaten |
PVGISPrognose | monatswerte, module_monatswerte, jahresertrag_kwh | PVGIS API | SOLL-Werte pro Monat/Modul |
TagesEnergieProfil | pv_kw, verbrauch_kw, einspeisung_kw, netzbezug_kw, batterie_kw, soc_prozent, komponenten (JSON) | Scheduler/Monatsabschluss | 24 Zeilen/Tag, stündliche kW-Werte + Wetter |
TagesZusammenfassung | ueberschuss_kwh, defizit_kwh, peak_pv_kw, batterie_vollzyklen, performance_ratio | Aggregiert aus TagesEnergieProfil | 1 Zeile/Tag, Tagessummen + KPIs |
Legacy-Felder (NICHT neu befüllen):
Monatsdaten.batterie_*- NutzeInvestitionMonatsdaten(Speicher)Monatsdaten.pv_erzeugung_kwh- kein Schreibziel für neuen Code (Pro-Modul-Werte gehören inInvestitionMonatsdaten) und seit 2026-07-29 auch keine allgemeine Lesequelle mehr: das Feld trägt den manuell erfassten oder importierten PV-Gesamtwert eines Monats und ist ausschließlich Eingang des Read-time-SoTcore/berechnungen/pv_verteilung.py(resolve_pv_je_modul). Der füllt damit die Lücken der Module ohne eigenen Wert und kennzeichnet sie als gerechnet. Wer nur einen Gesamt-Sensor hat, pflegt weiterhin ausschließlich hier. Jede einzelne Berechnung liest die Pro-Modul-Schicht bzw. deren Summe — nie das Feld selbst. Ladepfad:services/pv_monatswerte.py.
Seit 2026-07-31 ist die Lesequelle nicht mehr
lade_pv_je_monat, sondern eine Schicht darüber:services/monats_fakten.py::lade_monats_fakten(ADR-002/P10, Konzept). Sie liefert die ganze Monatszeile kanonisch aufgelöst — die PV ist darin ein Feld (erzeugung.pv_module_kwhbzw.erzeugung.pv_kwh), daneben stehen Zähler, Speicher, E-Mobilität, Wärmepumpe, Sonstiges, Tarif, §51 und die Verbrauchs-Kennzahlen. Sie ruftlade_pv_je_monat(die P7-Regel bleibt unverändert), wendet aber zusätzlich einmal alle Zeitfilter (aktiv· Anschaffung · Stilllegung) und den Dienstwagen-Filter an. Wer eine abgeleitete Monatsgröße auswertet, nimmt sie von dort;lade_pv_je_monatdirekt zu rufen bleibt richtig, wo nur die Pro-Modul-PV gebraucht wird (String-Vergleich, PV-Diagnose). Ausgenommen sind Schreib-, Import- und Checker-Pfade — die Schicht ist reines Lesen.Die Migration läuft sichtweise (
KONZEPT-MONATS-FAKTEN.md§10): umgehängt sind Aussichten, Jahresbericht-PDF und der Investitions-ROI (S2). Der baumweite Wächter wird mit S5 scharf gestellt.
Achtung, ein Name für zwei Größen:
pv_erzeugung_kwhbezeichnet drei verschiedene Dinge, je nachdem, wo es steht — die DB-SpalteMonatsdaten.pv_erzeugung_kwh(manuelles Gesamt-Aggregat, s. o.), den Schlüssel inInvestitionMonatsdaten.verbrauch_daten(Erzeugung dieses einen Moduls) und das Response-Feld von/monatsdaten/aggregiert(PV-Module + Balkonkraftwerk). Der Identifier bleibt bewusst unverändert — er ist zugleich MQTT-Topic-Segment, CSV-Spaltenname und Backup-Feld. Siehe Glossar.⛔ Und eine vierte Stelle, die es NICHT gibt: der Wechselrichter. Er ist kein PV-Erzeuger —
PV_ERZEUGER_TYPEN = ("pv-module", "balkonkraftwerk"), baumweit. Bis 2026-08-24 bot die Eingabe-Registry trotzdem einwechselrichter/pv_erzeugung_kwhan; gelesen wurde es von niemandem (gemessen: 900 kWh dort ⇒ 0,0 kWh Anlagen-PV), und weil es der Alternativ-Gruppepv_energieangehörte, setzte ein dort zugeordneter Zähler den Anlagen-Zähler und das Modul-Feld auf „bereits an anderer Stelle zugeordnet”. Ergebnis: drei PV-Quellen inaktiv, keine wirksam, und die Live-Kachel fiel auf die Hochrechnung aus der Leistung zurück (#388, rund 31 % zu hoch). Das Feld trägt seithernur_bestand— es erscheint nur noch, solange eine alte Zuordnung daran hängt, und der Daten-Checker sagt, wohin sie gehört. Wer die Gruppe erweitern will, liest zuerst den Wächtertest_wechselrichter_kein_pv_erzeuger.py::test_es_gibt_genau_drei_pv_energie_quellen: er hält Eingabe-Registry und Rechen-Kern gegeneinander und meldet rot, sobald sie auseinanderlaufen.
Schicht 2: Berechnungslogik
Abschnitt betitelt „Schicht 2: Berechnungslogik“| Datei | Funktionen | Beschreibung |
|---|---|---|
services/monats_fakten.py | lade_monats_fakten(), finanz_zeile_eingabe(), kennzahlen_aus_fakten() | Eingabe-Aufbereitung, keine Formel (ADR-002/P10): löst die Monatszeile einmal auf und ruft die SoT-Helfer. Vorschaltet jeder aggregierenden Lese-Sicht |
core/calculations.py | berechne_monatskennzahlen(), berechne_speicher_einsparung(), berechne_eauto_einsparung(), berechne_waermepumpe_einsparung(), berechne_roi(), berechne_ust_eigenverbrauch() | Reine Berechnungsfunktionen ohne DB-Zugriff |
api/routes/cockpit.py | 6 Endpoints | Aggregation aller Daten für Dashboard |
api/routes/aussichten.py | 4 Endpoints | Prognosen und Finanzberechnungen |
api/routes/investitionen.py | ROI-Dashboard | PV-System-Gruppierung und ROI pro Komponente |
api/routes/strompreise.py | lade_tarife_fuer_anlage() | Multi-Tarif-Lookup mit Fallback |
utils/sonstige_positionen.py | berechne_sonstige_summen() | Strukturierte Erträge/Ausgaben |
services/energie_profil_service.py | aggregate_day(), rollup_month(), backfill_range() | Tages-Aggregation + Monats-Rollup |
Schicht 3: Frontend-Anzeige
Abschnitt betitelt „Schicht 3: Frontend-Anzeige“Die API-Endpoints sind unverändert; die Sicht (Spalte „Wo in v4”) folgt der neuen Achsen-Navigation (Cockpit = Zeit-Achse, Komponenten = Was-Achse, Auswertungen = Wie-Achse — siehe Bedienung):
| Wo in v4 | API-Endpoint | Angezeigte Kennzahlen |
|---|---|---|
| Cockpit → Monat/Jahr | GET /api/cockpit/uebersicht/{id}?jahr= | Autarkie, EV-Quote, Netto-Ertrag, Rendite, CO2 |
| Auswertungen → Prognose | GET /api/cockpit/prognose-vs-ist/{id}?jahr= | Performance Ratio pro Monat |
| Cockpit → Jahr/Gesamt · Auswertungen → CO₂ (§4.4) | GET /api/cockpit/nachhaltigkeit/{id} | CO2-Zeitreihe (Block „CO₂-Bilanz”), Äquivalente, Amortisation — die eine CO₂-Quelle beider Sichten (§3.8) |
| Komponenten (je Typ) | GET /api/cockpit/komponenten-zeitreihe/{id} | Speicher-Effizienz, WP-JAZ, E-Auto PV-Anteil |
| Komponenten → PV-Anlage | GET /api/cockpit/pv-strings/{id}?jahr= | SOLL vs IST pro String |
| Auswertungen → ROI | GET /api/investitionen/roi/{id} | ROI%, Amortisation pro System |
| Auswertungen → Tabelle | GET /api/monatsdaten/aggregiert/{id} | Spalten-Explorer, Vorjahres-Delta |
| Cockpit → Aussicht | GET /api/aussichten/kurzfristig/{id} | 7-Tage PV-Prognose + SFML-Linie |
| Cockpit → Aussicht | GET /api/aussichten/langfristig/{id} | 12-Monats-Prognose |
| Cockpit → Aussicht | GET /api/aussichten/trend/{id} | Degradation, Jahresvergleich |
| Auswertungen → Finanzen | GET /api/aussichten/finanzen/{id} | Amortisations-Fortschritt, Prognose |
2. Konstanten
Abschnitt betitelt „2. Konstanten“Definiert in core/calculations.py:
| Konstante | Wert | Einheit | Verwendung |
|---|---|---|---|
CO2_FAKTOR_STROM_KG_KWH | 0.38 | kg CO2/kWh | Deutscher Strommix |
CO2_FAKTOR_BENZIN_KG_LITER | 2.37 | kg CO2/L | Benzinverbrennung |
CO2_FAKTOR_GAS_KG_KWH | 0.201 | kg CO2/kWh | Erdgasverbrennung |
CO2_FAKTOR_OEL_KG_KWH | 0.266 | kg CO2/kWh | Heizölverbrennung |
SPEICHER_ZYKLEN_PRO_JAHR | 250 | Vollzyklen | Für Speicher-Prognose |
Definiert in api/routes/aussichten.py:
| Konstante | Wert | Verwendung |
|---|---|---|
DEFAULT_SYSTEM_LOSSES | 0.14 (14%) | Kurzfrist-PV-Prognose |
TEMP_COEFFICIENT | 0.004 (0.4%/°C) | Leistungsabnahme über 25°C |
| Konfidenz-Faktor | 0.15 (15%) | Langfrist-Konfidenzband |
Hardcodierte Werte in cockpit.py:
| Wert | Verwendung |
|---|---|
| Gas-Preis: 10.0 ct/kWh | WP-Ersparnis (vs. Gas) |
| Gas-Wirkungsgrad: 0.9 (90%) | WP CO2-Vergleich |
| Benzin-Verbrauch: 7.0 L/100km | E-Mob-Ersparnis |
| Benzin-Preis: 1.80 EUR/L | E-Mob-Ersparnis (Cockpit-Fallback) |
3. Berechnungsketten nach Thema
Abschnitt betitelt „3. Berechnungsketten nach Thema“3.1 Energie-Bilanz (Monatskennzahlen)
Abschnitt betitelt „3.1 Energie-Bilanz (Monatskennzahlen)“Funktion: berechne_monatskennzahlen() in core/calculations.py
Verwendet in: Cockpit-Übersicht (inline Berechnung), Monatsdaten-Anzeige
Eingabefelder
Abschnitt betitelt „Eingabefelder“| Feld | Quelle | Tabelle |
|---|---|---|
einspeisung_kwh | Zähler | Monatsdaten |
netzbezug_kwh | Zähler | Monatsdaten |
pv_erzeugung_kwh | PV-Module | InvestitionMonatsdaten.verbrauch_daten (Typ: pv-module) |
batterie_ladung_kwh | Speicher | InvestitionMonatsdaten.verbrauch_daten (Typ: speicher) |
batterie_entladung_kwh | Speicher | InvestitionMonatsdaten.verbrauch_daten (Typ: speicher) |
v2h_entladung_kwh | E-Auto V2H | InvestitionMonatsdaten.verbrauch_daten (Typ: e-auto) |
einspeiseverguetung_cent | Tarif, ggf. Monatswert | Strompreis.einspeiseverguetung_cent_kwh; bei variabler Vergütung schlägt Monatsdaten.einspeise_durchschnittspreis_cent (#392, resolve_einspeise_preis_cent) |
netzbezug_preis_cent | Tarif | Strompreis.netzbezug_arbeitspreis_cent_kwh |
grundpreis_euro_monat | Tarif | Strompreis.grundpreis_euro_monat |
netzbezug_durchschnittspreis_cent | HA-Sensor oder Monatsdaten | Dynamischer Ø-Preis |
leistung_kwp | Anlage | Summe aller Investition.leistung_kwp (pv-module) |
Formeln
Abschnitt betitelt „Formeln“Erzeugung_gesamt = PV_Erzeugung + BKW + sonstige_Erzeuger (hinter dem Zähler)Direktverbrauch = max(0, Erzeugung_gesamt - Einspeisung - Batterie_Ladung)Eigenverbrauch = Direktverbrauch + Batterie_Entladung + V2H_EntladungGesamtverbrauch = Eigenverbrauch + NetzbezugEV-Quote (%) = Eigenverbrauch / Erzeugung_gesamt * 100 (wenn Erzeugung > 0)Autarkie (%) = Eigenverbrauch / Gesamtverbrauch * 100 (wenn GV > 0)Spez. Ertrag = PV_Erzeugung / Leistung_kWp (kWh/kWp, NUR PV; zwei Varianten, s. u.)
Einspeise-Erlös (EUR) = (Einspeisung - Einspeisung_neg_Preis) * Einspeisevergütung / 100Netzbezug-Kosten (EUR) = Netzbezug * Netzbezug_Preis / 100 + GrundpreisArbeitspreis-Kosten (EUR)= Netzbezug * Netzbezug_Preis / 100 (ohne Grundpreis, reiner Ausweis)EV-Ersparnis (EUR) = Eigenverbrauch * Netzbezug_Preis / 100Netto-Ertrag (EUR) = Einspeise-Erlös + EV-ErsparnisCO2-Einsparung (kg) = PV_Erzeugung * 0.38 (VERALTET — s. Kasten)⚠ Die CO₂-Zeile dieser Funktion ist NICHT der Kanon.
berechne_monatskennzahlenträgt noch die vor DI-2 gültige Formel (Erzeugung statt Eigenverbrauch, ohne WP und E-Mobilität). Sie wird ausschließlich vonGET /api/monatsdaten/{id}alskennzahlen.co2_einsparung_kgausgeliefert und dort von keiner Sicht gelesen (gemessen 2026-07-31: das Feld existiert im Client-TypMonatsKennzahlen, es gibt keinen Leser). Sie bewegt also keine angezeigte Zahl — sie steht hier, damit niemand sie für die gültige Definition hält. Der Kanon ist §3.8.
Zwei Kostenzahlen, eine Rechnung —
netzbezug_kosten_eurovs.netzbezug_arbeitspreis_kosten_euro: verrechnet wird immer die Gesamtsumme inkl. Grundpreis; sie ist das, was auf der Rechnung steht, und hängt an T-Konto, Netto-Ertrag und den Finanz-Sichten. Daneben steht der reine Arbeitspreis-Anteil als Ausweis — kein zweiter Kostenposten. Er gehört überall dorthin, wo eine Sicht kWh und € so nebeneinander stellt, dass ein Leser sie dividiert: dann muss der Ø-Preis herauskommen. Die Ø-Preis-Kachel in Cockpit → Monat tat das mit den Gesamtkosten nicht (559 kWh · 210,45 € ⇒ 37,6 ct statt 33 ct; Forum simon42 #89667). Faustregel: neben einem Preis steht der Arbeitspreis-Anteil, in einer Kostenaufstellung die Gesamtsumme.Welcher Preis gilt: bei einem flexiblen Tarif der verbrauchsgewichtete Monatsdurchschnitt (
Monatsdaten.netzbezug_durchschnittspreis_cent), sonst der Tarif-Arbeitspreis. Das gilt für Netzbezug-Kosten, EV-Ersparnis und — ohne eigenen WP-Tarif — auch für die WP-Ersparnis. Bis v4.0.6 nahm der laufende Monat hier den Tarifpreis, während Vorjahres-Vergleich und die per-Investition-Details schon den Durchschnitt nahmen; derselbe Monat trug damit je nach Sicht zwei Beträge.
§51 EEG im Einspeise-Erlös: Einspeisung_neg_Preis sind die kWh, die in Stunden
mit negativem Börsenpreis eingespeist wurden — für betroffene Anlagen entfällt dafür
die Vergütung (Herleitung des Volumens: Abschnitt „§51 EEG (Negativpreis-Analyse)”). Ist
die Anlage nicht §51-pflichtig oder liegt keine Strompreis-Mitschrift vor, ist der
Wert null und es wird nichts abgezogen. Der Abzug gilt überall gleich, und es
gibt dafür eine Implementierung: core/berechnungen/einspeise_erloes.py. Bis
2026-08-04 stand daneben ein Frontend-Spiegel (lib/calculations.ts::calcEinspeiseErloes)
für Auswertungen → Finanzen + Tabelle; er ist mit Fund N-22 entfallen, weil die
Monats-Finanzzeile jetzt fertig aus /monatsdaten/aggregiert kommt.
Netzpunkt-Bilanz (Erzeugung_gesamt): Am EINEN Netzanschluss messen die Zähler
(Einspeisung/Netzbezug) die Summe aller dahinter liegenden Erzeuger. Deshalb
geht in die Eigenverbrauchs-/Autarkie-Ableitung die Gesamterzeugung ein —
PV-Module + Balkonkraftwerk + sonstige Erzeuger (z. B. Mini-BHKW). Würde ein
Erzeuger ignoriert, drückte der gemessene Einspeise-Zähler Direktverbrauch zu
niedrig (auf 0 geklemmt) und Autarkie/EV-Quote würden unterschätzt. SoT-Helper:
core/berechnungen/energie.erzeugung_hinter_zaehler_kwh (ADR-001).
Die Größe ist ein eigenes Response-Feld, kein Rechenschritt je Sicht. /monatsdaten/aggregiert
liefert sie als erzeugung_hinter_zaehler_kwh mit — vorher summierte jede Sicht die Einzelteile
selbst, und genau dort entstand die Drift (zwei unterschiedlich hohe Stapel im Komponenten-Hub).
Die Felder derselben Antwort:
| Feld | Bedeutung |
|---|---|
pv_module_kwh | nur die PV-Module (bis v4.0.0 pv_anlage_kwh — irreführend, weil „PV-Anlage” im Produkt sonst die ganze Anlage inklusive Balkonkraftwerk meint) |
bkw_kwh | nur Balkonkraftwerk(e) |
sonstige_erzeugung_kwh | „Sonstiges” mit Kategorie Erzeuger (z. B. Mini-BHKW) |
pv_erzeugung_kwh | pv_module_kwh + bkw_kwh — nicht die gleichnamige DB-Spalte (s. Schicht 1) |
erzeugung_hinter_zaehler_kwh | Σ aller drei Erzeuger-Felder = der Nenner der EV-Quote |
Ein Balkonkraftwerk mit zugeordneten PV-Modulen (ADR-002/P11)
Abschnitt betitelt „Ein Balkonkraftwerk mit zugeordneten PV-Modulen (ADR-002/P11)“Ein Balkonkraftwerk trägt eine Ausrichtung und eine Neigung. Wer seine Module über Eck hängen hat (Balkon und Terrasse, Ost und West), legt sie deshalb als PV-Module an und ordnet sie unter Gehört zu dem Balkonkraftwerk zu — jedes Modul trägt dann seine eigene Ausrichtung, und die Prognose rechnet sie getrennt.
Damit tritt das Balkonkraftwerk drei Größen an seine Module ab, und zwar immer gemeinsam: die Nennleistung (die Anzeige am Gerät zeigt die Σ seiner Module), die Ausrichtung/Neigung und die Erzeugung. Für die Erzeugung gilt dieselbe Leserichtung wie beim Anlagen-Aggregat (P7), nur eine Ebene tiefer — die Präzedenz hat damit drei Stufen:
- der gemessene Wert des Moduls gewinnt,
- der Monatswert des Balkonkraftwerks füllt die Lücken seiner Module (nach kWp verteilt),
- das Anlagen-Aggregat
Monatsdaten.pv_erzeugung_kwhfüllt, was danach noch offen ist.Der Monatswert am Balkonkraftwerk bleibt also voll erfassbar und zuordenbar — bei einem Set ist der Wechselrichter oft der einzige Zähler, und die Module darunter haben gar keinen eigenen. Er zählt dann aber nicht mehr zusätzlich in
bkw_kwh:pv_erzeugung_kwhwäre sonst doppelt, mit Folgen für Autarkie, EV-Quote, CO₂, Finanzen, Community-Datensatz und HA-Sensoren.⚠ Nicht abgetreten wird die AC-Grenze. Die Wechselrichter-Leistung (W) gehört dem Gerät und gilt weiter für die Summe seiner Module — sie ergibt sich gerade nicht aus ihnen. Das sind zwei unabhängige Grenzen: 800 VA am Wechselrichter-Ausgang und (regulatorisch) 2.000 Wp an den Modulen; nur die zweite wächst mit den Kindern. Für die Kappung wechselt das Balkonkraftwerk deshalb die Rolle vom Erzeuger zum Träger — genau die Rolle, die ein Wechselrichter für seine Strings hat.
Ein Balkonkraftwerk ohne zugeordnete Module rechnet unverändert wie bisher. SoT:
core/berechnungen/erzeuger_traeger.py. Melder: Discussion #366, Forum T89667 #172.Dieselbe Abtretung gilt für die Wirtschaftlichkeit (ab v4.0.19, F-33/#381)
Abschnitt betitelt „Dieselbe Abtretung gilt für die Wirtschaftlichkeit (ab v4.0.19, F-33/#381)“Die ROI-Sicht fasst einen Wechselrichter mit seinen Modulen und seinem DC-Speicher seit jeher zu einer Systemzeile zusammen: die drei Positionen teilen sich eine Energiemenge, und ihre Ersparnisse sind deshalb nicht addierbar. Für ein Balkonkraftwerk als Träger galt das nicht — dort bekam jede Ebene ihre eigene Zeile, und die Gesamtzeile addierte sie. An einer gemeldeten Anlage (BKW + Modul-Kind + Speicher-Kind) standen so 1.079 €/Jahr statt der gemessenen 606 €, und die Gesamt-Amortisation bei 1,9 statt 3,3 Jahren. Das Modul-Kind trug zusätzlich die Aufforderung „(ohne WR) — bitte zuordnen”, obwohl es zugeordnet war.
Seither ist Trägergerät die Rolle, nicht der Typ: ein Balkonkraftwerk mit Kindern ist Systemkopf wie ein Wechselrichter. Es tritt seine Ersparnis genauso an die Module ab, wie es oben seine Erzeugung abtritt — hat es keine Modul-Kinder (nur einen Speicher), trägt es sie weiterhin selbst und steuert sie der Systemzeile bei. Die Zeile trägt Name und Typ ihres Kopfes, damit ein Balkonkraftwerk nicht als „PV-System” mit fremdem Namen erscheint.
⚠ Die pauschale BKW-Schätzung ist davon unberührt und bleibt eine Schätzung (0,9 kWh/Wp, 80 % Eigenverbrauch). Sie greift nur noch für ein Balkonkraftwerk ohne Modul-Kinder — dort, wo keine gemessenen Modulwerte existieren. Melder: Issue #381 (azywietz-web).
Auch die PDF-Berichte rechnen so. Der Jahresbericht leitete Eigenverbrauch, Autarkie und EV-Quote bis v4.0.0 allein aus der PV-Erzeugung ab, während der Einspeise-Zähler daneben die Summe aller Erzeuger misst — bei einer Anlage mit sonstigem Erzeuger fielen die Werte dort zu niedrig aus und widersprachen dem Cockpit. Seit v4.0.1 nutzen alle Bilanz-Pfade (Cockpit, Monatsbericht, Live, PDF-Jahresbericht, HA-Sensoren) dieselbe Größe.
Spezifischer Ertrag — zwei Größen, ein Formelname. Die Roh-Division oben ist nur eine davon:
| Größe | Rechenweg | Wo |
|---|---|---|
| Annualisiert | saisonal gewichtet (PVGIS-Monatsverteilung) und mit der im jeweiligen Monat tatsächlich installierten Leistung — vergleichbar über verschieden lange Zeiträume | Cockpit, HA-Export, Community-Vergleich (SoT core/berechnungen/spez_ertrag.py) |
| Zeitraum | Erzeugung des Berichtszeitraums ÷ Anlagen-Nennleistung, ohne Normierung — summiert sich über mehrere Jahre auf | PDF-Jahresbericht (dort seit v4.0.1 als „Spez. Ertrag (Zeitraum)” beschriftet), Monatsbericht |
Beide Zahlen sind richtig, sie beantworten verschiedene Fragen. Eine Angleichung der Rechnung steht aus, weil dieselbe Kennzahl im Community-Vergleich steht.
Achsen-Trennung (bewusst): PV-eigene Kennzahlen (spez. Ertrag, Performance-
Ratio, SOLL/IST, kWp) nutzen nur PV_Erzeugung, nicht Erzeugung_gesamt — ein
sonstiger Erzeuger ist energetisch Erzeuger, aber kein PV-Modul. Ebenso bleibt
CO₂/Wirtschaftlichkeit quellenspezifisch: ein brennstoffbasierter Erzeuger (BHKW)
spart kein CO₂, sondern emittiert, und hat Brennstoffkosten — er bekommt daher keine
PV-artige CO₂-Ersparnis (bewertet als „nicht bewertet”, bis ein eigenes BHKW-Modell
existiert). Insel-Anlagen (kein Netzanschluss, kein Bezug/keine Einspeisung)
fallen nicht unter diese Bilanz — das ist ein Anlagen-Merkmal (eigenes KZ, geplant).
Messpunkt der Sensoren (DC vs. AC): Die Bilanz rechnet mit den Werten, die die Geräte
liefern — sie kann nicht wissen, wo gemessen wurde. Viele Hybrid-Wechselrichter (z. B.
E3DC) melden PV-Erzeugung und Speicher-Ladung/-Entladung DC-seitig (Modul- bzw.
Batterieklemme), Einspeisung und Netzbezug dagegen AC-seitig (Zähler). Dann stecken die
Wandlungsverluste der PV- und Speicherstrecke im bilanzierten Gesamtverbrauch: er liegt
typischerweise 3–5 % der Erzeugung über dem „Hausverbrauch”, den das Herstellerportal
ausweist — das rechnet seine Verluste intern heraus. Keiner der beiden Werte ist falsch, sie
beantworten verschiedene Fragen: eedc „was musste die Anlage liefern” (inklusive Verluste
— die richtige Basis für Autarkie, EV-Quote und Wirtschaftlichkeit, denn erzeugt und bezahlt
werden muss auch der Verlust), das Portal „was zogen die Verbraucher”.
Diagnose-Rezept für einen abgeschlossenen Tag:
Residuum = PV + Netzbezug + Entladung − Einspeisung − Ladung − Hausverbrauch(Portal)Liegt das Residuum bei wenigen Prozent der Erzeugung, ist es der Verlustanteil und kein Rechenfehler. Gemessenes Beispiel (E3DC, 03.07.2026, Issues #200/#340): 60,83 + 0,31 + 7,17 − 48,19 − 7,59 − 10,26 = 2,27 kWh = 3,7 % der Erzeugung — davon 0,42 kWh Batterie- Rundlauf (DC), der Rest DC→AC-Wandlung. Da die Verluste mit dem Durchsatz skalieren, liegt ein ertragsstarker Tag über dem Monatsschnitt.
Bewusst kein Hausverbrauchs-Sensor: eedc bietet kein Mapping eines fremden Hausverbrauchs-Sensors an. „Hausverbrauch” ist je Hersteller anders definiert (Verluste drin oder herausgerechnet, Wallbox enthalten oder nicht) — zwei Definitionen derselben Kennzahl würden Cockpit, Berichte und Community-Vergleich auseinanderlaufen lassen. Die Bilanz aus Zähler- und Komponentenwerten bleibt die eine Wahrheit; die Differenz wird erklärt, nicht durch eine zweite Datenquelle ersetzt.
Wichtig: Netto_Ertrag enthält NICHT den Abzug der Netzbezugskosten, da diese auch ohne PV angefallen wären.
3.2 Finanzen (Cockpit)
Abschnitt betitelt „3.2 Finanzen (Cockpit)“Endpoint: GET /api/cockpit/uebersicht/{anlage_id} in cockpit.py
Die Cockpit-Übersicht aggregiert alle Monatsdaten für ein Jahr (oder alle Jahre) und berechnet:
Finanzielle Kennzahlen
Abschnitt betitelt „Finanzielle Kennzahlen“Einspeise-Erlös = Σ(Einspeisung) * Einspeisevergütung / 100EV-Ersparnis = Σ(Eigenverbrauch) * Netzbezug_Preis / 100Netto-Ertrag = Einspeise-Erlös + EV-Ersparnis [- USt_Eigenverbrauch]BKW-Ersparnis = Σ(BKW_Eigenverbrauch) * Netzbezug_Preis / 100Sonstige-Netto = Σ(sonstige_ertraege) - Σ(sonstige_ausgaben)
Betriebskosten_Zeitraum = Σ(Betriebskosten_Jahr) * Anzahl_Monate / 12
Kumulative Ersparnis = Netto-Ertrag + WP-Ersparnis + E-Mob-Ersparnis + BKW-Ersparnis + Sonstige-Netto - Betriebskosten_Zeitraum
Jahres-Rendite (%) = Kumulative_Ersparnis / Investition_gesamt * 100Die Einspeisevergütung ist ein flacher Satz — eedc kennt keine EEG-Leistungsstaffel.
Einspeisevergütungist genau der Wert aus dem für den Monat gültigen Tarif (Strompreis.einspeiseverguetung_cent_kwh); es gibt keine Ableitung ausleistung_kwp, keine Stufengrenze und keine Aufteilung der eingespeisten Menge auf mehrere Sätze. Wer gestaffelt vergütet wird, trägt den nach kWp gewichteten Mischsatz ein — mathematisch identisch, weil das EEG nach installierter Leistung staffelt und nicht nach eingespeister Menge. Der einzige Automatismus ist der FallbackEINSPEISEVERGUETUNG_DEFAULT_CENT(core/wirtschaftlichkeit_defaults.py), der ausschließlich greift, wenn gar kein Tarif gepflegt ist — und genau das meldet der Daten-Checker. Anwendersicht: Einstellungen §2.2.Variable Einspeisevergütung (#392, seit 2026-08-22): Trägt der Tarif das Häkchen „Einspeisevergütung wechselt monatlich“ (
Strompreis.einspeisung_variabel, z. B. OeMAG-Marktpreis in Österreich), bietet der Monatsabschluss das FeldMonatsdaten.einspeise_durchschnittspreis_centan. Der gepflegte Monatssatz schlägt den Stammwert — aufgelöst ausschließlich überresolve_einspeise_preis_cent(api/routes/strompreise.py), mitis not None: 0 ct ist ein Wert. Monate ohne Eintrag rechnen mit dem Stammwert. Die Hochrechnung nach vorn (Aussichten-Prognose) und die ROI-Rechnung über die Lebensdauer nehmen bewusst weiter den heutigen Stammwert — künftige Monate haben keinen Monatswert (dieselbe Begründung wie beim Netzbezug, N-113). eedc holt den Satz nicht automatisch ab (keine länderspezifische Quelle); er wird eingetragen oder per CSV importiert (SpalteEinspeiseverguetung_Cent).
Kanonisches Finanz-Aggregat (SoT
core/berechnungen/finanz_aggregat.py): Netto-Ertrag, Einspeise-Erlös, EV-/BKW-Ersparnis und Sonstige-Netto werden per-Monat gerechnet und über die sichtbaren Monate summiert (nicht mit einem Ø-Preis) — bei Flex-Tarifen (Tibber/aWATTar/EPEX) laufen Monats-Preis und EV/Netzbezug-Split sonst auseinander (#326). Der naivenetto_ertrag_euro= Einspeise-Erlös + EV-Ersparnis + BKW-Ersparnis + Sonstige-Netto; Sites mit Zusatzlogik (Cockpit ziehtUSt_Eigenverbrauchab) bauen den Netto-Ertrag aus den Einzel-Komponenten selbst zusammen.
G19-1 — Sonstige Positionen auf Anlage-Ebene (ab v4.0):
Sonstige-Nettoumfasst jetzt auch die auf Anlage-Ebene (nicht nur pro Komponente) erfassten Positionen — siehe §3.10. Sie fließen als eigene T-Konto-Zeilen „Anlage — Sonstige Erträge/Ausgaben” in fünf Anlage-Finanz- Pfade (Cockpit-Monat/-Jahr, Übersichts-Netto, PDF-Jahresbericht, HA-Export-Netto-Sensor). Wichtig für Bestandsdaten: die früher rein informativenMonatsdaten.sonderkosten_euro(die nirgends rechneten) werden migriert und wirken ab v4.0 in den Finanz-Summen. Bewusste Inkonsistenz: Die Aussichten- und ROI-Pfade (investitions-zentrierte Amortisation, s. §3.6 / §4.4) sehen die Anlage-Positionen NICHT — sie fließen dort nicht in Bisherige-Erträge/Amortisation ein. Das ist so dokumentiert, kein Bug (offener Entscheid unter „Kennzahlen-Drift-Inventur”).
Grundgebühr / Zählergebühr (K3, ab v4.0): Die Grundgebühr (
Strompreis.grundpreis_euro_monat) steckt bereits in den Netzbezugskosten (s. §3.1); der Cockpit-Finanz-Teaser (Monat + Jahr) weist sie zusätzlich nachrichtlich aus („davon Grundgebühr: … €”). Die Zählergebühr (neues optionales Tarif-FeldStrompreis.zaehlergebuehr_euro_jahr) wird im Jahr-Modus als „Zählergebühr: … €/Jahr (nachrichtlich)” gezeigt, aber nicht in Kosten/Netto verrechnet — eine Einrechnung wäre ein eigener Kennzahlen-Entscheid.baueJahrAlsMonat: Grundgebühr = Σ, Zählergebühr = letzter Wert.
Cockpit-Finanzen-Block = Komponenten-Finanz-Tabelle (G20-1, ab v4.0): Der Finanzen-Block in Cockpit-Monat/-Jahr zeigt eine Zeile je Komponente (Reihenfolge = Typ-SoT) mit den Spalten Erträge (tatsächliche Zahlungsflüsse) · Einsparungen (kalkulatorisch/vermiedene Kosten) · Aufwand (inkl. anteilig umgelegter Betriebskosten, Speicher-Zeile inkl. Netzladungs-Kosten) · Saldo; die Summenzeile ist die Block-Kopf-Kennzahl (Kopf == sichtbare Summe). Diese Tabellen- Summe ist bewusst eine dritte, komponenten-attribuierte Netto-Semantik neben (a) dem kanonischen
netto_ertrag_euro(PV-Anlage: Einspeise-Erlös + EV-Ersparnis + BKW-Ersparnis + Sonstige-Netto) und (b)gesamtnettoertrag_euro(Einspeise-Erlös + EV-Ersparnis + WP-Ersparnis + E-Mob-Ersparnis − Netzbezug-Kosten). Sie fasst die Beiträge aller Komponenten zusammen und wird rein aus den vorhandenen T-Konto-Posten gebaut — keine neue Berechnung:netto_ertrag_euro, der HA-Export- Sensor und der PDF-Jahresbericht bleiben unangetastet. Netzbezug-Kosten und Grundgebühr stehen nachrichtlich (nicht im Saldo). Zusätzlich weist der Block als zweite Perspektive die Zeile „Ergebnis nach Stromrechnung” = Tabellen-Saldo − Netzbezug-Kosten (G20-4) aus — das Haushalts- ergebnis; der Komponenten-Saldo bleibt davon unberührt und ist weiterhin die Kopf-Kennzahl. (Die Vergleichs-AsymmetriegesamtnettoertragMonat vs. Vorjahr ist ein offener Punkt der Kennzahlen-Drift-Inventur, kein Bug.)
Anschaffungsdatum-Grenze auch im Vorjahres-Vergleich (DI-5/DI-2-C): Der Trend-Pfeil zum Vorjahr zieht die Vorjahres-Werte symmetrisch zum laufenden Monat — WP- und E-Mob-Ersparnis fließen nur für im jeweiligen Vorjahresmonat aktive Komponenten ein (
ist_aktiv_im_monat, also innerhalb Anschaffungs-→Stilllegungs-Fenster), und die energieseitige Vorjahres-Aggregation ist gleich gefiltert. Dienstwagen (ist_dienstlich) bleiben in beiden Jahren aus den E-Mob-Bilanzen. So vergleicht der Pfeil gleiche Komponenten-Mengen, statt Alt-Werte vor der Anschaffung mitzuzählen.
WP-Ersparnis im Cockpit
Abschnitt betitelt „WP-Ersparnis im Cockpit“WP-Ersparnis = (WP_Wärme / 0.9 * Gas_Preis - WP_Strom * WP_Preis) / 100Wobei:
WP_Wärme= Σ(heizenergie_kwh + warmwasser_kwh) aus InvestitionMonatsdatenWP_Strom= Σ(stromverbrauch_kwh) aus InvestitionMonatsdaten0.9= angenommener Gasheizungs-WirkungsgradGas_Preis= 10.0 ct/kWh (hardcodiert)WP_Preis= Spezialtarif waermepumpe (Fallback: allgemein)
E-Mob-Ersparnis im Cockpit
Abschnitt betitelt „E-Mob-Ersparnis im Cockpit“Benzin_Verbrauch = Σ(km_gefahren) * 7 / 100 (7 L/100km Annahme)Benzin_Kosten = Benzin_Verbrauch * 1.80 (1.80 EUR/L Annahme)Strom_Kosten = (Ladung_gesamt - Ladung_PV) * Wallbox_Preis / 100E-Mob-Ersparnis = Benzin_Kosten - Strom_KostenHinweis: Dienstliche E-Autos/Wallboxen (ist_dienstlich = true) werden NICHT in die E-Mob-Ersparnis eingerechnet. Deren Ladekosten fließen als kalkulatorische Ausgaben in sonstige_ausgaben_gesamt — Formel und Begründung stehen in §3.10 „Sonstige Positionen” unter Dienstliche Ladekosten — der PV-Anteil zählt dort zum Netzbezugspreis, nicht zur Einspeisevergütung.
G20-2 — Aggregat bei mehreren E-Autos = Σ der Einzel-Fahrzeuge: Die Gesamt-E-Mob-Ersparnis wird als Summe der pro Fahrzeug gerechneten Ersparnisse gebildet — jedes E-Auto mit seinem eigenen Vergleichsverbrauch (L/100 km) und Benzinpreis. Sie ist NICHT ein Einmal-Lauf über die Gesamt-Kilometer mit dem Parametersatz des ersten Fahrzeugs (das überschätzte die Ersparnis, sobald zwei E-Autos unterschiedliche Vergleichsverbräuche hatten). Bei genau einem E-Auto ist das Ergebnis unverändert. Die Per-Fahrzeug-Zeilen (T-Konto) rechneten schon immer je Fahrzeug korrekt; nur das aggregierte Cockpit-Feld ist jetzt symmetrisch dazu.
Kanonische Heimladungs-Quelle (ab Phase 2a): Ladung_gesamt und Ladung_PV der Heimladung kommen strukturell aus genau einer Quelle: existiert eine Wallbox-Investition mit Heimladung, ist sie die Quelle (Infrastruktur misst den Stromfluss am Ladepunkt); ohne Wallbox (Steckerlader/Schuko) liefert das E-Auto die Werte. Bei mehreren Wallboxen ist die Heimladung die Summe aller Wallbox-Ladepunkte. Diese Regel ist deterministisch (existiert eine Wallbox?), nicht magnitudenabhängig — der frühere Pool-/„größere Heimladung gewinnt”-Mechanismus entfällt. Die km-anteilige Aufteilung auf mehrere Fahrzeuge (Attribution) bleibt unverändert. Zentraler Helper: get_emob_heimladung_canonical().
Ø Verbrauch (kWh/100 km) — Quellen-Vorrang: Die Effizienz-KPI in E-Auto-Dashboard, Monatsbericht und Komponenten-Auswertung kommt aus einem Helper (core/berechnungen/emob.py, eauto_effizienz_100km):
1. gemessener Fahrverbrauch: verbrauch_kwh ÷ km × 100 (Vorrang, exakt)2. sonst Näherung aus Ladung: Ladung_gesamt ÷ km × 100 (Fallback)3. sonst: — (nie 0,0 erfinden)Die Ladungs-Näherung überschätzt den echten Fahrverbrauch (AC-Ladung an der Wallbox enthält Ladeverluste ~10–15 %, blendet SoC-Drift + nicht erfasste Fremdladung aus) — in der UI als „≈ aus Ladung (inkl. Ladeverluste)” gelabelt. Vorteil: funktioniert auch ohne Verbrauchssensor (den die wenigsten Fahrzeuge liefern). Alle Read-Sites zeigen denselben Wert; das Aggregat rechnet über die Summen (Σverbrauch / Σladung / Σkm), nicht über das Mittel der Monats-Prozente. Symmetrie abgesichert durch test_emob_readsite_symmetrie.py.
Hinweis Kraftstoffpreis (ab v3.17.0): Im Cockpit werden weiterhin die hardcodierten Defaults verwendet. In der Finanz-Prognose (Auswertungen → Finanzen), im HA-Sensor-Export und im PDF-Finanzbericht wird stattdessen pro Monat der echte Kraftstoffpreis aus Monatsdaten.kraftstoffpreis_euro verwendet (Quelle: EU Weekly Oil Bulletin). Fallback auf den statischen benzinpreis_euro-Parameter der Komponente wenn kein Monatswert vorhanden.
Investitionskosten (Mehrkosten-Ansatz)
Abschnitt betitelt „Investitionskosten (Mehrkosten-Ansatz)“PV-System-Kosten = Σ(Kosten) für pv-module, wechselrichter, speicher, wallbox, bkwWP-Mehrkosten = max(0, WP_Kosten - alternativ_kosten_euro) (Default: 8.000 EUR)E-Auto-Mehrkosten = max(0, E-Auto_Kosten - alternativ_kosten_euro) (Default: 35.000 EUR)Sonstige-Kosten = Σ(Kosten) für andere Typen
Investition_gesamt = PV-System + WP-Mehrkosten + E-Auto-Mehrkosten + Sonstige3.3 Speicher-Einsparung
Abschnitt betitelt „3.3 Speicher-Einsparung“Funktion: berechne_speicher_einsparung() in core/calculations.py
Verwendet in: ROI-Dashboard (investitionen.py)
Eingabefelder
Abschnitt betitelt „Eingabefelder“| Feld | Quelle |
|---|---|
kapazitaet_kwh | get_speicher_nutzbare_kapazitaet_kwh(inv) — netto, still auf brutto zurückfallend (A31-2/E17, s. u.). Greift nur im Prognose-Modus; mit gemessener Entladung übernimmt der Spread-Service und liest gar keine Kapazität. |
wirkungsgrad_prozent | Investition.parameter["wirkungsgrad_prozent"] (Default: 95) |
arbitrage_faehig | Investition.parameter["arbitrage_faehig"] (Default: false) — der Kanon; nutzt_arbitrage war der Name vor v3.25.0 |
lade_preis_cent | Investition.parameter["lade_durchschnittspreis_cent"] (Default: 12) |
entlade_preis_cent | Investition.parameter["entlade_vermiedener_preis_cent"] (Default: 35) |
Formeln
Abschnitt betitelt „Formeln“Ohne Arbitrage (Eigenverbrauchsoptimierung):
Wirkungsgrad = wirkungsgrad_prozent / 100Nutzbare_Speicherung = Kapazität * 250 Zyklen * WirkungsgradStandard_Spread = Netzbezug_Preis - EinspeisevergütungJahres-Einsparung = Nutzbare_Speicherung * Standard_Spread / 100Mit Arbitrage (70/30-Modell):
PV-Anteil (70%) = Nutzbare_Speicherung * 0.70Arbitrage-Anteil (30%) = Nutzbare_Speicherung * 0.30
PV-Einsparung = PV-Anteil * Standard_Spread / 100Arbitrage-Spread = Entlade_Preis - Lade_PreisArbitrage-Einsparung = Arbitrage-Anteil * Arbitrage_Spread / 100Jahres-Einsparung = PV-Einsparung + Arbitrage-EinsparungVollzyklen — eine Definition für alle Sichten
Abschnitt betitelt „Vollzyklen — eine Definition für alle Sichten“Vollzyklen = Entladung_kWh ÷ Kapazität_brutto_kWhSoT: core/berechnungen/speicher.py::vollzyklen. Alle Sichten rufen ihn auf —
Komponenten-Hub (investitionen/dashboards.py), Cockpit Tag (energie_profil/tage_werte.py),
Monat/Jahr (aktueller_monat.py) und Cockpit-Übersicht (cockpit/uebersicht.py),
PDF-Jahresbericht, HA-Sensor speicher_zyklen.
Gewächtert von backend/tests/test_speicher_zyklen_kapazitaets_basis.py (inkl. Drei-Pfad-Symmetrie),
test_tage_werte_symmetrie.py und test_speicher_kanon_symmetrie.py.
⚠ Diese Aufzählung war bis zum 2026-08-04 unvollständig — und genau die fehlende Zeile war die fehlerhafte.
cockpit/uebersicht.pyrechnete weiterhinLadung ÷ Kapazität; der Sweep vom 2026-07-28 hatte die Route übersehen, und weil ihr Wert damals keinen Client-Leser hatte, fiel es niemandem auf (#358 gab ihm einen). Lehre: eine Liste von Aufrufern in der Doku ist eine Behauptung über den Code — sie gehört mit einem baumweiten Grep belegt, sonst dokumentiert sie den Soll- statt den Ist-Zustand.
Warum die Entladung: Ein Vollzyklus meint die einmal entnommene Kapazität — die Größe, auf die sich Hersteller-Garantien beziehen, und unabhängig von den Wandlungsverlusten des Ladepfads. Sie ist außerdem ein Energiedurchsatz und damit über Tag → Monat → Jahr additiv.
Warum Brutto im Nenner: nutzbare_kapazitaet_kwh ist optional und meist nicht gepflegt; ein
Nenner, der je nach Pflegezustand wechselt, wäre schlimmer als ein durchgehend leicht konservativer
Wert. Im HA-Export ist das Netto-Feld reiner Fallback, falls die Brutto-Kapazität fehlt — die
Lese-Reihenfolge dort ist bewusst brutto → netto und bleibt es auch nach A31-2.
Auslastung — zeitraum-normierte Nutzung (#358 Phase 1)
Abschnitt betitelt „Auslastung — zeitraum-normierte Nutzung (#358 Phase 1)“Auslastungs-Basis = Kapazität_brutto_kWh × Tage_im_ZeitraumAuslastung [%] = Entladung_kWh ÷ Auslastungs-Basis × 100SoT: core/berechnungen/speicher.py::auslastungs_basis_kwh + auslastung_prozent.
Geliefert von aktueller_monat.py (Felder speicher_auslastungs_basis_kwh /
speicher_auslastung_prozent), angezeigt in Cockpit → Monat und → Jahr.
Warum die Basis ein eigenes Feld ist: Auslastungen mehrerer Monate lassen sich nicht
mitteln — ein Februar wiegt weniger als ein Juli, ein angefangener Monat noch weniger. Wer ein
Jahr bildet, summiert Entladung und Basis und teilt einmal. Ein Prozent-Mittelwert wäre die
Drift-Klasse, die diese Trennung von vornherein ausschließt (Beleg:
test_speicher_kanon_symmetrie.py::test_auslastungs_basis_ist_additiv_ueber_monate — 40 % und
61 % ergeben zusammen 50 %, nicht 50,5 %).
Im laufenden Monat zählen nur die abgelaufenen Tage. Sonst stünde am 3. eines Monats eine Auslastung von 10 %, die nichts über den Speicher aussagt, sondern über das Datum — ein voller Nenner gegen einen angefangenen Zähler, genau der Fall aus KONZEPT-UNVOLLSTAENDIGE-WERTE §3.
Kein Deckel bei 100 %: zwei Zyklen an einem Tag sind real, und die Zahl soll das sagen dürfen.
Ohne gepflegte Kapazität liefern beide Funktionen None — „unbekannt”, nicht 0.
Der Nutzen in Euro — Spread, nicht Voll-Strompreis
Abschnitt betitelt „Der Nutzen in Euro — Spread, nicht Voll-Strompreis“PV-Anteil der Entladung = Entladung − min(Entladung, Netzladung × η)Netz-Anteil der Entladung = min(Entladung, Netzladung × η)
Ersparnis = PV-Anteil × (Netzbezug − Einspeisevergütung) / 100 + Netz-Anteil × (Netzbezug − Ladepreis) / 100SoT: core/berechnungen/speicher_wirtschaftlichkeit.py::berechne_speicher_ersparnis.
Aufrufer: T-Konto (aktueller_monat.py::_baue_investition_financial), Speicher-Dashboard und
Sonstiges-Speicher (investitionen/dashboards.py), Aussichten. Gewächtert von
test_speicher_kanon_symmetrie.py (drei Achsen, mit absoluten Erwartungen — Symmetrie allein
ließe auch drei gleich falsche Zahlen durch, Lehre aus N-130).
Warum der Spread: die entladene kWh ersetzt Netzbezug, hätte aber sonst Einspeisevergütung erbracht. Die entgangene Vergütung ist eine reale Gegenposition (Drift-Audit A3, von Gernot am 2026-08-04 für #358 bestätigt).
Warum die Netzladung ausgenommen ist: sie hätte nie eingespeist werden können, der PV-Spread
gilt für sie nicht. Ihr Vorteil ist Netzbezug − Ladepreis; ohne gepflegten Ladepreis ist sie
kostenneutrale Durchleitung (z. B. Backup-Vorhaltung).
⚠ Zwei Fundstellen wichen bis zum 2026-08-04 ab — beide sichtbar:
aktueller_monat.pyrechneteEntladung × Netzbezug(bei 30/8 ct 36 % zu hoch),dashboards.pyden Spread inline auf der gesamten Entladung und wies den Arbitrage-Gewinn zusätzlich aus — der Komponenten-Hub addiert beide Posten, netzgeladene Energie zählte damit doppelt. Die Formel im Layer zu haben genügt nicht; sie ist erst durchgesetzt, wenn keine Inline-Kopie mehr danebensteht (ADR-001).
Brutto oder netto — wann welche Kapazität gilt
Abschnitt betitelt „Brutto oder netto — wann welche Kapazität gilt“Ein Speicher trägt zwei Kapazitäten, beide im Formular. Die Trennlinie läuft nicht zwischen „genau” und „ungefähr”, sondern zwischen zwei Fragen:
| Frage | Kapazität | Warum | Stellen |
|---|---|---|---|
| Wie oft wurde der Speicher umgeschlagen? | brutto (kapazitaet_kwh) | Bezugsgröße der Hersteller-Garantie; ein Nenner, der am Pflegezustand hängt, macht dieselbe Anlage unvergleichbar | Vollzyklen (alle Sichten), graue Last, Community-Datensatz, Anzeige/Beschreibung der Komponente |
| Wie viel Energie geht durch den Speicher? | netto (nutzbare_kapazitaet_kwh, still auf brutto zurückfallend) | Simuliert bzw. prognostiziert wird eine durchgefahrene Menge — und durch den Speicher geht nur der nutzbare Hub | Tages-Vorschau „Speicher voll um …” (Planungs-Tab und HA-Sensor eedc_speicher_voll_um), Wirtschaftlichkeits-Prognose ohne IST-Aggregat, η-SoC-Delta |
⚑ Und seit N-238 (2026-08-12) gehört der Wirkungsgrad dazu. Die Tages-Vorschau rechnete den Ladeweg verlustfrei — für 12 kWh im Speicher hielt sie 12 kWh Überschuss für ausreichend, real sind es bei η = 95 % rund 12,7 und bei 85 % über 14. „Speicher voll um” lag deshalb systematisch vor der tatsächlichen Uhrzeit. Kapazität und Wirkungsgrad kommen jetzt aus einem Helper (
aggregiere_speicher_basis: Kapazität summiert, Wirkungsgrad als Minimum — eine Kette ist nicht besser als ihr schwächstes Glied); der HA-Sensor und die Kachel lesen dieselbe Regel, statt sie zweimal zu bilden. Die Verluste sitzen ganz auf der Ladeseite, weil die übergebene Kapazität die abgabefähige Menge ist — sie zusätzlich beim Entladen anzusetzen wäre Doppelzählung (dieselbe Konvention wiecore/berechnungen/speicher_sizing.py).⚠ Die gepflegte Kapazität bleibt maßgeblich und wird NICHT durch die gemessene ersetzt (Gernots Entscheid 2026-08-12). Sie trägt eine Absicht: es gibt Anwender, die ihren Speicher bewusst nicht auf 100 % laden, und für die wäre eine Korrektur nach unten falsch — sie wollen wissen, wann ihr Ziel erreicht ist. Der Sizing-Block weist die gemessene Größe stattdessen daneben aus und trennt die zwei Ursachen einer Lücke (Ladegrenze ⟷ Ladeverlust) über den tatsächlich genutzten Ladestands-Bereich (
messe_soc_nutzung).
⚑ Seit #379 (2026-08-15) trägt das Netto-Feld eine dritte Aussage: die Entlade-Untergrenze. Aus dem Verhältnis beider Kapazitäten leitet
core/berechnungen/speicher_potential.py:: leer_schwelle_prozentab, ab welchem Ladestand dieser Speicher nichts mehr abgibt —(1 − netto ÷ brutto) × 100, bei 24 von 30 kWh also 20 %. Vorher stand dort die feste Zahl 5 %, und das war ein Fehler mit Folgen: Wer eine eigene Untergrenze fährt, dessen Speicher erreicht 5 % nie, also lief er nie „leer”, also war das Nutzbare Zusatzpotential strukturell 0 — die Sicht behauptete „ein größerer Speicher hätte nichts gebracht”, obwohl sie es gar nicht messen konnte.Drei Festlegungen gehören dazu, jede aus einer Messung:
- Aufschlag von 3 Prozentpunkten (
SOC_LEER_TOLERANZ_PP). Der Melder hat 20 % eingestellt, seine Kurve dreht bei 21 % — Wechselrichter schalten mit Puffer ab, der SoC kommt in ganzen Prozent, und diese Auswertung liest Stundenmittel. Ohne Aufschlag träfe die neue Schwelle so zuverlässig daneben wie die alte.- Deckel bei 50 % (
SOC_LEER_MAX_PROZENT) gegen die N-235-Klasse: „1 kWh nutzbar von 30” ergäbe sonst 96,7 % und ließe den Speicher fast immer als leer gelten — die Kennzahl fiele zu hoch aus, also in die Gegenrichtung der Deckelung.- Die Reserve sitzt unten. Bei Heimspeichern der Normalfall; die Oberseite deckt
SOC_VOLL_PROZENTab, und eine gewollte Lade-Grenze erkenntmesse_soc_nutzungan den Daten.⚠ Wer nichts pflegt, sieht keine geänderte Zahl — Rückfall auf 5 %, Abnahmekriterium des Baus. Umgekehrt heißt das: Die Korrektur erreicht nur Anlagen mit gepflegter nutzbarer Kapazität.
⚑ Und die Umkehrung gehört dazu (N-254): Ohne gepflegte nutzbare Kapazität ist „der Speicher wurde nie leer” zweideutig — er kann groß genug sein (dann hätte mehr Kapazität nichts gebracht) oder an einer Untergrenze hängen (dann hätte sie geholfen). Zwei Ursachen, entgegengesetzte Antworten, und eedc kann sie ohne die Pflege nicht trennen.
boden_nie_erreicht(soc_min, schwelle, schwelle_ist_abgeleitet)erkennt genau diesen Zustand; die Sicht sagt dann „nicht beurteilbar” und die Kachel zeigt „—” statt 0 — dieselbe Doktrin wie „nicht gemessen statt 0”.⚠ Die Regel hängt an der Pflege, nicht am Abstand allein — sonst verlöre auch der Fall seine Aussage, für den die Kennzahl gebaut wurde (Dev-Anlage, nie unter 31 %). Ist die Schwelle abgeleitet, kennt eedc die Untergrenze, und ein Speicher, der sie nicht erreicht, war wirklich groß genug. Wer seinen Boden real berührt (Winter, 6 %), bekommt die klare Aussage ebenfalls.
⚠ Die Schwelle gilt im ganzen Speicher-Hub, nicht nur in Phase 2.
speicher_sizing.pydefinierte95.0/5.0bis dahin selbst — mit dem Kommentar „dieselben Schwellen wie inspeicher_potential.py”. Die Absicht war formuliert, die Kopplung fehlte; ein==-Test hätte nie angeschlagen, weil beide Werte gleich waren. Jetzt Import statt Kopie, geprüft an der Quelle.
SoT netto (seit A31-2): core/investition_kennwerte.py::get_speicher_nutzbare_kapazitaet_kwh —
netto, sonst brutto, sonst None. Der Brutto-Fallback ist still (Entscheidung E17): kein
Hinweis, keine Kennzeichnung, kein P4-Fall. Der Brutto-Wert ist nicht unvollständig, er ist die
andere gültige Lesart derselben Größe; die Zahlenänderung aus A31-2 trifft deshalb ausschließlich
Anlagen, die das optionale Feld bewusst gepflegt haben. Die Leserichtung geht nur netto → brutto und
nie zurück — ein Brutto-Helper mit Netto-Fallback wäre genau die Verwechslung, die die Vollzyklen
wieder vom Pflegezustand abhängig machte.
Nebenwirkung der Vorschau, die dazugehört: dieselbe Simulation liefert auch Einspeisung, Eigenverbrauch und Autarkie des Vorschautags. Ein kleinerer Puffer nimmt weniger Überschuss auf — mehr geht ins Netz, weniger bleibt im Haus. Gemessen an der Demo-Anlage (15,4 kWh brutto gegen 13,9 kWh netto, 28.07.–02.08.2026): Einspeisung +0,75 bis +1,08 kWh/Tag, Eigenverbrauch entsprechend niedriger, Autarkie −1,7 bis −3,2 Prozentpunkte, „Speicher voll” an einem der sechs Tage eine Stunde früher (die Stundenauflösung verschluckt den Effekt an den übrigen).
Woher die Kapazität kommt (SoT seit A31-1): core/investition_kennwerte.py::get_speicher_kapazitaet_kwh
— brutto (kapazitaet_kwh), nur aus dem parameter-JSON, ohne Default. Ist nichts gepflegt,
liefert er None, und der Aufrufer entscheidet (summieren mit or 0, Zahl unterdrücken, Rechnung
auslassen); eine Zahl erfindet er nicht (Entscheidung E16, ADR-002/P3-a). Bis dahin stand an drei
Stellen ein .get(…, 10): ein Speicher ohne gepflegte Kapazität bekam still 10 kWh und daraus
Vollzyklen und eine Jahres-Ersparnis (N127). Der fehlende Wert wird stattdessen ausgewiesen —
Daten-Checker („Kapazität (kWh) fehlt”) und die Antwort selbst (kapazitaet_fehlt + Hinweis, P4).
Abgrenzung „SoC-Hübe” (
TagesZusammenfassung.batterie_vollzyklen= ΣΔSoC ÷ 200): eine andere Kennzahl, die reale Lade-Hübe misst und damit als einzige eine 10/90-Fahrweise abbildet (ein voller Hub = 160 pp = 0,8). Sie ist ein Bestandsmaß, hängt an einem SoC-Sensor und ist kein Ersatz für die Vollzyklen. Sichtbar in der Energieprofil-Tagestabelle unter diesem Namen.
Historie: Bis 2026-07-28 rechneten vier von fünf Stellen mit der Ladung und nur der HA-Sensor mit der Entladung; die Tages-Kachel zeigte sogar die ΔSoC-Größe unter dem Namen „Vollzyklen”. Auf derselben Anlage standen dadurch Zahlen, die um den Speicher-Wirkungsgrad auseinanderlagen (gemessen 10,97 gegen 8,57 bei η 78 %). Kein Test hat das bemerkt — daher jetzt der Symmetrie-Test über alle Pfade.
eedc kennt und braucht keinen Ziel-SOC: Vollzyklen und Wirkungsgrad kommen aus gemessenen Lade-/Entlademengen. Eine Annahme steckt nur in der Wirtschaftlichkeits-Prognose (250 Vollzyklen × Brutto-Kapazität,
SPEICHER_ZYKLEN_PRO_JAHR), die vor dem Vorliegen von Messdaten greift, sowie in der Tages-Vorschau („Speicher voll um …”,core/berechnungen/speicher_simulation.py), die von 0 bis 100 % der Brutto-Kapazität simuliert — wer bei 90 % abriegelt, ist real früher voll. Beides ist mit v4.0.2 erledigt: Kapazitäts-SoTget_speicher_kapazitaet_kwh(52d3714b) und die Netto-Umstellung von Tagesvorschau und Wirtschaftlichkeits-Prognose (c5c4437c), baumweit gewächtert (5dc3f488, ADR-002 P3-a). Die Vollzyklen bleiben bewusst brutto (Kanonf1644cc8) — die Netto-Umstellung zieht sie nicht mit.
Round-Trip-Wirkungsgrad (η) — SoC-korrigiert, nicht roh
Abschnitt betitelt „Round-Trip-Wirkungsgrad (η) — SoC-korrigiert, nicht roh“Funktion: services/speicher_wirtschaftlichkeit.py::berechne_ist_wirkungsgrad
Verwendet in: Cockpit → Monat, Komponenten-Hub, ROI-Analyse
η ist kein einfacher Quotient über einen Zeitraum. Ladung und Entladung sind Flüsse, der Speicher ist ein Bestand: Was am Periodenende im Akku steht, wird erst danach entladen. Über eine Monatsgrenze verrutscht dadurch Energie, und der rohe Quotient zappelt — er kann 100 % überschreiten oder zu niedrig ausfallen.
Zwei Pfade, in dieser Reihenfolge:
| Lage | Rechnung | quelle |
|---|---|---|
Fenster ≥ WIRKUNGSGRAD_FENSTER_MONATE_MIN (6) | entladung ÷ ladung — ΔSoC mittelt sich aus | fenster_lang |
| Kurzes Fenster, SoC am Rand bekannt | (entladung + ΔSoC_kwh) ÷ ladung, geklemmt auf 0…100 % | soc_korrigiert |
| Kurzes Fenster, kein SoC | roher Quotient, nur wenn ≤ 100 % | roh-unkorrigiert |
| sonst | kein Wert, Grund steht an der Kachel (P4) | fenster-zu-kurz |
Warum der rohe Wert nicht einfach verworfen wird: Die meisten Anlagen haben keinen SoC-Sensor. Ihn zu unterdrücken hieße, für sie dauerhaft „—” anzuzeigen — P4 verlangt „sagen, was man weiß und wie sicher”, nicht Schweigen. Deshalb wird er ausgewiesen und gekennzeichnet. Unterdrückt wird nur, was nachweislich falsch ist: über 100 % kann kein Speicher.
Der Layer-SoT
speicher_effizienz_prozentklemmt bewusst NICHT („Diagnose statt stillem Cap”). Die Diagnose ist der Daten-Checker: meldet ein Speicher kumulativ mehr Entladung als Ladung, ist das kein Carry-over mehr, sondern ein Erfassungsfehler — meistladung_kwhals reine PV-Ladung gepflegt (#281).ladung_netz_kwh⊆ladung_kwhist Vertrag, kein zweiter Posten.
Historie (F-22, rapahl 2026-05-22 und 2026-08-08): Bis v4.0.11 stand in
aktueller_monat.pyein Alles-oder-Nichts-Schalter auf|ΔSoC| > 20 pp— über der Schwelle wurde ausgeblendet, darunter der rohe Wert gezeigt; korrigiert wurde nie, und ohne SoC-Randwerte ging er ungeprüft hinaus. Gemessen an der Demo-Anlage: 2025-11 „—” statt 81,6 %, 2025-10 roh 83,1 statt 82,4 %. Die korrigierende Funktion existierte seit #264 und erreichte 2 von 12 Sichten — dieselbe Klasse wie F-16.
3.4 E-Auto-Einsparung
Abschnitt betitelt „3.4 E-Auto-Einsparung“Funktion: berechne_eauto_einsparung() in core/calculations.py
Verwendet in: ROI-Dashboard (investitionen.py)
Eingabefelder
Abschnitt betitelt „Eingabefelder“| Feld | Quelle |
|---|---|
jahresfahrleistung_km | Investition.parameter (Default: 15000) |
verbrauch_kwh_100km | Investition.parameter (Default: 18) |
pv_ladeanteil_prozent | Investition.parameter (Default: 60) |
benzinpreis_euro | Investition.parameter (Default: 1,65) |
vergleich_verbrauch_l_100km | Investition.parameter (Default: 7,5) — der fiktive Vergleichs-Benziner |
v2h_faehig | Investition.parameter (Default: false) |
eigener_verbrauch_l_100km | Investition.parameter — kein Default, s. „Plug-in-Hybrid” unten |
elektrischer_fahranteil_prozent | Investition.parameter — kein Default |
⚠ Diese Tabelle nannte bis 2026-08-08 vier Schlüssel, die eedc nicht kennt (
km_jahr,pv_anteil_prozent,benzin_verbrauch_liter_100km,nutzt_v2h) und zwei falsche Defaults (1,85 € statt 1,65 €; 7,0 L statt 7,5 L). Es sind genau die Legacy-Keys, deren Lesen v3.25.0 im Code als Bugs #1–#4 korrigiert hat — die Doku ist damals nicht mitgezogen. Historie:LEGACY_PARAM_KEYSincore/investition_parameter.pyführt die Zuordnung alt → neu.
Formeln
Abschnitt betitelt „Formeln“km_elektrisch = jahresfahrleistung_km (BEV — Normalfall) = jahresfahrleistung_km * Fahranteil / 100 (Plug-in-Hybrid, s. u.)km_verbrenner = jahresfahrleistung_km - km_elektrisch
Strom_Bedarf = km_elektrisch * Verbrauch_kWh_100km / 100PV_Anteil = pv_ladeanteil_prozent / 100Netz_Anteil = 1 - PV_Anteil
Strom_Kosten = Strom_Bedarf * Netz_Anteil * Strompreis / 100Benzin_Verbrauch = jahresfahrleistung_km * Vergleich_L_100km / 100 ← ALLE KilometerBenzin_Kosten = Benzin_Verbrauch * Benzinpreis_EURFossile_Kosten = km_verbrenner / 100 * Eigener_L_100km * Benzinpreis_EUR
V2H_Einsparung = V2H_Entladung_kWh * V2H_Preis / 100 (wenn V2H aktiv) V2H_Preis = v2h_entlade_preis_cent (Override, optional) = Bezugspreis - Einspeisevergütung (Normalfall, aus den gepflegten Tarifen)
Jahres-Einsparung = Benzin_Kosten - Strom_Kosten - Fossile_Kosten + V2H_Einsparung
CO2_Verbrenner = Benzin_Verbrauch * 2.37CO2_E-Auto = Strom_Bedarf * Netz_Anteil * 0.38CO2_Fossil = km_verbrenner / 100 * Eigener_L_100km * 2.37CO2-Einsparung = CO2_Verbrenner - CO2_E-Auto - CO2_FossilPlug-in-Hybrid: elektrischer und fossiler Anteil (ab #331)
Abschnitt betitelt „Plug-in-Hybrid: elektrischer und fossiler Anteil (ab #331)“Ein Plug-in-Hybrid fährt einen Teil seiner Kilometer mit Kraftstoff. eedc unterstellte bis dahin 100 % elektrisch — Ersparnis und CO₂-Bilanz fielen dadurch zu gut aus.
Es gibt keinen Fahrzeugtyp und keinen Schalter: das gepflegte Feld ist die Aussage.
Ist eigener_verbrauch_l_100km gesetzt, hat das Fahrzeug einen Verbrenner; ist es leer,
bleibt jede Zahl exakt wie vorher. Bestandsfahrzeuge tragen das Feld nicht, für sie
ändert sich nichts.
Wie der elektrische Anteil bestimmt wird (SoT core/berechnungen/phev_anteil.py,
dieselbe Funktion für IST und Prognose):
| Weg | Bedingung | Rechnung |
|---|---|---|
| gemessen | monatlicher Fahrverbrauch (kWh) erfasst | km_elektrisch = min(km_gefahren, Fahrverbrauch / Verbrauch_kWh_100km × 100) |
| geschätzt | nur elektrischer_fahranteil_prozent gepflegt | km_elektrisch = km_gefahren × Anteil / 100 |
| unbestimmt | keines von beiden | km_elektrisch = km_gefahren (Verhalten wie vorher) — der Daten-Checker meldet es |
⚠ Die Deckelung auf die gefahrenen Kilometer ist nicht kosmetisch. Ist der Kennwert zu niedrig gepflegt oder der Zähler zu großzügig, käme rechnerisch mehr elektrische Strecke heraus als gefahren wurde — und damit negative Verbrenner-Kilometer, also eine Ersparnis größer als die Wahrheit.
Zwei Verbräuche, zwei Bedeutungen: vergleich_verbrauch_l_100km beschreibt weiterhin
den fiktiven Vergleichs-Benziner, und die Frage „was hätte ein Benziner gekostet”
bleibt über alle Kilometer gestellt — sonst verglichen wir ein Auto mit einem halben.
eigener_verbrauch_l_100km ist der real getankte Verbrauch und erzeugt eine eigene
Kostenposition daneben. Die geladene Energie wird nicht zusätzlich skaliert: sie ist
im IST gemessen, ein Hybrid lädt ohnehin weniger.
Dynamischer Kraftstoffpreis (ab v3.17.0)
Abschnitt betitelt „Dynamischer Kraftstoffpreis (ab v3.17.0)“In der Finanz-Prognose (Auswertungen → Finanzen, Backend aussichten.py) wird die E-Auto-Ersparnis pro Monat mit dem echten Kraftstoffpreis berechnet:
Für jeden historischen Monat: Benzinpreis = Monatsdaten.kraftstoffpreis_euro (wenn vorhanden) ∨ Investition.parameter.benzinpreis_euro (Fallback statisch) Benzin_Kosten_Monat = km_gefahren / 100 * Vergleich_L_100km * Benzinpreis Strom_Kosten_Monat = ladung_netz_kwh * Netzbezug_Preis / 100 # kanonische Heimladungs-Quelle (Wallbox bzw. E-Auto), s. §3.2
Für Jahresprognose: Prognose_Benzinpreis = Ø(Monatsdaten.kraftstoffpreis_euro) (historischer Durchschnitt) ∨ Investition.parameter.benzinpreis_euro (Fallback)Datenquelle: EU Weekly Oil Bulletin (Euro-Super 95, inkl. Steuern, wöchentlich, History seit 2005). Befüllung durch den Scheduler-Job (täglich 06:00 + Startlauf), den Reparatur-Pfad „Kraftstoffpreise nachpflegen” oder den Backfill-Endpoint. Fehlt der Monatswert, rechnet die Kette mit dem Investitions-Parameter bzw. 1,65 €/L weiter — der Daten-Checker meldet offene Monate deshalb als eigene Kategorie.
Betroffen: Aussichten (aussichten.py), HA-Sensor-Export (ha_export.py), PDF-Finanzbericht (pdf_operations.py).
PV-Anteil der Heimladung: gemessen, sonst abgeleitet (ab 2026-08-08, N-141)
Abschnitt betitelt „PV-Anteil der Heimladung: gemessen, sonst abgeleitet (ab 2026-08-08, N-141)“Die Formeln oben beschreiben die Prognose-Achse, die den von Hand gepflegten
pv_ladeanteil_prozent liest — und, wo er fehlt, seit 2026-08-08 den IST-Anteil (s. unten).
Auf der IST-Achse gibt es diesen Parameter nicht — dort steht
je Monat das erfasste Paar ladung_pv_kwh / ladung_netz_kwh. Und genau das fehlte den meisten
Anlagen: eine Wallbox misst ihren PV-Anteil nicht, sie zählt nur Kilowattstunden. Ohne evcc
oder einen eigenen Zähler lieferte get_emob_pv_netz_kwh deshalb (0, Gesamtladung) — die
gesamte Heimladung galt als Netzstrom.
Reihenfolge (SoT core/berechnungen/pv_anteil_ladung.py):
-
Ein erfasster Wert gewinnt immer — auch eine gepflegte 0. Geprüft wird die Anwesenheit des Schlüssels
ladung_pv_kwh, nicht seine Größe: „diesen Monat kam nichts aus der Sonne” ist eine Aussage, keine Lücke. -
Sonst leitet eedc den Anteil aus den eigenen Stundenwerten ab, Regel Einspeise-Deckung:
je Stunde: ungedeckt = max(0, Ladung − Netzbezug − Speicherentladung)PV = min(Ladung, ungedeckt + Einspeisung)Netz = Ladung − PVDer zweite Summand fängt die Unschärfe der Stundenmittelung auf: Was in derselben Stunde eingespeist wurde, hätte stattdessen laden können.
-
Angewandt wird der Anteil, nicht die Kilowattstunde. Der abgeleitete Prozentsatz geht auf die kanonische Monatsladung — nur so bleibt
Ladung == PV + Netzexakt geschlossen, auch wenn die Tagesspur eine andere Menge kennt als die Monatszeile.
Wo die Regel angewandt wird — eine Schicht unter dem Pool (services/emob_ladeanteil.py, seit
2026-08-08, Fund F-16): die Monatszeilen werden angereichert, bevor irgendjemand sie zu
einer Heimladung poolt. Das ist keine Feinheit, sondern der Unterschied zwischen vier und achtzehn
Sichten: liegt die Ableitung über get_emob_heimladung_canonical, trifft sie nur die Felder
EmobFakten.ladung_pv_kwh/ladung_netz_kwh — jede Sicht, die die mitgereichten Rohzeilen selbst
poolt (Cockpit → Jahr, Jahresbericht-PDF) oder InvestitionMonatsdaten direkt liest
(Komponenten-Hub, Aussichten, HA-Export), zeigt daneben weiter 0 %. Sichten der zweiten Gruppe
holen dieselbe Anreicherung über reichere_monatszeilen_an.
Zwei Ausnahmen, beide bewusst:
- Der Community-Payload liest
eauto_summe_gemessen/wallbox_summe_gemessen— der Server hat die Rohdaten nie gesehen und rechnet nichts nach; eine Schätzung wäre in einem Benchmark nicht mehr als solche erkennbar (dieselbe Linie wie beim BKW-Eigenverbrauch). - Schreib-, Import- und Checker-Pfade bleiben außen vor. Die Anreicherung geschieht zur
Lesezeit auf Kopien; programmatisch in
verbrauch_datenzu schreiben bleibt verboten.
Die Prognose-Achse rechnet mit derselben Zahl (N-188, seit 2026-08-08): fehlt am Fahrzeug der
gepflegte pv_ladeanteil_prozent, nimmt die ROI-Prognose den IST-Anteil über
monats_fakten.ist_pv_ladeanteil_prozent (Σ PV ÷ Σ Ladung, ladungsgewichtet) statt des früheren
Vorgabewerts von 60 %. Der Default greift nur noch, wenn auch das IST keine Heimladung kennt. Die
zweite Prognose-Quelle (aussichten.py, leitet ihre Quote aus der Historie ab) zieht über dieselbe
Anreicherung mit.
Herkunft: EmobFakten.ladung_anteil_abgeleitet sagt, ob die Aufteilung gerechnet ist; auf der
Tagesebene trägt source_provenance die Marke einspeise_deckung bzw.
einspeise_deckung_teilweise (letztere, wenn nicht jede Ladestunde auswertbar war — dann ist der
Wert eine Teilsumme, P4).
Vermessen gegen evcc (2026-08-08, Anlage 1, Feb–Aug 2026, 963 kWh Heimladung, Referenz
sensor.evcc_helper_pv/net_charged_kwh):
| Regel | PV-Anteil | Abweichung zu evcc (67,9 %) |
|---|---|---|
netzbasiert min(Ladung, Netzbezug) | 73,8 % | +5,9 pp |
| netz + Speicherentladung | 60,8 % | −7,2 pp |
| Einspeise-Deckung (gebaut) | 64,7 % | −3,2 pp |
⚠ Keine rückwirkende Berechnung. Der Wert entsteht beim Aggregieren eines Tages; Zeiträume vor
diesem Feature tragen NULL, und NULL heißt „keine Aussage”, nicht „keine Sonne”.
⚠ Die Auflösung begrenzt die Genauigkeit. Meldet ein Wallbox-Zähler nur ganze Kilowattstunden (an der Referenzanlage 218 von 218 Stunden-Deltas ganzzahlig), trifft keine Rechnung die einzelne Stunde — über den Monat ist die Ableitung brauchbar, über die Stunde nicht.
⚠ Korrektur (2026-08-08): hier stand bis dahin „Offen: die Prognose zieht noch nicht mit (N-188)”. Das ist seit demselben Tag erledigt und vier Absätze weiter oben beschrieben — der Satz stammte aus dem Paket davor und ist beim Nachziehen stehen geblieben. Ein Dokument, das an zwei Stellen dasselbe verschieden behauptet, ist schlechter als eines, das schweigt.
Welcher Strompreis die Ladung bewertet (F-18 · ADR-002/P8)
Abschnitt betitelt „Welcher Strompreis die Ladung bewertet (F-18 · ADR-002/P8)“Die Netzladung eines E-Autos wird mit dem Tarif bewertet, der im jeweiligen Monat galt — nicht
mit dem heutigen. SoT ist services/eauto_wirtschaftlichkeit.aufgeloester_strompreis_cent, gerufen
aus berechne_eauto_ersparnis_periode.
Bis 2026-08-08 löste jede Sicht diese eine Größe selbst auf, und zwar auf vier Arten:
| Sicht | vorher | jetzt |
|---|---|---|
| Cockpit → Jahr | heutiger Wallbox-Tarif | Monatstarif inkl. Flex-Ø |
| HA-Export (Anlagen-Sensoren) | heutiger allgemeiner Tarif | Monats-Wallbox-Tarif |
| HA-Export (Fahrzeug-Sensor) | heutiger allgemeiner Tarif | Monats-Wallbox-Tarif |
| Komponenten-Hub | mengengewichteter Monats-Ø | unverändert (jetzt über den SoT) |
| Auswertungen → Aussichten | Monatstarif, aber allgemein | Monats-Wallbox-Tarif inkl. Flex-Ø |
Gewichtet wird nach der Netzladung je Monat, nicht nach Kilometern: der Preis bepreist Kilowattstunden. Nur wo eine Monatsaufteilung der Netzladung nicht existiert — bei reinem PV-Laden — fällt die Mittelung auf die km zurück, den Schlüssel, nach dem der Wallbox-Pool ohnehin attribuiert.
⚠ Für eine Anlage ohne Tarifwechsel bewegt sich nichts. Der mengengewichtete Ø eines einzigen Tarifs ist dieser Tarif. Wer den Tarif gewechselt hat, sieht dagegen vier HA-Sensoren einmalig springen (
e_auto_ersparnis_vs_benzin_euro,netto_ertrag_euro,roi_prozent,amortisation_jahre) — an einer vermessenen Anlage um −41,10 €.
⚠ Der P8-Wächter deckt diese Fläche nicht ab. Alle vier beteiligten Dateien stehen in
P8_BASELINE_AUSNAHMEN, weil sie den heutigen Tarif für die Hochrechnung nach vorn auch brauchen. Gesichert wird die Preisachse deshalb durch einen eigenen Wächter (test_wurzelmuster_konformitaet.py::test_p8_emob_ersparnis_bekommt_die_monatspreise, baumweit) und den Sichten-Vergleichtest_emob_preisachse_sichten_symmetrie.py.
Wo die Wallbox die Quelle ist (F-14 · N-196)
Abschnitt betitelt „Wo die Wallbox die Quelle ist (F-14 · N-196)“Trägt eine Wallbox die Ladeenergie, ist sie die Quelle — auch ohne gesetzte Zuordnung
(parent_investition_id). Diese Regel gilt seit 2026-08-08 in allen drei Pfaden:
| Pfad | Ort |
|---|---|
| Monatsebene | eauto_wirtschaftlichkeit.get_emob_heimladung_canonical |
| Tages-Leistungspfad | services/live_sensor_config.py (#356) |
| Tages-Zählerpfad | snapshot/komponenten_beitraege.wallbox_deckt_ladung_ab |
Vorher kannte der Zählerpfad nur die Zuordnung; ein E-Auto mit eigenem kWh-Zähler ohne Parent
lief an ihr vorbei, und derselbe Ladevorgang stand zweimal im Tagesverlauf. Bereits gespeicherte
Tage bleiben davon unberührt — dafür meldet der Daten-Checker sie (Kategorie
emob_doppelzaehlung_tage) und bietet „Zeitraum neu aggregieren” an. Bewusst kein
Start-Migrationslauf: die Heilung überschreibt Messwerte und bleibt eine Entscheidung des
Anwenders.
3.5 Wärmepumpe-Einsparung
Abschnitt betitelt „3.5 Wärmepumpe-Einsparung“Funktion: berechne_waermepumpe_einsparung() in core/calculations.py
Verwendet in: ROI-Dashboard (investitionen.py)
3 Effizienz-Modi
Abschnitt betitelt „3 Effizienz-Modi“Modus A: gesamt_jaz (Standard - gemessene Jahresarbeitszahl)
WP_Strom_kWh = Gesamtwärmebedarf / JAZModus B: scop (EU-Label SCOP-Werte)
Strom_Heizung = Heizwärmebedarf / SCOP_HeizungStrom_Warmwasser = Warmwasserbedarf / SCOP_WarmwasserWP_Strom_kWh = Strom_Heizung + Strom_WarmwasserModus C: getrennte_cops (präzise Betriebspunkte)
Strom_Heizung = Heizwärmebedarf / COP_HeizungStrom_Warmwasser = Warmwasserbedarf / COP_WarmwasserWP_Strom_kWh = Strom_Heizung + Strom_WarmwasserGemeinsame Formeln (alle Modi)
Abschnitt betitelt „Gemeinsame Formeln (alle Modi)“PV_Anteil = pv_anteil_prozent / 100Netz_Anteil = 1 - PV_Anteilη_alt = alter_wirkungsgrad(Energieträger) # 0,90 Gas · 0,85 Öl · 1,0 Strom
WP_Kosten = WP_Strom * Netz_Anteil * Strompreis / 100Alte_Kosten = Gesamtwärmebedarf / η_alt * Alter_Preis / 100 + alternativ_zusatzkosten_jahr # Schornsteinfeger / Wartung / Grundpreis Gaszähler
Jahres-Einsparung = Alte_Kosten - WP_Kosten
CO2_alt = Gesamtwärmebedarf / η_alt * CO2_Faktor[gas|oel|strom]CO2_WP = WP_Strom * Netz_Anteil * 0.38CO2-Einsparung = CO2_alt - CO2_WPWirkungsgrad der Altanlage (η_alt):
Gesamtwärmebedarfist abgegebene Wärme, nicht Brennstoff — das Eingabefeld heißt „Heizwärmebedarf (kWh/Jahr) — aus Energieausweis”, und derselbe Wert wird oben durch die JAZ geteilt (JAZ = Wärme/Strom). Ein Kessel muss dafürWärme / ηverfeuern;Alter_Preisist der Preis je kWh Brennstoff (so steht er auf der Rechnung). Die Umrechnung macht der Layer-SoTgas_kosten_altanlage, die η-Wahl der Resolveralter_wirkungsgrad— beide incore/berechnungen/alternativkosten.py.Bis v4.0.1 fehlte die η-Rückrechnung in diesem Pfad: die ROI-Seite wies für dieselbe Wärmepumpe eine niedrigere Ersparnis (und CO₂-Einsparung) aus als Aussichten, HA-Export und WP-Dashboard, die alle über
gas_kosten_altanlagelaufen. Die fixen Zusatzkosten werden nicht durch η geteilt — sie sind keine Energie.Strom-Direktheizung: η = 1,0. Eine Widerstandsheizung (Nachtspeicher, Infrarot) setzt Strom verlustfrei in Wärme um; ihr einen Kesselverlust anzurechnen, würde die WP-Ersparnis überhöhen. Die η-Wahl lag vorher an vier Stellen dupliziert vor und kannte diesen Fall nirgends.
Diese Rechnung braucht zwei gepflegte Angaben — sonst läuft sie nicht. ⚠ Bis 2026-08-16 stand hier das Gegenteil: „Split-Klimaanlagen (
wp_art = luft_luft) durchlaufen diese Rechnung gar nicht … ein Luft-Luft-Gerät ersetzt in aller Regel keine Heizung.” Diese Prämisse ist gefallen — eine Luft-Luft-Wärmepumpe kann sehr wohl eine Gasheizung ersetzen, und viele Anwender heizen damit. Der echte Defekt war nie die Bauart, sondern eine erfundene Eingabe: WeilHeizwärmebedarf/WarmwasserbedarfDefaults hatten (12.000/3.000 kWh), kam die Formel nie ohne Ergebnis heraus — bei den übrigen Standardwerten rund 1.100 €/Jahr und 2.210 kg CO₂/Jahr Ersparnis gegen eine Gasheizung, die es nie gab, inklusive Beitrag zu den Anlagen-Summen. Heute gilt stattdessen: (1) Ist beim ersetzten Energieträger „Nichts ersetzt (Neubau)” gewählt, wird weder Ersparnis noch CO₂-Ersparnis konstruiert — für jede Wärmepumpenart, nicht nur für Klimaanlagen; das war der Neubau-Fall, der bis dahin still eine Gaskessel-Ersparnis bekam. (2) Ist kein Wärmebedarf gepflegt, gibt es keinen Default mehr; die ROI-Zeile trägt „—” undnicht_bewertetsamt Begründung. Eine halb gepflegte Angabe zählt entsprechend halb (nur Warmwasser gepflegt ⇒ Heizwärme 0). (3) Bestandsschutz ohne Migration: Steht an einer Luft-Luft-WP noch exakt die alte Vorbelegung 12.000/3.000, zählt sie als offene Frage statt als Antwort — sie war seit v4.0.6 unsichtbar und damit nicht korrigierbar. Bei klassischen Wärmepumpen bleibt sie eine Schätzung und wird gerechnet. Die gemessenen Pfade (services/wp_wirtschaftlichkeit.py,co2_wp_ersparnis_kg, Aussichten, JAZ/COP) haben zusätzlich weiterhin ihrenwp_waerme_kwh <= 0-Wächter. SoT der Unterscheidung:core/berechnungen/alternativkosten.py::ersetzt_keine_heizung; die Bauart-Unterscheidungist_luft_luft_waermepumpeträgt nur noch den Bestandsschutz aus (3).
⚠ Punkt (1) galt bis 2026-08-29 nur je Gerät, nicht in den anlagenweiten Sichten — und der häufigste Fall fiel genau durch diese Lücke. Wer neben einer Wärmepumpe, die eine Gasheizung ersetzt hat, eine Split-Klimaanlage betreibt (in eedc ebenfalls eine Wärmepumpe, meist mit „Nichts ersetzt (Neubau)”), bekam auch deren Wärme als vermiedenes Gas gutgeschrieben: Die Sperre griff erst, wenn keine einzige Wärmepumpe etwas ersetzt hatte. Betroffen waren die CO₂-Ersparnis im Jahresbericht — bei zwei gleich großen Geräten war sie doppelt so hoch (2.458 statt 1.229 kg/Jahr, an einer nachgestellten Anlage gemessen) — und die Alternativkosten-Ersparnis in der Jahresprognose unter Auswertungen → Aussichten, dort mit umgekehrtem Vorzeichen: Der Strom des Neubau-Geräts wurde von der Ersparnis der ersetzenden Wärmepumpe abgezogen (1.057 → 704 €/Jahr, ebenfalls nachgestellt).
Heute gilt Punkt (1) überall. Der fossile Vergleich rechnet auf beiden Seiten mit den Geräten, die etwas ersetzt haben — dieselbe Grundmenge im Zähler wie im Abzug. Die Mengen der Anlage (Stromverbrauch, gelieferte Wärme) bleiben davon unberührt; kleiner wird allein die Grundmenge des Vergleichs. SoT sind die Teilmengen
WpFakten.waerme_mit_ersatz_kwh/strom_mit_ersatz_kwhaus der Monats-Fakten-Schicht (ADR-002/P10).⭐ Warum Teilsummen und keine Sperre. Die Arbeitszahl wird bei gemischten Bauarten gesperrt, weil ein Quotient über zwei Maßstäbe keine Aussage hat. Eine CO₂-Menge dagegen ist additiv — für sie genügt die Teilsumme. Kennzahlen trennen wir je Bauart, Mengen summieren wir.
Alternativ-Zusatzkosten (v3.21.0, #141):
alternativ_zusatzkosten_jahr(€/Jahr) deckt laufende Fixkosten der Alt-Heizung (Schornsteinfeger, Wartung, Gaszähler-Grundpreis) ab. Wird in fünf Berechnungs-Pfaden berücksichtigt: Aussichten historisch + Prognose, HA-Sensor-Export inkl. WP-Sensor, PDF-Jahresbericht, Investitions-Vorschau. In historischen Aggregaten anteilig pro erfasstem Monat (alternativ_zusatzkosten_jahr / 12).
Eingabefelder
Abschnitt betitelt „Eingabefelder“| Feld | Parameter-Key | Default |
|---|---|---|
| Effizienz-Modus | effizienz_modus | gesamt_jaz |
| JAZ | jaz | 3.5 |
| SCOP Heizung | scop_heizung | 4.5 |
| SCOP Warmwasser | scop_warmwasser | 3.2 |
| COP Heizung | cop_heizung | 3.9 |
| COP Warmwasser | cop_warmwasser | 3.0 |
| Heizwärmebedarf | heizwaermebedarf_kwh | kein Default mehr in der ROI-Rechnung; im Formular Vorbelegung 12000, bei luft_luft nicht vorbelegt (Feld seit 2026-08-16 wieder angeboten) |
| Warmwasserbedarf | warmwasserbedarf_kwh | kein Default mehr in der ROI-Rechnung; im Formular Vorbelegung 3000, bei luft_luft nicht vorbelegt (Feld seit 2026-08-16 wieder angeboten) |
| PV-Anteil | pv_anteil_prozent | 30 |
| Alter Energieträger | alter_energietraeger | gas |
| Alter Preis | alter_preis_cent_kwh | 12 (Fallback wenn Monatsdaten.gaspreis_cent_kwh leer) |
| Alternativ-Zusatzkosten | alternativ_zusatzkosten_jahr | 0 (€/Jahr) |
| WP-Strompreis | Spezialtarif waermepumpe | Fallback: allgemein |
3.5b Modus-Split: Heizen und Kühlen trennen (#263 K-2)
Abschnitt betitelt „3.5b Modus-Split: Heizen und Kühlen trennen (#263 K-2)“Eine Split-Klimaanlage heizt und kühlt über denselben Zähler. Die Aufteilung ist aus keinem
gespeicherten Wert rekonstruierbar — sie entsteht nur, wenn eedc den Betriebsmodus zur Messzeit
mitschreibt (TagesEnergieProfil.betriebsmodus_je_wp, seit S2).
Welcher Wert die Stunde bestimmt (
core/betriebsmodus.py::normalisiere_betriebsmodus): der eingestellte Modus (hvac_mode), verfeinert durch den Ist-Betrieb (hvac_action), wo die Integration ihn liefert (D2). ⚠ Verfeinern heißt nicht verwerfen — der Ist-Betrieb schlägt den Modus nur, wo er eine Richtung nennt (heating/cooling/drying/fan) oderoff.idle(Leerlauf) nennt keine und fällt deshalb auf den eingestellten Modus zurück (#399, 28.08.2026; davor ergab esunbestimmt, und bei einem taktenden Inverter-Gerät fiel damit der Großteil des Stroms in nicht aufgeteilt). Ohne Richtung im Modus — Automatik (heat_cool/auto) oder gar kein Modus — bleibt esunbestimmt; geraten wird nicht.
je Tag d, je Gerät i: roh_h = |komponenten["waermepumpe_<i>"]| # Leistungspfad, NEGATIV gespeichert faktor_d = zaehler_kwh_d / Σ_h roh_h # nur wenn Zählersumme vorhanden kwh[modus] += roh_h × faktor_d # Stunden OHNE Modus: nur in den Nenner
je Monat (ein Gerät): modus_strom_heizen_kwh = Σ_d kwh[heizen] modus_strom_kuehlen_kwh = Σ_d kwh[kuehlen] modus_abdeckung_h = Σ_d Stunden mit Modus-Signal nicht_aufgeteilt = Bezug − heizen − kuehlen # NIE gespeichert
anlagenweit (mehrere Geräte, gleicher Zeitraum): modus_strom_*_kwh = Σ_i je Gerät # Mengen: addieren modus_abdeckung_h = max_i je Gerät # Zeit: NICHT addierenSoT: core/berechnungen/modus_split.py (rein) · services/energie_profil/modus_split_monat.py
(Lader) · …/modus_split_schreiben.py (Schreiber, Schritt 4 des Monatsabschlusses).
Teilmengen, keine Summanden.
stromverbrauch_kwhbleibt die einzige Bilanzgröße; die zwei Anteile werden ausgewiesen und nie addiert (Präzedenzladung_pv_kwhbei der Wallbox). Wächter:test_263_k2_modus_split.py::test_teilmengen_werden_nirgends_addiert.
Zwei Vorzeichen-Welten.
TagesEnergieProfil.komponentenführt die Wärmepumpe negativ (Leistungspfad,seite: "senke"⇒-abs(...)),TagesZusammenfassung.komponenten_kwhpositiv (Zählerpfad).waermepumpe_kwh_je_investitionliefert für beide Beträge — und ist zugleich die Stelle, diewaermepumpe_1vonwaermepumpe_12unterscheidet (die ID ist kein Präfix) und Suffix-Keys (…_heizen) demselben Gerät zuschlägt.
Normierung ist opportunistisch, die Invariante ist der Schutz. Wo der Zählerpfad eine Tagesmenge kennt, wird die Stundenform darauf normiert (Präzedenz v3.45.5). Er kennt sie nicht immer — dann gilt die Roh-Summe. Was
Σ Teilmengen ≤ Gesamthält, ist die Regel im Schreibpfad: größer ⇒ gar nicht schreiben, vorhandene Werte entfernen, Daten-Checker meldet die Monate.
Der Bezug von „nicht aufgeteilt” ist nicht der Gesamtstrom. Anlagenweit trägt
strom_kwhauch Wärmepumpen ohne Modus-Sensor; ihr Verbrauch erschiene sonst als unbeobachtete Zeit der Klimaanlage. Bezug istWpFakten.modus_strom_bezug_kwh— der Strom nur der Geräte mit Split.⭐ Und genau deshalb nennt die Anzeige diesen Bezug seit v4.0.29 (W-17b). Die Kachel „Strom verbraucht” zeigt
strom_kwh, der Balken darunter beschreibtmodus_strom_bezug_kwh— bei einem Melder 30 gegen 284 kWh. Beide Zahlen waren richtig, nur stand nirgends, dass es zwei verschiedene Grundmengen sind. Die Zeile „Aufgeteilte Menge” erscheint, sobald sie abweichen.
Eine Menge ist additiv, ein Zeitraum nicht (W-17).
modus_abdeckung_hwird über Tage summiert und über Geräte maximiert. Zwei Wärmepumpen, die dieselben 18 Stunden liefen, ergeben 18 Stunden Beobachtung, nicht 36 — im Tag sofort sichtbar, im Monat jahrelang plausibel. SoT der Regel:core/berechnungen/modus_split.py::abdeckung_ueber_geraete.⚠ Das Maximum ist eine Untergrenze der exakten Vereinigung, die aus verdichteten Summen nicht mehr rekonstruierbar ist (
abdeckung_hist eine Anzahl, keine Stundenliste). Zwischen einer Zahl, die zu klein sein kann, und einer, die einen Tag mit 36 Stunden behauptet, ist die Wahl keine Geschmacksfrage.
3.5c Abgeleitete Heizwärme und die JAZ-Sperre (#263 K-2, Konzept §3.4/§3.5)
Abschnitt betitelt „3.5c Abgeleitete Heizwärme und die JAZ-Sperre (#263 K-2, Konzept §3.4/§3.5)“Ohne Wärmemengenzähler wird die Heizwärme aus dem modus-aufgeteilten Strom gerechnet:
heizenergie_kwh = modus_strom_heizen_kwh × Heiz-Effizienz # NUR wenn gepflegtDie Heiz-Effizienz kommt je nach effizienz_modus aus jaz / scop_heizung / cop_heizung
(heiz_effizienz_gepflegt). ⚠ Nie aus einem Default — PARAM_WAERMEPUMPE_DEFAULTS trägt
jaz: 3.5, und wer den anwendet, erfindet für jede ungepflegte Wärmepumpe eine Wärmemenge und
damit eine Ersparnis, eine CO₂-Zahl und einen Kostenvergleich.
Geschrieben wird mit Quelle auto:monatsabschluss (AUTO_AGGREGATION, Priorität 3) und der
Provenance-Marke jaz_modus_split. „Gemessen schlägt abgeleitet” folgt daraus per
Konstruktion: MANUAL ist 1, EXTERNAL_AUTHORITATIVE 2 — beide gewinnen.
Die eine Regel, die daraus folgt — sie trennt teilen von multiplizieren:
| darf abgeleitete Wärme verwenden? | warum | |
|---|---|---|
| teilt Wärme durch Strom → JAZ/COP | nein | heraus käme exakt die gepflegte JAZ — eine Zahl, die nichts misst |
| multipliziert Wärme mit Preis / η / CO₂-Faktor | ja, mit Kennzeichnung | die Wärmemenge ist die beste verfügbare Schätzung |
Getragen wird die Sperre von WpFakten.jaz_belastbar (Fakten-Leser) bzw.
heizwaerme_ist_abgeleitet(source_provenance) (Direktleser). Sie hängt am Wert, nicht an der
Bauart: eine Luft-Wasser-WP ohne Wärmemengenzähler fällt unter dieselbe Regel, eine Klimaanlage
mit Zähler ist gemessen wie jede andere. Wächter:
test_263_k2_modus_split.py::test_keine_jaz_stelle_rechnet_mit_abgeleiteter_waerme (fünf Dateien,
sieben Formeln).
⚠ Kühlen ersetzt keine Heizung (Konzept E-B).
berechne_wp_ersparnisundco2_wp_ersparnis_kgnehmenstrom_kuehlen_kwhaus dem Vergleich heraus; die Kosten bleiben inwp_kosten_euround werden alskuehl_kosten_euroeigens ausgewiesen. Ohne diese Trennung gemessen: −45,04 € Ersparnis und −52 kg CO₂ für eine Klimaanlage, die im Winter geheizt und im Sommer gekühlt hat (nachher +2,48 € / +8,2 kg). Default 0.0 ⇒ ohne Split unverändert.⭐ Dieselbe Abgrenzung gilt für die Arbeitszahl — seit W-14 (26.08.2026). Bis dahin nannte dieser Kasten nur Ersparnis und CO₂, und das war keine Unvollständigkeit im Text, sondern im Code: Die Arbeitszahl war die einzige der drei Größen, an der niemand nachgezogen hatte. Ein Gerät, das im Sommer kühlt, sah dadurch aus wie eine schlechte Heizung.
⭐ Und sie gilt für alle Funktionen ohne bewertete Nutzenergie — seit E4 (26.08.2026).
arbeitszahl(..., strom_funktionsfremd_kwh=…)bekommt heute Kühlen + Lüften + Entfeuchten aus einer einzigen Größe:WpFakten.modus_strom_funktionsfremd_kwhbzw.ModusStromZeile.funktionsfremd_kwh. Eine Größe statt drei Summanden an vier Aufrufern — die Aufzählung war die Bauform, an der W-14 entstanden ist. Abgezogen, nicht gesperrt: die Mengen bleiben in jeder Bilanz, es ändert sich allein der Nenner.⬜ Offen und bewusst nicht mitgebaut: Der Community-Server kennt nur den Kühlstrom (Feld
wp_strom_kuehlen_kwh). Wer Lüften oder Entfeuchten getrennt misst und am Vergleich teilnimmt, sieht dort eine etwas niedrigere Arbeitszahl als im eigenen Cockpit. Es braucht ein neues Feld samt Migration im zweiten Repo; der Vermerk steht an der Stelle inservices/community_service.py.
3.5d Arbeitszahl je Funktion und die Arbeitszahl Kühlen (W-4 · W-5)
Abschnitt betitelt „3.5d Arbeitszahl je Funktion und die Arbeitszahl Kühlen (W-4 · W-5)“⛔ Dieser Abschnitt fehlte bis zum 27.08.2026 vollständig — beide Größen waren gebaut und in keiner Referenz beschrieben. Aufgefallen beim Prüfauftrag „§3.5c gegen W-4/W-5 halten”: Der Kasten darüber war richtig, aber er beschreibt eine dritte Regel. Ein Dokument, das niemand gegen den Code hält, produziert beides — vergessene Arbeit und erfundene Arbeit.
Alle vier Arbeitszahlen kommen aus derselben Funktion (core/berechnungen/waermepumpe_kennzahl.py),
und das ist der Punkt: Was für eine gilt, gilt für alle.
Arbeitszahl gesamt = waerme_kwh ÷ (strom_kwh − funktionsfremd_kwh)Arbeitszahl Heizen = heizenergie_kwh ÷ strom_heizen_kwhArbeitszahl Warmwasser = warmwasser_kwh ÷ strom_warmwasser_kwhArbeitszahl Kühlen = nutzenergie_kuehlen_kwh ÷ betriebsart_strom_kuehlen_kwh| Voraussetzung | Grund, wenn sie fehlt | |
|---|---|---|
| je Funktion (W-4) | getrennte_strommessung und die zugehörige Wärmemenge | GRUND_STROM_NICHT_JE_FUNKTION |
| Kühlen (W-5) | Kühlstrom und Kältemengenzähler | GRUND_KEINE_KAELTEMENGE · "kein Kühlbetrieb in diesem Zeitraum" |
⚠ Die R2-Sperren gelten für alle vier — Anwender-Angabe abgrenzung, abgeleitete Wärme,
gemischte Bauarten, Geräte ohne Wärme, Zeitraum-Versatz.
R2/Bauart — neu am 28.08.2026 (
GRUND_BAUARTEN_GEMISCHT, SOLL §5): Trägt ein Block eine Luft-Wasser-Wärmepumpe und eine Luft-Luft-Split-Klimaanlage, gibt es keine gemeinsame Kennzahl — verschiedene Nutzenergie, verschiedene Vergleichsmaßstäbe. Die Mengen bleiben summiert (§5: „Mengen dürfen nebeneinander stehen, eine gemeinsame JAZ nicht”), und im Komponenten-Hub behält jedes Gerät seine eigene Zahl. ⭐ Erkannt aus den Stammdaten (ist_luft_luft_waermepumpe), nicht aus einer Messreihe — damit ist es nachwaerme_deckt_nicht_alle_geraetedie zweite Lage, die eedc selbst erkennt. Sie steht vor ihr in der Reihenfolge: Der allgemeinere Grund riete zu einem Wärmemengenzähler, den eine Split-Klimaanlage bauartbedingt nicht haben kann. Genau daran ist W-4 entstanden: Die Funktions-Kennzahlen wurden an einer eigenen Stelle gerechnet und kannten die Sperren nicht. Ein Heizstab auf dem WP-Zähler ließ die Gesamtzahl mit Begründung verschwinden, während „JAZ Heizen” unbeeindruckt danebenstand — dieselbe Anlage, zwei Aussagen. Dazu stand dort eine 0, wo „unbekannt” gemeint war (ADR-002/P4).
Warum die Arbeitszahl Kühlen nicht „SEER” heißt (Entscheid, 26.08.2026): SEER ist eine genormte Größe aus definierten Prüfstandsbedingungen. Was eedc bildet, ist der Quotient zweier Zähler über einen Zeitraum. Sie „SEER” zu nennen behauptete eine Vergleichbarkeit mit Datenblatt-Werten, die sie nicht hat. Anwender-Fassung: Wärme & Klima §3.
Warum keine geschätzte Kältemenge: Aus einem angenommenen Wirkungsgrad käme genau der Faktor zurück, mit dem gerechnet wurde — dieselbe Zirkularität, die §3.5c für die abgeleitete Heizwärme beschreibt, nur auf der Kälte-Achse.
3.5e Anzeige-Regeln der Betriebsart-Aufteilung (W-17 · W-17b · W-18)
Abschnitt betitelt „3.5e Anzeige-Regeln der Betriebsart-Aufteilung (W-17 · W-17b · W-18)“Drei Regeln, die keine Formel sind und trotzdem in jede Sicht gehören:
| Regel | Kurz |
|---|---|
| Zeit ist nicht additiv (W-17) | modus_abdeckung_h wird über Tage summiert, über Geräte maximiert (abdeckung_ueber_geraete). Zwei Geräte mit je 18 h ergeben 18, nicht 36. |
| Eine Aufteilung nennt ihre Grundmenge (W-17b) | Der Balken bezieht sich auf modus_strom_bezug_kwh, die Kachel darüber auf strom_kwh. Weichen sie ab, steht die Zeile „Aufgeteilte Menge” darunter. |
| Ein fehlender Wert nennt seinen Grund (W-18) | Drei unterscheidbare Zustände (core/tageswert_grund.py): kein Zähler · zugeordnet, aber für diesen Tag ohne Zählerstände · Zählerrücksprung. Der Grund wird hergeleitet, nie behauptet — arbeitszahl(waerme_fehlt_grund=…) nimmt ihn entgegen, weil der Layer ihn nicht kennen kann. |
3.6 ROI & Amortisation
Abschnitt betitelt „3.6 ROI & Amortisation“Funktion: berechne_roi() in core/calculations.py
Nenner-SoT: core/berechnungen/kapitalrechnung.py
Warum eine Zahl dort steht, wo sie steht:
docs/KONZEPT-WIRTSCHAFTLICHKEITSRECHNUNG.md. Dort stehen die Entscheidungen samt den verworfenen Alternativen und den Messungen, die sie verworfen haben — inklusive der häufigen Einwände mit Antwort. Dieses Kapitel hier nennt die Formeln, jenes die Begründung.
Relevante_Kosten = Anschaffungskosten - AlternativkostenNetto_Einsparung = Jahres-Einsparung - Betriebskosten_JahrROI (%) = Netto_Einsparung / Relevante_Kosten * 100Amortisation (Jahre) = Relevante_Kosten / Netto_EinsparungWobei Betriebskosten_Jahr = Investition.betriebskosten_jahr (Wartung, Versicherung etc., Default: 0).
WICHTIG - Zwei verschiedene ROI-Metriken:
| Metrik | Wo angezeigt | Formel | Bedeutung |
|---|---|---|---|
| Jahres-Rendite | Cockpit | Kumul. Ersparnis / Relevante Kosten * 100 | Wie viel % bereits amortisiert (kumuliert) |
| ROI p.a. | Auswertungen → ROI (pro Komponente) | Jahres-Einsparung / Relevante Kosten * 100 | Rendite pro Jahr |
| Amortisations-Fortschritt | Auswertungen → ROI (Kachel), Jahresbericht-PDF | Bisherige Erträge / Relevante Kosten * 100 | Kumulierter Fortschritt |
Ein Nenner für alle drei (N-137, seit 2026-08-04). „Relevante Kosten” sind die Mehrkosten
Σ max(0, anschaffungskosten_gesamt − anschaffungskosten_alternativ)— SoTcore/berechnungen/investitionskosten.py::relevante_kosten_aus_investitionen, identisch mit der USt-Bemessungsgrundlage (§3.7). Vorher gab es drei Antworten: die ROI-Sicht rechnete ohne Klemmung je Position, Aussichten und Cockpit trugen eine Hybrid-Summe, deren WP-/E-Auto-Mehrkosten ausparameter["alternativ_kosten_euro"]kamen — einem Schlüssel ohne Schreiber, der immer auf die Festannahmen 8.000 € / 35.000 € zurückfiel. Gepflegt wird die Spalteanschaffungskosten_alternativ, die der Daten-Checker mit WARNING einfordert.Ohne gepflegte Alternativkosten zählen die Vollkosten. eedc setzt keine Annahme mehr ein; die Amortisation fällt dadurch für WP und E-Auto ungünstiger aus als vorher, dafür entspricht sie den Daten, die tatsächlich da sind. Gesichert durch
test_amortisation_nenner_symmetrie.py(Regression, drei Sichten auf einer Fixture mit Alternativkosten ≠ Festannahme).
Sonstige Positionen: Zeitraum-Bilanz ↔ Kapitalrechnung (seit 2026-08-09). Eine Reparatur ist zweierlei, je nach Frage — und seit 2026-08-10 gilt dasselbe für eine Förderung:
Frage Sicht Wo die Position steht „Was hat der Monat gekostet und eingebracht?” Cockpit, Monatsbericht, Jahresbericht-PDF, CSV-Export, Sensor netto_ertrag_euroErtragsseite — Aufwand bzw. Ertrag des Zeitraums, unverändert „Wie lange dauert es, bis sich das rechnet?” ROI, Amortisation, HA-Sensoren, Finanzbericht-PDF Kapitaleinsatz (Nenner): Ausgabe +, Ertrag − Warum: vorher wurden sonstige Ausgaben im Zähler über die Laufzeit annualisiert. Eine einmalige Reparatur von 3.000 € an einer Wärmepumpe (Einsparung 1.235 €/Jahr) verlängerte die Amortisation dadurch von 8,1 auf 42,6 Jahre, in der Jahressicht erschien das Gerät als nie amortisiert — und die Zahl driftete jedes Folgejahr weiter, ohne dass etwas passierte. Im Nenner ergibt dieselbe Reparatur 10,5 Jahre. Bei laufenden Kosten (Wartung) liefert die kumulative Rechnung sogar exakt dasselbe Ergebnis wie vorher (
K + 180n = 1235n ⇒ n = 9,5).Sonstige Erträge stehen seit 2026-08-10 im Nenner (Bauschritt 7 — die Vollkostenrechnung ist damit vollständig). Eine Position im Monatsabschluss ist per Form einmal geflossen; sie zu mitteln und in jedes künftige Jahr zu verlängern unterstellt eine Wiederholung, die niemand behauptet hat (spiegelbildlich zur Ausgabenseite, seit demselben Tag). Eine THG-Quote oder eine Förderung mindert deshalb das eingesetzte Kapital, statt eine künftige Jahres-Ersparnis zu erhöhen. In der Zeitraum-Bilanz bleibt sie unverändert stehen — dort ist sie ein Ertrag des Zeitraums.
Warum nicht schon mit F-19: ein wiederkehrender Ertrag
Eim Nenner ergibt(K − E·n) ÷ Z, eine Zahl, die jedes Jahr schrumpft und irgendwann negativ wird. Voraussetzung war deshalb ein Ort für wiederkehrende Erträge, und den gibt es seit demselben Tag gleich zweimal: „Ertrag/Jahr (€)” an der Investition (Wallbox/Sonstiges) und „Einspeise-Erlös (€)” bei Sonstiges/Erzeuger, per HA-Sensor monatsgenau befüllbar. Praktischer Fall: wer zwei Wechselrichter mit verschiedenen Einspeisetarifen betreibt (eedc kennt genau einen Einspeisesatz je Anlage), pflegt den zweiten Erlös dort — beide Felder sind wiederkehrend gemeint und wirken im Zähler. Die monatliche Handpflege bleibt möglich, beschreibt aber die Vergangenheit und mindert seither den Kapitaleinsatz; der Daten-Checker weist auf den besseren Ort hin, wenn derselbe Posten mehrfach auftaucht.⚠ Der Nenner kann dadurch unter die Anschaffungskosten fallen — das ist die Aussage: eine Förderung ist Geld, das nie eingesetzt wurde. Fällt er auf ≤ 0, zeigt eedc keine Amortisationsdauer und keinen ROI statt einer negativen Zahl.
Die Unterscheidung kostet keine zusätzliche Pflege:
typ: ertrag|ausgabewird beim Erfassen ohnehin gewählt, und der Ort sagt die Häufigkeit.Und der Kapitaleinsatz ist nicht die USt-Bemessungsgrundlage. Die bleibt bei den reinen Mehrkosten (§ 3 Abs. 1b UStG, §3.7) — weder eine Reparatur noch eine Förderung gehört dort hinein. Gesichert durch
test_kapitaleinsatz_vier_sichten_symmetrie.py(Regression, vier Sichten + beide Seiten des Bruchs, beide Erfassungsorte — an der Investition und auf der Monatsdaten-Zeile). ⚠ Die zweite Hälfte kam erst am 2026-08-10 dazu: bis dahin trugen anlagenweit erfasste Ausgaben nur die HA-Sensoren, und die Fixture konnte das nicht sehen, weil sie die Position komponentengebunden anlegt.
Jede Ersparnis wird mit IHRER eigenen Laufzeit hochgerechnet. Die HA-Sensoren
jahres_ersparnis_euro,roi_prozentundamortisation_jahreteilten bis 2026-08-09 alles durch die Monatszahl der Anlage — auch die Wärmepumpen- und E-Auto-Ersparnis. Eine 2025 nachgerüstete Komponente wurde dadurch anteilig verdünnt, während ihre Kosten voll im Nenner standen. An einer vermessenen Anlage (31 Anlagenmonate, Wärmepumpe 25, zweiter Wagen 12) waren dadurch mindestens 3,1 Jahre reine Verdünnung: 26,4 statt höchstens 23,3 Jahre — eine Schranke, keine Schätzung, denn kein Posten lief länger als 25 Monate. Jetzt bringt jeder Posten seine eigene Monatszahl mit, und der Sensor-Rechenweg schreibt sie aus.
Und zwei verschiedene Amortisations-Angaben — Modell neben Messung:
| Angabe | Wo | Grundlage |
|---|---|---|
| Amortisationsdauer — Jahre und Break-Even-Jahr | Auswertungen → ROI | MODELL: Relevante Kosten ÷ prognostizierte Jahres-Einsparung, konstant hochgerechnet. Anker des Kalenderjahres ist das früheste Anschaffungsjahr der Investitionen; ohne gepflegtes Anschaffungsdatum bleibt es beim Jahres-Index ohne Jahreszahl. |
| Amortisations-Fortschritt | Auswertungen → ROI (Kachel daneben), Jahresbericht-PDF | MESSUNG: die tatsächlich erzielten Netto-Erträge seit Inbetriebnahme, geteilt durch dieselben relevanten Kosten. Formel-SoT core/berechnungen/amortisation.py. |
| Amortisation (Prognose) | PDF-Finanzbericht | Dieselbe Zahl wie Auswertungen → ROI (seit 2026-08-09). Vorher rechnete das PDF Gesamt-Anschaffung ÷ Σ einsparung_prognose_jahr — beide Seiten trugen nicht: der Nenner war die Gesamt- statt der relevanten Kosten, und der Zähler ein Feld ohne Schreiber (in keinem Formular, keinem Import, keinem Update-Schema). In der Praxis stand dort deshalb „—”. Seit 2026-08-10 ist das Feld als „Ertrag/Jahr” pflegbar (Wallbox/Sonstiges) — die Amortisation kommt trotzdem weiter aus dem ROI-Dashboard, damit alle vier Sichten dieselbe Zahl nennen. |
| Amortisation (HA-Sensor) | sensor.*_amortisation_jahre | Kapitaleinsatz ÷ Jahres-Ersparnis, wobei die Jahres-Ersparnis aus der gemessenen Historie annualisiert wird — je Posten mit seiner eigenen Laufzeit. Trägt denselben Nenner wie die drei anderen, aber einen Ist- statt Modell-Zähler; die Zahl liegt deshalb je nach Anlage über oder unter der Modell-Sicht. |
Die beiden ersten beantworten dieselbe Frage verschieden — „laut Rechnung in 9,2 Jahren” gegen „4.800 € von 12.000 € sind drin”. Das ist gewollt und in beiden Tooltips ausgeschrieben; Bedingung ist der gemeinsame Nenner, sonst ließen sich die Zahlen nicht ineinander überführen.
Verteilen sich die Anschaffungen über mehrere Jahre, ist das ausgewiesene Amortisationsjahr optimistisch (der Anker ist die erste Anschaffung, die Kosten sind die Summe). Der Break-Even-Text sagt das dazu.
Jede Dauer nennt ihre Annahme (seit 2026-08-10). Der Fortschritt unterstellt nichts, die Dauer muss etwas unterstellen — gewählt ist Modell A („es geht nie wieder etwas kaputt”, Begründung und die verworfenen Modelle B/C in
KONZEPT-WIRTSCHAFTLICHKEITSRECHNUNG.md§5). Formuliert wird der Satz einmal, im Layer-SoTcore/berechnungen/kapitalrechnung.py::annahme_dauer_text, und von allen Ausgabewegen abgeholt: ROI-Dashboard (gesamt und je Zeile), PDF-Finanzbericht, HA-Sensoramortisation_jahre(dort im Rechenweg-Attribut — ein Sensor hat keinen Tooltip) und die Restlaufzeit der Finanz-Prognose (amortisation_prognose_jahr): sie steht zwar in der gemessenen Kachel, rechnet aber den offenen Rest mit der Jahres-Prognose hoch und ist damit selbst eine Dauer-Aussage. Das Client-Pendantlib/amortisationAnnahme.tsträgt nur den einen Fall, für den es keine Backend-Zahl gibt: die Wallbox-Dauer im Komponenten-Hub, die ausAnschaffung ÷ Ersparnisim Client entsteht.⚠ Der Satz richtet sich nach den Daten, nicht nach dem Modellnamen. Sobald
betriebskosten_jahrgepflegt ist, rechnet eedc Modell C — der Betrag steht als Abzug im Zähler, und „ohne künftige Instandhaltung” wäre dann eine falsche Aussage über die eigene Rechnung. Gesichert durchtest_konzept_wirtschaftlichkeit_konformitaet.py::test_schritt6_*(Regression — die vier Quellen werden namentlich aufgerufen).
3.7 USt auf Eigenverbrauch
Abschnitt betitelt „3.7 USt auf Eigenverbrauch“SoT: core/berechnungen/ust_eigenverbrauch.py (Berechnungs-Layer, ADR-001)
Bedingung: Nur wenn Anlage.steuerliche_behandlung == "regelbesteuerung"
Die Selbstkosten je kWh sind eine Jahresgröße. Gerechnet wird deshalb je Kalenderjahr und summiert — auch wenn die Sicht einen mehrjährigen Zeitraum zeigt:
Bemessungsgrundlage = Σ max(0, anschaffungskosten_gesamt − anschaffungskosten_alternativ)Abschreibung_Jahr = Bemessungsgrundlage / 20 (20 Jahre lineare AfA)
je Kalenderjahr j: Jahreskosten_j = (Abschreibung_Jahr + Betriebskosten_Jahr) × Monate_j / 12 Selbstkosten_kWh_j = Jahreskosten_j / PV_Erzeugung_j USt_j = Eigenverbrauch_j × Selbstkosten_kWh_j × USt_Satz / 100
USt_Eigenverbrauch = Σ USt_j| Feld | Quelle |
|---|---|
Bemessungsgrundlage | Mehrkosten je Investition, geklemmt bei 0 — nicht die Vollkosten. Der volle Kaufpreis eines E-Autos gehört nicht in die Selbstkosten des PV-Stroms; maßgeblich ist, was er gegenüber der Alternative gekostet hat |
Betriebskosten_Jahr | Σ(Investition.betriebskosten_jahr) |
Monate_j | Monate des Jahres j, die im ausgewerteten Zeitraum liegen (1–12). Ein angeschnittenes Jahr trägt anteilig AfA — sonst stünden zwölf Monate Abschreibung gegen sieben Monate Ertrag |
PV_Erzeugung_j | PV-Erzeugung des Jahres j aus den Monats-Fakten |
USt_Satz | Anlage.ust_satz_prozent (DE: 19, AT: 20, CH: 8.1) |
Auswirkung: USt wird vom Netto_Ertrag abgezogen — Cockpit, Jahresbericht-PDF,
HA-Export, Aussichten und (seit 2026-08-04, Fund N-22) auch Auswertungen →
Finanzen + Tabelle. Bis dahin behauptete dieser Satz den Abzug für die
Auswertungen bereits, während der Client dort ohne ihn rechnete.
Je Monat verteilt (Auswertungen): die Formel ist linear im Eigenverbrauch, deshalb trägt jeder Monat
EV_m × Selbstkosten_je_kWh × USt_Satz. Der Nenner der Selbstkosten ist die Jahres-PV, damit Σ der Monatsbeträge exakt der Jahresbetrag bleibt. Auf Tagesebene gibt es die Größe nicht — Investitionssumme und Jahresertrag lassen sich keinem Tag zuordnen; bei Regelbesteuerung gilt daher Σ Tage ≠ Monat beim Netto-Ertrag (dieselbe bewusste Asymmetrie wie bei CO₂).Historie (2026-08-04, Funde N-129/N-130): Bis dahin standen im Baum vier Bemessungsgrundlagen nebeneinander — vier Sichten die Vollkosten, das Cockpit eine zusammengesetzte Summe, die als einzige
anschaffungskosten_alternativnicht las. Und die Sichten übergaben die Erzeugung ihres gesamten Zeitraums als „Jahres-Erzeugung”: bei Filter „alle Jahre” stand eine mehrjährige Menge im Nenner gegen eine Ein-Jahres-Abschreibung, die USt fiel um den Faktor der Jahresanzahl zu niedrig aus. Beides ist mit dem Layer-SoT aufgelöst.⚠ Was weiterhin auseinandergeht: welche Menge als Eigenverbrauch eingeht. Cockpit und HA-Export setzen den Netzpunkt-Eigenverbrauch ein (inklusive eines Brennstoff-Erzeugers), Jahresbericht, Aussichten und die Monatszeile den Finanz-Eigenverbrauch (PV allein). Bei einer Anlage mit Mini-BHKW nennen die beiden Gruppen deshalb verschiedene USt-Beträge. Register N-131.
3.8 CO2-Bilanz
Abschnitt betitelt „3.8 CO2-Bilanz“Endpoint: GET /api/cockpit/nachhaltigkeit/{anlage_id}
Sichten: Cockpit → Jahr/Gesamt, Block „CO₂-Bilanz” (seit 2026-07-31) ·
Auswertungen → CO₂, Blöcke „CO₂-Bilanz & Wirkung” und „CO₂-Amortisation” (seit 2026-07-31).
Eine Definition — wer sie bildet und wer sie liest
Abschnitt betitelt „Eine Definition — wer sie bildet und wer sie liest“| Rolle | Ort |
|---|---|
| Bildet die Zahl (einzige erlaubte Stelle) | core/calculations.py::berechne_co2_bilanz (ADR-001, DI-2) |
| Liefert sie je Monat aus | GET /api/cockpit/nachhaltigkeit/{id} (co2_pv_kg · co2_wp_kg · co2_emob_kg · co2_gesamt_kg · co2_kumuliert_kg) |
| Zeigt sie | Cockpit → Jahr (v4/JahrCo2Chart.tsx) · Auswertungen → CO₂ (v4/AuswertungenCo2V4.tsx, über useAuswertungBasis().co2) · HA-Sensor „CO₂ Einsparung” · PDF-Jahresbericht · WP-Dashboard |
| Rechnet nicht | der Client. CO2_FAKTOR_KG_KWH darf dort nur noch angezeigt werden (× 1000 → g/kWh); gewächtert von npm run check:co2-roh (Baseline 0) |
Warum das ausgeschrieben dasteht (N-21, 2026-07-31). Bis dahin standen im Produkt drei CO₂-Zahlen für denselben Monat: die kanonische im Cockpit — und zwei Überlebende der DI-2-Ablösung, die
Erzeugung × 0,38rechneten, also auch der eingespeisten kWh die volle Netzstrom-Vermeidung gutschrieben und weder Wärmepumpe noch E-Mobilität kannten (pages/auswertung/types.tsim Client,services/energie_profil/tage_werte.pyim Backend — ein Spiegelpaar, Monatstabelle und Tagestabelle). Beide sind auf den Kanon umgestellt. Das war keine Definitionsfrage, sondern eine unvollendete Migration.
Jahres-Scope: Der Endpoint kennt kein
?jahr=und liefert die gesamte Historie — der Jahresfilter sitzt in der Sicht (v4/JahrCo2Chart.tsx::baueJahrCo2ChartDatenbzw.v4/AuswertungenCo2V4.tsx::baueCo2Monatsreihe) und greift auf die ganze Monatszeile, nicht auf einzelne Serien. Nicht jahresgebunden istco2_kumuliert_kg: eine Lebensdauer-Größe, die deshalb als eigener Kennwert („CO₂ kumuliert”) steht und nicht als Linie im Jahres-Chart — eine kumulierte Kurve, die im Januar auf halber Höhe beginnt, erklärt sich nicht selbst. Aus demselben Grund rechnet die CO₂-Amortisation (Auswertungen → CO₂, Block ②) immer gegenco2_kumuliert_kgder gesamten Historie, auch wenn ein Einzeljahr gefiltert ist — sichtbar gekennzeichnet.
Der Tageswert trägt nur den PV-Anteil
Abschnitt betitelt „Der Tageswert trägt nur den PV-Anteil“Die Spalte „CO₂-Einsparung (PV)” der Werte-Tabelle (Auswertungen → Tabelle,
Monats- und Tages-Granularität) zeigt bewusst nur co2_pv_kg:
CO2-Einsparung (PV) = Eigenverbrauch * 0.38Warum nicht die volle Bilanz: WP-Wärme und E-Mobilitäts-Kilometer sind
Monatsgrößen (InvestitionMonatsdaten). Stündlich liegt von der Wärmepumpe nur die
Stromaufnahme vor (TagesEnergieProfil.waermepumpe_kw) — ohne Wärmemenge ist die
WP-Ersparnis nicht bestimmbar, und eine allein aus dem Stromverbrauch gebildete
Komponente wäre rein negativ. Eine Spalte, die im Monat drei Quellen und am Tag eine
addiert, wäre über die Granularitäten nicht summierbar.
Folge, die nicht stillschweigend bleiben darf: Σ Tage ≠ CO₂-Monatswert, sobald die Anlage eine Wärmepumpe oder ein E-Auto hat. Die Differenz ist genau
max(0, CO2_WP) + max(0, CO2_E-Mob). Die vollständige Bilanz zeigen Cockpit → Jahr und Auswertungen → CO₂.
Monatliche CO2-Berechnung
Abschnitt betitelt „Monatliche CO2-Berechnung“CO2_PV = Eigenverbrauch * 0.38 (vermiedener Netzstrom)CO2_WP = (WP_Wärme / 0.9 * 0.201) - (WP_Strom * 0.38) (vs. Gasheizung)CO2_E-Mob = (Benzin_L * 2.37) - ((Ladung - PV_Ladung) * 0.38) (vs. Benziner)CO2_gesamt = CO2_PV + max(0, CO2_WP) + max(0, CO2_E-Mob)Kanonische CO₂-Helfer (SoT
core/calculations.py, DI-1/DI-2): Diese Bilanz läuft über genau eine Helfer-Familie —berechne_co2_bilanz(setzt PV + WP + E-Mob zusammen), internco2_wp_ersparnis_kg(WP: vermiedenes Gas MINUS WP-Strom-CO₂; der Gas-Wirkungsgrad η_gas = 0,90 kommt ausWP_WIRKUNGSGRAD_GAS_DEFAULT) undco2_emob_ersparnis_kg. Cockpit-CO₂-Kachel, HA-Export-Sensor „CO₂ Einsparung”, WP-Dashboard und der PDF-Jahresbericht rufen dieselben Helfer auf und zeigen daher denselben Wert (vorher rechneten einzelne Pfadepv_erzeugung × f_strombzw. eigene WP-Formeln → Drift). Die WP- und E-Mob-Komponente werden für die Summe bei 0 geklammert (negative Einzelwerte kürzen die Gesamtbilanz nicht). Brennstoff-Erzeuger (BHKW/„sonstiges”) erzeugen bewusst keine CO₂-Gutschrift — sie zählen zwar in EV/Autarkie (hinter dem Zähler), aber nicht als vermiedenes CO₂.
Äquivalente
Abschnitt betitelt „Äquivalente“Bäume = CO2_gesamt / 20 (kg/Baum/Jahr)Auto-km = CO2_gesamt / 0.12 (kg/km)Flug-km = CO2_gesamt / 0.25 (kg/km)3.9 PV-String SOLL-IST Vergleich
Abschnitt betitelt „3.9 PV-String SOLL-IST Vergleich“Endpoint: GET /api/cockpit/pv-strings/{anlage_id}?jahr=
Was eine „String”-Zeile ist — die Erzeuger-Abgrenzung (F-10)
Abschnitt betitelt „Was eine „String”-Zeile ist — die Erzeuger-Abgrenzung (F-10)“Die Zeilen dieser Sicht sind PV-Erzeuger, nicht PV-Module. Maßgeblich ist
PV_ERZEUGER_TYPEN(SoTcore/berechnungen/spez_ertrag.py) =pv-module+balkonkraftwerk. Ein Balkonkraftwerk trägt alles, was die Sicht braucht: kWp überget_erzeuger_kwp(beim BKWleistung_wp × anzahl), Ausrichtung und Neigung als eigene Formularfelder, und seit #367 ein eigenes PVGIS-SOLL.Betroffen sind vier Ausgaben derselben Sicht —
GET /pv-strings,GET /pv-strings-gesamtlaufzeit, Abschnitt 10 des Jahresbericht-PDF und die beiden Leertexte im Client. #367 hatte nur die zwei PVGIS-Endpunkte erweitert und im Issue-Text „zwei Endpunkte” behauptet; es waren fünf. Klasse #236 — ein Filter auf einer Schicht reicht nicht, wenn parallele Pfade existieren.⚠ Die IST-Quelle ist je Typ eine andere, und das ist Absicht. Ein
pv-moduleholt seinen Wert ausErzeugungFakten.pv_je_modul(P7-Auflösung); einbalkonkraftwerksteht dort nicht, sondern inBkwFakten.erzeugung_je_investition. Grund: die Σ vonpv_je_modulistpv_module_kwh, und die geht in die ROI-Rechnung, wo das Balkonkraftwerk eine eigene Zeile hat (investitionen/crud.py::get_pv_erzeugung) — läge es in beiden, zählte seine Erzeugung dort doppelt. Wer die Sicht erweitert, erweitert deshalb nichtpv_je_modul. Gewächtert intests/test_bkw_erzeuger_sichten_f10.py.Kein zweiter Erfassungsweg.
pv-moduleunterbalkonkraftwerkbleibt verboten (models/investition.py::ERLAUBTE_PARENT_TYPEN): das wäre dieselbe Erzeugung zweimal erfasst, mit doppelter kWp als Folge — der Workaround, den der Melder selbst zurückgenommen hat. Wer mehrere Ausrichtungen hat, erfasst Wechselrichter + PV-Module; die Abgrenzung steht in HANDBUCH_EINSTELLUNGEN §3.5.Dieselbe Erzeuger-Abgrenzung gilt für die Community-Stammdaten (
services/community_service.py): Neigung und Ausrichtung werden über beide Typen gemittelt. Vorher fiel eine reine Balkonkraftwerk-Anlage auf die Annahme 30° / Süd zurück — der Community-Server rechnet nichts nach, die Anlage wurde also gegen die falsche Vergleichsgruppe gemessen.
SOLL-Berechnung (PVGIS)
Abschnitt betitelt „SOLL-Berechnung (PVGIS)“Genau eine Prognose ist die aktive. eedc bewahrt beliebig viele PVGIS-Abrufe einer Anlage auf; gelesen wird ausschließlich die als aktiv markierte — auch wenn das bewusst eine ältere ist (Nutzerwille, Einstellungen → Solarprognose). Auswahlregel:
ist_aktiv == True,ORDER BY abgerufen_am DESC,LIMIT 1; SoTservices/prognose_auswahl.py, datenbankseitig gesichert durch einen partiellen Unique-Index (ADR-002/P5). Ist keine Prognose aktiv, bleibt die SOLL-Seite leer, statt eine beliebige zu zeigen.
Wann eine Prognose von selbst nachgezogen wird (#363). Eine Prognose wird beim Abruf eingefroren; ändert sich die Anlage danach, rechnet jede SOLL-Sicht gegen eine Anlage, die es nicht mehr gibt (gemeldeter Extremfall: 357 MWh Jahres-SOLL für ein 2,4-kWp-Balkonkraftwerk). Der nächtliche Job
pvgis_aktualitaetprüft deshalb je Anlage, ob die aktive Prognose noch passt — SoTservices/pvgis_aktualitaet.py, dieselbe Funktion versorgt die Statusanzeige der Einstellungs-Kachel. Auslöser sind ausschließlich:
Auslöser Vergleich Nennleistung Σ get_erzeuger_kwpder aktiven Erzeuger gegengesamt_leistung_kwpAusrichtung / Neigung nach kWp gewichtet, wie im Speicherpfad Standort latitude/longitudeder AnlageHorizontprofil hinzugekommen oder entfernt Strahlungsdatensatz raddatabaseder Zeile gegen den der konfigurierten API-VersionDas Alter ist ausdrücklich KEIN Auslöser. PVGIS rechnet auf einem abgeschlossenen Klimamittel (API v5_2 → PVGIS-SARAH2 2005–2020, v5_3 → PVGIS-SARAH3 2005–2023); bei unveränderten Eingaben liefert ein zweiter Abruf dieselbe Zahl, ein turnusmäßiger Abruf wäre Last ohne Wirkung. Die Systemverluste sind ebenfalls kein Auslöser, sondern werden in den Neuabruf übernommen — sie sind nirgends sonst gespeichert. Die abgelöste Prognose bleibt als inaktive Zeile erhalten.
⚠ Mit #363 hebt eedc die API-Version von v5_2 auf v5_3. Damit wechselt der Strahlungsdatensatz von SARAH2 auf SARAH3 — für dieselbe Anlage rund +2 % (am 2026-08-07 gemessen: 9,8 kWp Süd 35°, 10.495,79 → 10.727,57 kWh). Bestandsprognosen tragen kein
raddatabaseund werden deshalb genau einmal automatisch nachgezogen; danach rechnen alle Installationen auf derselben Grundlage.
Wann die AC-Kappung NICHT greift (F-11). Die Grenze begrenzt, was ein Wechselrichter ins Haus abgibt — nicht, was die Module ernten. Hängt am Träger der Grenze ein DC-gekoppelter Speicher, läuft der Überschuss gleichstromseitig in den Akku, ohne je durch den Wechselrichter zu müssen; er ist dann nicht verloren, und eine Kappung des Erzeugungsprofils würde ihn wegrechnen. SoT
core/berechnungen/wr_kappung.py::_dc_speicher_traeger.
Lage Kappung kein Speicher am Träger ja (#347/#354 unverändert) DC-gekoppelter Speicher am Träger nein — der Überschuss lädt den Akku AC-gekoppelter Speicher am Träger ja — alles läuft durch den Wechselrichter Speicher ohne Zuordnung, oder an einem anderen Wechselrichter ja — er kann den Überschuss dieses Erzeugers nicht aufnehmen Warum die IST-Größe die Ernte vor dem Speicher ist — das entscheidet nicht der Hersteller, sondern die eigene Bilanz:
direktverbrauch = max(0, pv − einspeisung − speicher_ladung)(§3.1). Die Speicherladung wird von der PV-Summe abgezogen, sie muss darin enthalten sein. Die Sensor-Referenz sagt dasselbe in Worten, und der BKW-Akku-Kanon führt Ladung/Entladung als eigene Speicher-Investition daneben.⚠ Damit ändert die Kopplung eines Speichers erstmals eine Zahl. Bis v4.0.10 war
parameter.kopplung(#351) rein beschreibend — sie sagte, wo gemessen wird, und keine ADR-001-Formel las sie. Ab F-11 liestwr_kappungsie, und die Wirkung bleibt auf SOLL-Werte beschränkt: Prognose-Kanon und PVGIS-Monatsprognose. Kein IST-Pfad, keine Energiebilanz, keine Finanzrechnung. Wer die Kopplung eines Speichers ändert, verschiebt seither sein SOLL.
Ab v2.3.2 (Per-Modul PVGIS-Daten vorhanden):
SOLL_Monat = PVGISPrognose.module_monatswerte[modul_id][monat].e_mFallback (ältere Prognosen - proportional nach kWp):
kWp_Anteil = Modul_kWp / Gesamt_kWpSOLL_Monat = PVGISPrognose.monatswerte[monat].e_m * kWp_AnteilDer PDF-Jahresbericht nutzt seit v4.0.1 denselben Weg (vorher verteilte sein String-Vergleich die Prognose immer nach kWp, obwohl die Pro-Modul-Werte gespeichert sind — bei Ost-West-Dächern ~20–25 % Abweichung gegenüber dem Cockpit).
Faire Vergleichsbasis (ab v2.3.2): SOLL wird NUR für Monate gezählt, die auch IST-Daten haben. Verhindert aufgeblähten SOLL bei Teil-Jahren.
Der laufende Monat zählt anteilig (ab v4.0.9, N-69): PVGIS liefert Monatssummen — im laufenden Monat stünde diese volle Summe als Nenner über einem angefangenen Ertrag. Die Quote maß dann das Datum statt die Anlage: am 4. August meldete eine gesunde Anlage 19 % SOLL-Erfüllung (264,8 IST gegen 1.387,9 SOLL), während dieselbe Anlage über Jan–Jul auf 119 % kam; in der Jahres-Kachel wurden daraus 104 % statt 119 %.
Tage = min(heutiger Tag, Tage im Monat) im laufenden Monat, sonst alle TageSOLL_kWh = PVGIS-Monatswert × Tage ÷ Tage im MonatGekürzt wird der Nenner, nicht das Zeitfenster des IST (Entscheid 2026-08-04) — sonst verlöre die Monatssicht ihre einzige Einordnung des PV-Werts bis zum Monatsabschluss. Der laufende Tag zählt voll mit: ihn wegzulassen machte den Nenner kleiner und die Quote höher, also genau die Richtung, aus der der Fehler kam. Innerhalb des Monats gilt Gleichverteilung; am Beispiel oben liegt die Jahresquote damit bei 119,8 % gegen 119,2 % aus den abgeschlossenen Monaten allein.
Ein Monat in der Zukunft hat null Tage und damit kein SOLL — die Sichten lassen die Quote weg, statt 0 % für einen Monat zu melden, der noch nicht stattgefunden hat. Abgeschlossene Monate und damit die gesamte Historie bleiben unberührt.
SoT der Formel: core/berechnungen/monatsfenster.py (auch die Grundlast-Hochrechnung und die
Speicher-Auslastung zählen ihre Tage dort, statt jede für sich). Die Jahres-Sicht summiert die
Monatswerte und erbt die Kürzung ohne eigene Rechnung; die Oberfläche weist das Fenster aus
(„anteilig · 4 von 31 Tagen”).
Der Anschaffungsmonat zählt ebenso anteilig (ab v4.0.19, F-34): Dieselbe Schieflage gibt es am anderen Ende — nur kommt die Kante dort nicht aus dem Kalender, sondern aus den Stammdaten. Eine Anlage, die am 19.03. ans Netz ging, bekam den vollen PVGIS-März gegenübergestellt: 175,1 kWh SOLL gegen 60,8 gemessene, Performance Ratio 0,347 — während dieselbe Anlage in jedem vollen Monat über 1,0 lag (April 1,039 · Juli 1,139). Nicht die Anlage war schwach, der Vergleich war schief; das Jahres-PR fiel dadurch von ~1,08 auf 0,973.
Tage = Tage des Monats zwischen Anschaffungs- und Stilllegungsdatum (beide inklusive)SOLL_kWh = PVGIS-Monatswert × Tage ÷ Tage im MonatGekürzt wird wieder der Nenner, aus demselben Grund. Der Stilllegungsmonat ist die Spiegelkante und wird genauso behandelt.
⚠ Zwei Datums-Ebenen, zwei Formeln — bewusst nicht zusammengelegt.
monatsfensterbeantwortet „wie viel des Monats ist am Stichtag vergangen” (Kalender),monatsfenster_investition„wie lange gab es dieses Gerät in diesem Monat” (Stammdaten). Wer beide braucht, ruft beide und nimmt das kleinere Fenster. Das ist dieselbe Trennung, die ARCHITEKTUR §4 füranschaffungsdatumgegenAnlage.installationsdatumzieht — ihre Vertauschung hat schon zweimal zu Abstürzen geführt.
Die saisonale Zeile der Laufzeit-Sicht kürzt bewusst nicht: sie stellt einen IST-Durchschnitt über mehrere Jahre einem Klimamittel gegenüber und fragt „wie fällt der Mai typischerweise aus”. Ein einzelner angebrochener Anschaffungsmonat verzerrt dort bereits den IST-Durchschnitt; beide Seiten zu kürzen hieße, ihn doppelt zu bestrafen. Melder: Discussion #366 (azywietz-web).
SOLL im Monatsbericht: derselbe Grundsatz — der Monatswert kommt aus den Monatszeilen genau der aktiven Prognose. Vor v4.0.1 stand dort eine Summe über alle aktiven Prognosen; bei einem Bestand mit zwei aktiven war der SOLL-PV-Wert verdoppelt, und mit ihm die SOLL/IST-Abweichung und die Grundlast-SOLL-Kachel.
AC-Kappung im SOLL (ab v4.0.9, #354/#367)
Abschnitt betitelt „AC-Kappung im SOLL (ab v4.0.9, #354/#367)“Das PVGIS-SOLL rechnet aus der Modulleistung (DC) und kennt die Grenze des Wechselrichters nicht. Bei einer überbelegten Anlage ist es damit systematisch unerreichbar: was das Gerät mittags abriegelt, taucht im SOLL/IST-Vergleich als Minus auf, das der Betreiber nicht zu verantworten hat.
Die Kappung wirkt stündlich, nie als kWp-Deckel — ein 7-kW-Gerät begrenzt die Mittagsspitze,
nicht den Morgen (Begründung im Modul-Docstring core/berechnungen/wr_kappung.py). Weil PVcalc
nur Monatssummen liefert, entsteht der Faktor aus einem eigenen seriescalc-Stundenprofil
derselben Anlage:
Faktor_Monat = Σ min(Stunde, AC-Grenze) ÷ Σ Stunde (über 2018–2020, ertragsgewichtet)SOLL_Monat = PVcalc.e_m × Faktor_MonatDamit bleibt PVGIS die einzige Ertragsquelle; hier entsteht kein zweiter Ertragswert, nur ein Faktor ≤ 1. Drei Jahre statt einem, weil der Faktor sonst das Wetter eines Einzeljahres trägt (am Demo-Standort schwankt der April zwischen 0,804 und 0,875).
Die Grenze gehört dem Wechselrichter, nicht der Himmelsrichtung. Mehrere Strings an einem Gerät teilen sich seine AC-Grenze und werden gemeinsam gekappt, anteilig nach ihrem Stundenbeitrag — auch über Orientierungsgruppen hinweg. Am Demo-Bestand (Süd 12 · Ost 5 · West 3 kWp an einem 10-kW-Fronius) ist das der ganze Effekt: je String einzeln gekappt bliebe das SOLL unverändert, weil kein einzelner String allein 10 kW erreicht — gemeinsam liefern sie 1.227 kWh im Jahr, die das Gerät nie abgeben kann (20.812 → 19.585 kWh, −5,9 %; April −10 %, November/Dezember 0).
Woher die Grenze kommt (SoT core/investition_kennwerte.get_wr_grenze_kw, Zuordnung
wr_kappung.zuordne_grenzen):
| Erzeuger | Grenze | Geteilt? |
|---|---|---|
balkonkraftwerk | eigener Parameter wechselrichter_leistung_w | nein — Erzeuger und Wechselrichter sind ein Gerät |
pv-module | max_leistung_kw des zugeordneten Wechselrichters (Fallback: Legacy leistung_ac_kw, dann die Spalte leistung_kwp, die dort kW AC trägt) | ja — alle Strings desselben Geräts |
Ohne gepflegte Grenze wird nicht gekappt (None, kein Default) und PVGIS gar nicht zusätzlich
gefragt — die Prognose bleibt dann bitgleich zu vorher. Fällt der seriescalc-Abruf aus, gibt es
keine Faktoren statt geratener: ein ungekapptes SOLL ist eine bekannte Größe, ein halb
gekapptes wäre keine.
Derselbe Layer kappt seit v4.0.4 die Tages-Prognose (services/prognose_kanon.py); seit
v4.0.9 sehen beide Pfade dieselben Grenzen und dieselbe Gruppierung.
IST-Berechnung
Abschnitt betitelt „IST-Berechnung“Der IST-Wert je Modul kommt aus dem Read-time-SoT core/berechnungen/pv_verteilung.py
(resolve_pv_je_modul) — nicht aus einem rohen Feldzugriff. Präzedenz:
1. Messwert InvestitionMonatsdaten.verbrauch_daten["pv_erzeugung_kwh"] → Quelle „gemessen" — IMMER und AUSNAHMSLOS2. Lücke füllen (Monatsdaten.pv_erzeugung_kwh − Σ gemessene) × kWp_Anteil, nur auf die Module OHNE eigenen Wert → Quelle „geschätzt (kWp-Anteil)", in der Anzeige gekennzeichnet3. keine Quelle kein Wert (kein 0)Die Präzedenz ist modulweise, nicht anlagenweit (ab 2026-07-29). Bis dahin genügte ein Modul ohne eigenen Wert, damit der Gesamtwert über alle Module verteilt wurde — die echten Messwerte der übrigen Strings wurden dabei verworfen und durch kWp-Anteile ersetzt. Dafür ist der Gesamtwert nicht da: er füllt Lücken, er überschreibt keine Messungen. Teil-Messung ist auch kein Sonderfall — sie entsteht bei jedem Sensor-Aussetzer, jedem neu angelegten String und in jedem Monat vor der Umstellung auf Pro-String-Messung.
Übersteigt die Summe der Messwerte den Gesamtwert, wird der Rest auf 0 geklemmt statt negativ
verteilt (Σ > Gesamtwert) — das ist ein Messfehler und gehört gemeldet, nicht weggerechnet.
Der Gesamtwert ist ausschließlich Eingang dieser Auflösung. Er darf in keiner einzelnen Berechnung direkt gelesen werden; jede PV-Zahl kommt aus der Pro-Modul-Schicht bzw. deren Summe. Zielbild für die Erfassung: alle Strings erfassen und „PV gesamt” auf „keine” setzen — zusammengefasst wird höchstens je Ausrichtung/Neigung, sonst kippt die Multi-Orientierungs-Prognose. Die anteilige Verteilung ist ein Übergangswerkzeug, kein Dauerzustand.
Der kWp-Anteil ist ein Prognose-, kein Ertragsschlüssel (ADR-002/P2): auf der IST-Seite verteilt er nur, wenn kein Messwert existiert — und dann sichtbar beschriftet. Solange die Werte verteilt sind, nennen die String-Sichten bewusst keinen besten oder schwächsten String: eine Platzierung wäre dort nur die Reihenfolge der Nennleistungen.
Regel 1 gilt nur für echte Messungen (#352, seit v4.0.9)
Abschnitt betitelt „Regel 1 gilt nur für echte Messungen (#352, seit v4.0.9)“Die Präzedenz oben liest „ein eigener Wert in der Zeile = gemessen”. Das stimmt nur, solange in der
Zeile keine gerechnete Zahl steht — und zwei Schreibwege legen genau das dorthin: der Import
einer Legacy-Gesamtspalte (_distribute_legacy_pv_to_modules, aus CSV-Backup, Portal- und
Custom-Import) und der Monatsabschluss, wenn der zerlegte Vorschlag von Connector oder Cloud-Import
übernommen wird (_mapped_or_distribute). Beide schrieben ihre Aufteilung bis dahin mit derselben
Herkunft wie eine Gerätemessung; danach klassifizierte Regel 1 sie als gemessen — mit Ranking und
grünem Daten-Checker als Folge.
Seither vermerken diese Wege am Wert, dass er gerechnet ist:
InvestitionMonatsdaten.source_provenance["verbrauch_daten.<feld>"]["abgeleitet"] trägt
kwp_anteil (PV) bzw. kapazitaet_anteil (Speicher). SoT der Marken und ihrer Positivliste:
services/provenance.py (ABGELEITET_*, gepruefte_ableitung). Der Ladepfad
services/pv_monatswerte.py liest sie aus derselben Zeile — kein Join, keine zweite Abfrage —
und übergibt sie als PvModul.eigen_ist_abgeleitet; resolve_pv_je_modul liefert dafür
QUELLE_VERTEILT statt QUELLE_GEMESSEN, ohne den Wert anzufassen.
Drei Grenzen, alle bewusst:
- Ein Empfänger = keine Zerlegung. Geht der Gesamtwert an genau ein Modul bzw. einen Speicher,
ist er unverzerrt dort und bleibt
gemessen— dieselbe Grenze, die der Monatsabschluss für Beschriftung und Konfidenz schon vorher zog (ist_verteilt). - Der Client meldet die Herkunft, das Backend rät sie nicht. Nur die Oberfläche weiß, ob der
Anwender den zerlegten Vorschlag übernommen oder eine eigene Zahl getippt hat; sie schickt die
Marke im Investitions-Payload als
abgeleitet_feldermit (gleiches Muster wiegeprueft_gegen). Client-SoT:lib/erfassungZustand.ts::abgeleiteteMarke— sie schweigt, wenn ein gleich hoher Vorschlag ohne Marke danebensteht, weil dann nicht entscheidbar ist, welcher übernommen wurde. - Kein Altbestand-Heilen. Bereits gespeicherte Zeilen bleiben mehrdeutig: derselbe
writersteht dort für „echte Pro-Modul-Spalte aus der CSV” und für „verteilt”. Rückwirkend trennen geht nicht ([[feedback_kein_grosser_heiler_knopf]]); die Marke wirkt ab dem nächsten Schreibvorgang.
Der Daten-Checker zählt markierte Werte nicht als Messung. Damit ein vollständig importierter Monat
dadurch nicht von OK auf ERROR fällt, kennt klassifiziere_pv_monat den Parameter n_abgeleitet:
decken die gerechneten Werte den Monat ab, ist er verteilt (INFO) — die Zahlen sind da, sie sind
nur nicht gemessen.
Benannte Ausnahme (ADR-002/P2-A): Ist nur ein Teil der Module gemessen und kein Gesamtwert hinterlegt, behält die Pro-Modul-Sicht ihre Messwerte, während die Anlagen-Summe bewusst nichts zeigt.
Σ Strings ≠ Σ Anlageist dort gewollt — eine Teilsumme als „Gesamt-PV” auszuweisen wäre systematisch zu klein.
Beim Import und beim Monatsabschluss gilt dieselbe Rangfolge auf der Vorschlags-Seite: Ist ein Connector-Feld einer Komponente zugeordnet, geht der volle Zählerstand dorthin („Vom Wechselrichter (Zählerstand-Differenz)”); ohne Zuordnung wird nach Nennleistung verteilt und heißt dann „Gesamtwert, anteilig nach kWp auf die Strings verteilt” — mit niedrigerer Konfidenz als jede gemessene Quelle. Der Zuordnungs-Schritt des Import-Wizards schlägt die Anteile ebenfalls nach Nennleistung vor (bzw. nach Kapazität bei Speichern); ist die Bezugsgröße nirgends gepflegt, verteilt er gleichmäßig und sagt dazu, dass das keine proportionale Aufteilung ist.
IST je Erzeuger auf Tagesebene (ab v4.0.9, #350)
Abschnitt betitelt „IST je Erzeuger auf Tagesebene (ab v4.0.9, #350)“Die Präzedenz oben gilt für Monatswerte. Auf der Tagesebene gibt es sie nicht — dort wird nicht verteilt:
erzeuger_kwh[inv_id] = Σ komponenten_kwh[pv_<id> | bkw_<id>] (Boundary-Rollup) ∨ Σ TagesEnergieProfil.komponenten je Stunde (Fallback, kein Rollup)kein eigener Sensor ⇒ kein Eintrag (kein 0, kein kWp-Anteil)SoT der Formel: core/berechnungen/energie.py::erzeuger_kwh_je_investition, ausgeliefert als
TagWerteResponse.erzeuger_kwh (GET /api/energie-profil/{id}/tage-werte). Zwei Eigenschaften
sind dabei nicht optional:
- Der Schlüssel ist die Investitions-ID, nicht der Komponenten-Key. Dasselbe Balkonkraftwerk
heißt im Live-Keyspace
pv_<id>und im Boundary-Keyspacebkw_<id>(snapshot/komponenten_beitraege._TYP_PREFIXgegenlive_komponenten_builder). Je Roh-Key gruppiert bekäme ein Gerät zwei Spalten, deren Belegung vom Schreibpfad des jeweiligen Tages abhängt — dieselbe Mismatch-Klasse wie der BKW-Doppelzählungs-Bug vom 2026-05-19. - Keine kWp-Verteilung. Der Monatspfad füllt Lücken nach Nennleistung und kennzeichnet das; eine so gefüllte Tageszahl unter der Überschrift „Dach Süd” wäre von einer Messung nicht mehr zu unterscheiden (die Klasse aus #352). Fehlt der Sensor, nennt die Oberfläche das Gerät und den Weg zur Zuordnung, statt eine Spalte zu zeigen.
Der Anlagen-Zählerstand: Summe ja, Aufschlüsselung nein (ab 2026-08-07)
Abschnitt betitelt „Der Anlagen-Zählerstand: Summe ja, Aufschlüsselung nein (ab 2026-08-07)“Das Feld Anlage (Basis) → PV-Erzeugung Zählerstand (kWh) landet über basis["pv_gesamt"] in
Monatsdaten.pv_erzeugung_kwh und ist damit Eingang der Monats-Auflösung (P7). Seit
2026-08-07 ist es zusätzlich ein Snapshot-Zähler der Kategorie pv
(snapshot/keys.py::BASIS_ZAEHLER_FELDER) und trägt damit auch Tag und Stunde — als Summe der
ganzen Anlage. Wer nur einen Summenzähler hat, bekommt also vollständige Tageswerte; was fehlt,
ist allein die Aufschlüsselung je Erzeuger (obenstehende Formel liefert für ihn kein pv_<id>).
⚠ Bis dahin galt hier das Gegenteil („erreicht die Tagesebene gar nicht”), und das war der Fehler F-7: eine Anlage mit einem Zähler und mehreren Ausrichtungen hatte gar keine Tages-PV. Der naheliegende Ausweg — alle Ausrichtungen als eine Investition führen — bleibt falsch:
services/pv_orientation.pygruppiert je Investition nach (Neigung, Azimut), eine zusammengelegte Anlage bekäme einen systematisch falschen Tagesgang im gesamten Prognose-Kanon inklusive HA-Prognose-Sensoren und PVGIS-SOLL.
Die Regel ist alles-oder-nichts, nicht anteilig. Der Anlagen-Zählerstand zählt auf der
Tagesebene nur mit, solange kein Erzeuger einen eigenen kWh-Zähler trägt — genau wie im
Live-Pfad (not has_individual_pv). Sobald einer misst, gilt für Tag und Stunde nur noch, was je
Erzeuger gemessen ist. Zwei Gründe:
- Nebeneinander ginge nicht.
komponenten_kwhhat einen flachen Keyspace, und die Tages-PV ist die Summe allerpv_/bkw_-Schlüssel (summe_pv_bkw_kwh). Stündepv_gesamtnebenpv_7, wäre die Anlagensumme neben ihrem eigenen Summanden gebucht — Doppelzählung. - Der Rest ließe sich nur raten. Die Differenz „Anlagensumme minus gemessene Erzeuger” auf die übrigen zu verteilen, wäre eine kWp-Schätzung mit dem Aussehen einer Messung — dieselbe Klasse, die der Absatz „Keine kWp-Verteilung” oben ausschließt.
Folge für die Praxis: ein halber Umbau macht die Tageswerte schlechter, nicht besser. Wer
einem von drei Strings einen eigenen Zähler zuordnet, verliert die anderen beiden auf der
Tagesebene. Die Zuordnungs-Fläche und der Daten-Checker sagen das an der Zeile
(datenquellen_validierung.finde_aggregat_teilweise_verdraengt, WARNING nur in dieser
Teilbelegung — nicht, wenn der Summenzähler die ganze Anlage trägt). Die Monatswerte sind in
allen drei Lagen vollständig.
Bleibt gar kein kumulativer PV-Zähler übrig — weder je Erzeuger noch für die Anlage —, sagt die
Tagessicht das, statt zu rechnen: TagesBilanz.pv_erfasst trennt
„0 kWh gemessen” (Nacht, Schnee — gültig) von „nicht erfasst”. Im zweiten Fall bleiben
Erzeugung, Eigenverbrauch, spezifischer Ertrag, Performance Ratio und CO₂ leer. Vorher stand
dort 0 − Einspeisung, also ein negativer Eigenverbrauch neben einem Peak-PV-Wert aus dem
Leistungssensor (Forum kaba-kakao, T89667 #109). Regel-SoT:
KONZEPT-UNVOLLSTAENDIGE-WERTE — eine Summe darf 0 bleiben,
eine Differenz mit fehlendem Summanden nicht. Die Zuordnungs-Seite und der Daten-Checker nennen
in dieser Lage den Weg über einen HA-Integral-Sensor je Erzeuger
(HANDBUCH_DATEN_CHECKER §5.2).
Der Client schlüsselt ab zwei Erzeugern auf (lib/erzeugerSpalten.ts, geteilt von
Cockpit → Tag und Auswertungen → Tabelle) und berücksichtigt Anschaffungs-/Stilllegungsdatum.
Im Stundenverlauf ersetzen die Geräte-Flächen die PV-Fläche, statt auf ihr zu liegen; der
ungedeckte Rest (pvRestKw) steht als „PV (übrige)” daneben, damit die Stapelhöhe die Erzeugung
bleibt. Die Energie-Bilanz (erzeugung, pv_anlage, bkw) rechnet unverändert aus ihren eigenen
Quellen — die Aufschlüsselung ist eine Auskunft, keine Summe.
Kennzahlen pro String
Abschnitt betitelt „Kennzahlen pro String“Abweichung_kWh = IST - SOLLAbweichung_% = (IST - SOLL) / SOLL * 100Performance_Ratio = IST / SOLLSpez. Ertrag (kWh/kWp) = IST_Jahr / Modul_kWpOhne aktive PVGIS-Prognose entfallen Abweichung und Performance Ratio (None); IST,
Ertragsanteil und spezifischer Ertrag bleiben. Die Sicht zeigt sie seit v4.0.9 auch an — bis dahin
brach sie ohne Prognose vollständig ab und verbarg damit auch die gemessenen Werte (#350).
3.10 Sonstige Positionen
Abschnitt betitelt „3.10 Sonstige Positionen“Utility: utils/sonstige_positionen.py
Sonstige Positionen sind frei erfassbare Erträge/Ausgaben je Monat (Reparaturen, Wartung, THG-Quote, Abschlag, Guthaben-Auszahlung …), erfasst im Monatsdaten-Formular. Es gibt sie auf zwei Ebenen:
- Komponenten-Ebene:
InvestitionMonatsdaten.verbrauch_daten["sonstige_positionen"](seit jeher). - Anlage-/Basis-Ebene (G19-1, ab v4.0):
Monatsdaten.sonstige_positionen— für Positionen, die keiner einzelnen Komponente zuzuordnen sind. Gelesen über den Spiegel-Helperget_md_sonstige_positionen.
Vorrang neues Format:
get_sonstige_positionen()liest zuerstsonstige_positionen; nur wenn der Schlüssel fehlt, greift der Legacy-Fallbacksonderkosten_euro/sonderkosten_notiz(→ eine Ausgabe-Position). Eine additive Start-Migration materialisiert Alt-sonderkosten_euro > 0als „… (migriert)“-Ausgabe; die Legacy-Spalten bleiben lesbar (deprecated, nicht neu befüllen). Wirkung für Bestandsdaten: Alt-Sonderkosten, die bis v3.45 in keiner Berechnung auftauchten, zählen ab v4.0 in den Finanz-Summen (s. §3.2).
Jede InvestitionMonatsdaten.verbrauch_daten bzw. Monatsdaten-Zeile kann sonstige Positionen enthalten:
{ "sonstige_positionen": [ {"bezeichnung": "THG-Quote", "betrag": 200.00, "typ": "ertrag"}, {"bezeichnung": "Wartung", "betrag": 50.00, "typ": "ausgabe"} ]}Legacy-Format (backward-kompatibel):
{"sonderkosten_euro": 50.0, "sonderkosten_notiz": "Wartung"}wird automatisch zu [{"bezeichnung": "Wartung", "betrag": 50.0, "typ": "ausgabe"}] konvertiert.
Aggregation:
Sonstige_Erträge = Σ(betrag) wo typ == "ertrag" (alle Komponenten + Anlage-Ebene)Sonstige_Ausgaben = Σ(betrag) wo typ == "ausgabe" (alle Komponenten + Anlage-Ebene)Sonstige_Netto = Erträge - AusgabenSichtbarkeits-/Doppelzählungs-Regel: Die Aggregation filtert nach
aktiv+ Laufzeit-Fenster (Anschaffung → Stilllegung) wie jede andere Position; der Caller übergibt das bereits gefiltertesonstige_nettoals Skalar an das Finanz-Aggregat. Basis-Positionen zählen genau einmal in die Totals; die T-Konto-Zeilen sind reiner Ausweis (kein zweiter Kostenposten).
Dienstliche Ladekosten:
Bei ist_dienstlich == true (E-Auto/Wallbox) werden Ladekosten als kalkulatorische Ausgaben verbucht:
Dienstlich_Ladekosten = Netz_kWh * Wallbox_Preis + PV_kWh * NetzbezugspreisWarum der PV-Anteil zum Netzbezugspreis zählt, nicht zur Einspeisevergütung (seit 2026-07-31). Die Formel stand bis dahin andersherum, und die beiden für sich plausiblen Halbschritte gingen zusammen nicht auf: Der Eigenverbrauch ändert sich durch das Dienstwagen-Flag nicht — energetisch ist die Ladung Eigenverbrauch hinter dem Zähler —, also schreibt die EV-Ersparnis (
Eigenverbrauch × Netzbezugspreis) die dienstlich geladenen kWh voll gut. Der Abzug zog dagegen nur die Einspeisevergütung ab. Netto blieben +22 ct je verschenkter kWh Gewinn stehen (bei 30/8 ct). Die entgangene Einspeisevergütung braucht gar keinen Buchungssatz: sie steckt bereits in der niedrigeren gemessenen Einspeisung. Was einen braucht, ist die zurückzunehmende EV-Gutschrift — und die steht zum Netzbezugspreis.Gemessen (PV 1.000 · Einspeisung 400 · Netzbezug 100 · 30/8 ct · 200 kWh PV in den Wagen):
Fall Eigenverbrauch Netto-Ertrag gar kein Auto (200 kWh eingespeist) 400 kWh 168,00 € Privatwagen 600 kWh 212,00 € Dienstwagen — bis 2026-07-31 600 kWh 196,00 € Dienstwagen — seither 600 kWh (unverändert) 152,00 € Die Energiebilanz bleibt unangetastet — Eigenverbrauchs-kWh, Eigenverbrauchsquote und Autarkie ändern sich durch diesen Posten nicht. Korrigiert wurde ausschließlich die Bewertung in Euro.
Netzanteil: Wallbox-Stromvertrag, wenn vorhanden, sonst Anlagentarif — jeweils der Monats-Flexpreis vor dem Stammdaten-Arbeitspreis (P8). Die Aussichten nahmen dafür bis 2026-07-31 den allgemeinen Arbeitspreis, das Cockpit den Wallbox-Preis; Kanon ist das Cockpit.
SoT:
core/berechnungen/dienstliche_ladekosten.py(ADR-001). Alle drei Sichten — Cockpit/Übersicht, Aussichten/Finanz-Prognose und der HA-Sensornetto_ertrag_euro— rufen ihn; der HA-Export zog die Kosten bis 2026-07-31 gar nicht ab und stand damit über der Kachel, auf die er sich bezieht.
4. Prognosen (Aussichten)
Abschnitt betitelt „4. Prognosen (Aussichten)“Prognose-Kanon — „PV-Tagesprognose heute” ist EIN Wert. Der „heute”-Wert (sowie Rest heute, morgen/übermorgen, Vor-/Nachmittag, Stundenprofil) wird seit dem Prognose-Kanon-Fix über einen Service (
services/prognose_kanon.py) gebildet und an alle Konsumenten geliefert: Live/Cockpit (live_wetter), die „eedc”-Spalte im Vergleich (api/routes/prognosen), die HA-/MQTT-Sensoren (ha_export_prognose) und den persistierten Tageswert (TagesZusammenfassung.pv_prognose_kwh). Rechenweg: Multi-String-Fan-out pro Orientierungsgruppe (pv_orientation.orientierungs_gruppen→ je einget_solar_prognose) → slot-weise Summe = rohes OpenMeteo-kWh-Profil → eedc-Korrektur pro Energie-Slot (core/berechnungen/prognose_korrektur.korrigiere_tagesprofil, Kaskadekorrekturprofil_lookup) mit InvarianteTageswert == Σ Export-Slots. Der Wert rollt mit OpenMeteo, aber überall synchron. Mathematik incore/berechnungen/(ADR-001), Orchestrierung im Kanon-Service. Symmetrie-Test:tests/test_prognose_kanon.py.Was seit v4.0.1 zusätzlich am Kanon hängt. Der 14-Tage-Balken in Cockpit → Aussicht samt 14-Tage-Tabelle und den Kacheln „Morgen”/„Summe”/„Ø_Tag”, die Zeilen Morgen/Übermorgen der Live-Solar-Aussicht sowie die Blöcke „Stunden-Prognose”/„Stundenwerte” lasen bis dahin die unkorrigierte OpenMeteo-Zahl bzw. gingen einen eigenen Ein-Abruf-Weg mit der Orientierung einer beliebigen PV-Zeile. Sie kommen jetzt aus derselben Rechnung wie der Prognosen-Vergleich und der Sensor
eedc_prognose_day_plus_1_kwh. (Ausgenommen: die Spalte „OpenMeteo” im Prognosen-Vergleich — sie ist der Rohwert und bleibt es.) Fehlt eine Korrektur, bleibt der Wetterdienst-Wert stehen und die Kopfzeile sagt es („Quelle: Open-Meteo (ohne Korrektur)”).Fällt der Kanon aus (kein OpenMeteo-Ergebnis, keine kWp, Zieltag jenseits des Abruf-Horizonts), springt ein Ersatz-Weg ein — der fächert seit v4.0.1 ebenfalls je Orientierungsgruppe auf statt die Gesamtleistung mit der Orientierung einer beliebigen PV-Zeile zu rechnen (ADR-002/P1: kein Anlagen-Kennwert aus EINER Investition). Liefert eine Gruppe nichts, trägt die Antwort den Hinweis auf die Teilsumme — nicht das Log (ADR-002/P4). Dasselbe gilt, wenn gar keine Prognose vorliegt: 24 Nullen werden als „keine Prognose” ausgewiesen, nicht als Prognose „0 kWh”.
Genauigkeits-Endwert (§6). Das Genauigkeits-Ranking vergleicht IST gegen
TagesZusammenfassung.pv_prognose_final_kwh(Fallbackpv_prognose_kwh): dieser rollt mit, bis OpenMeteo für den Tag nach Sonnenuntergang konvergiert ist (core/berechnungen/prognose_final.soll_final_einfrieren), und wird dann viapv_prognose_final_ateingefroren. Der Anzeige-Wert bleibt rollend (Drei-Größen-Modell: Anzeige rollend · Lern-Snapshot gefroren · Tracking-Endwert konvergenz-gefroren).
4.1 Kurzfrist-Prognose (7-16 Tage)
Abschnitt betitelt „4.1 Kurzfrist-Prognose (7-16 Tage)“Endpoint: GET /api/aussichten/kurzfristig/{anlage_id}
Datenquelle: Open-Meteo (konfigurierbar, siehe Wettermodell-Kaskade)
PV_Ertrag_Tag = GTI_kWh_m2 * Anlagenleistung_kWp * (1 - System_Losses) * Lernfaktor
Wenn Temperatur > 25°C: Temp_Verlust = (Temperatur - 25) * 0.004 PV_Ertrag_Tag *= (1 - Temp_Verlust)| Parameter | Quelle | Default |
|---|---|---|
System_Losses | PVGISPrognose.system_losses / 100 | 0.14 (14%) |
Anlagenleistung_kWp | Σ(PV-Module) + Σ(BKW), gelesen über den SoT-Dispatcher get_erzeuger_kwp (ADR-002/P3). Reihenfolge: Feld Leistung (kWp) der Investition → ersatzweise die Detail-Felder kwp bzw. leistung_kwp (nur Bestands-/Importdaten) → beim Balkonkraftwerk zusätzlich leistung_wp × anzahl (Anzahl fehlt ⇒ 1, nicht die Formular-Vorbelegung 2). Seit v4.0.2 gilt dieselbe Kette an allen Lesestellen der Nennleistung — Prognose, Cockpit, PV-Strings, ROI, CO₂, Live und PDF. | Anlage.leistung_kwp |
GTI_kWh_m2 | Global Tilted Irradiance aus Open-Meteo Solar (modul-projiziert mit Tilt + Azimut). Bei Multi-String-Anlagen werden parallele Calls pro Orientierungsgruppe abgesetzt und kWp-gewichtet kombiniert. | – |
GTI-Spalte der 14-Tage-Tabelle ist ein kWp-gewichtetes Mittel („GTI Modulfläche”): die Einstrahlung auf die Modulflächen dieser Anlage, konsistent zur Ertragssumme derselben Zeile (
Ertrag ≈ GTI × kWp × (1 − Verluste)ist linear in kWp). Bis v4.0.0 wurde ungewichtet gemittelt — ein 0,8-kWp-Balkonmodul zählte so viel wie ein 12-kWp-Süddach, und die Spalte passte zu keiner anderen Zahl ihrer Zeile. Anlagen mit nur einer Ausrichtung sind nicht betroffen. (Die Performance Ratio läuft über einen anderen Pfad — die Tages-GTI aus der Aggregation, dort schon immer kWp-gewichtet.) |Lernfaktor| Anlagenspezifischer Korrekturfaktor (siehe §4.1c) | 1.0 (vor 7 Tagen Daten) |
GTI vs. GHI: Bis v3.19.x rechnete eedc mit GHI (
shortwave_radiation, horizontal). Bei steilen Modulen und tiefstehender Wintersonne ist die Modul-projizierte GTI 2–3× höher — der GHI-basierte „theoretische Ertrag” lag im Winter systematisch zu niedrig (PR-Werte > 1 möglich). Seit v3.20.0 werden GTI-Werte für Prognose und Performance Ratio verwendet.
Multi-String / PV-Parameter-Quelle (v3.20.2/v3.20.3): kWp, Neigung und Azimut werden über den Helper
services/pv_orientation.pygelesen, der in dieser Reihenfolge prüft: Top-Level-Spalte der Investition →parameter.{neigung,ausrichtung}_grad(Zahl) →parameter.{neigung,ausrichtung}(Zahl oder String mit Mapping{"süd": 0, "ost": -90, "west": 90, ...}) → Default. Damit liefern alle drei Prognose-Pfade (Energieprofil-Tagesprognose, Aussichten-Kurzfrist, Prefetch-Cache) identische Eingabe-Parameter an Open-Meteo.
Wettermodell-Kaskade
Abschnitt betitelt „Wettermodell-Kaskade“Das verwendete Wettermodell ist pro Anlage konfigurierbar (Anlage.wettermodell):
| Wert | Modell | Auflösung | Einsatz |
|---|---|---|---|
auto | Bright Sky (DWD) für DE, sonst Open-Meteo best_match | variabel | Standard — maßgeblich ist seit #386 das gepflegte Land, nicht die Koordinaten-Box |
meteoswiss_icon_ch2 | MeteoSwiss ICON-CH2 | 2 km | Alpine Standorte CH/AT/IT |
icon_d2 | DWD ICON-D2 | 2,2 km | Deutschland (hochauflösend) |
icon_eu | DWD ICON-EU | ~7 km | Europa |
ecmwf_ifs04 | ECMWF IFS | 0,25° | Global |
Bei einem spezifischen Modell versucht eedc zuerst dieses Modell. Schlägt der Abruf fehl oder liefert es keine Daten für den Standort, fällt es auf best_match zurück (Kaskade). Die verwendete Quelle pro Tag wird im Response als datenquelle-Kürzel (MS/D2/EU/EC/BM) mitgeliefert.
Geltungsbereich (seit v4.0.2, A30): Die Modellwahl wirkt auf alle Prognose-Pfade, weil der Prognose-Kanon (services/prognose_kanon.py) Anlage.wetter_modell an get_solar_prognose durchreicht — also auch auf die eedc-korrigierte Tagesprognose, die Stundenprofile, die Live-/Persistenz-Werte und den HA-/MQTT-Export (services/ha_export_prognose.py). Bis v4.0.1 rechnete dieser Pfad unabhängig von der Einstellung mit best_match, während Live-Wetter, 14-Tage-Wettertabelle und die OpenMeteo-Spalte von /solar-prognose das Modell bereits nutzten — dieselbe Seite zeigte damit zwei Modelle nebeneinander.
Am Rand der Modell-Reichweite entscheidet die Abdeckung, nicht die Existenz (seit v4.0.19, F-36).
Jedes Modell hat eine Reichweite (Spalte oben, hinterlegt in WETTER_MODELLE), und am letzten
Tag innerhalb dieser Reichweite liefert Open-Meteo häufig nur noch die Stunden bis zum Ende des
Modelllaufs. Gemessen am 2026-08-18: icon_d2 (2 Tage) endete um 08:00 — die 15 Stunden
danach, also der gesamte Ertragszeitraum, trugen null. Die Kaskade holt für solche Fälle
ohnehin zusätzlich best_match, verwarf diesen Tag aber, sobald das gewählte Modell für ihn
irgendetwas geliefert hatte:
falsch: Primary gewinnt, wenn der Tag in seiner Antwort VORKOMMTrichtig: Primary gewinnt, wenn er den Tag mindestens so weit ABDECKT wie best_matchGezählt werden Stunden mit einem GTI-Wert, nicht mit Ertrag — nachts ist GTI 0.0 und damit
vorhanden; ein vollständiger Tag hat 24. Bei Gleichstand behält das gewählte Modell den
Vorrang, sonst nähme der Fix stillschweigend die Modellwahl weg. Deckt auch best_match den
Tag nur teilweise ab, bleibt er teilweise — die fehlenden Felder bleiben leer, statt zu 0 zu
werden (ADR-002/P4). SoT: solar_forecast_service._gti_abdeckung_je_tag und
_merge_nach_abdeckung; der Ein-Abruf-Zweig (tage ≤ Reichweite) holt best_match in diesem
Fall nach, sonst wäre die Regel dort blind.
⚠ Diese Lücke war seit dem 2026-07-28 bekannt und wurde zunächst nur umgangen. Die Snapshot-Begrenzung auf den Modellhorizont (
wetter/cache.snapshot_days, Auflage E15-a) nimmt der Kaskade die ganz leeren Tage jenseits der Reichweite — den angeschnittenen Tag am Rand lässt sie stehen, denn der liegt ja im Fenster. Die Begrenzung bleibt richtig (sie spart Abrufe und hält den Cache-Kanon), sie ist nur nicht das, was den Fehler verhindert.
„Keine Daten” schließt die leere Antwort ein. Open-Meteo kann für ein Modell mit HTTP 200 antworten und trotzdem für jede Stunde null liefern; _hat_nutzbares_gti behandelt das wie einen Fehlschlag, damit die best_match-Kaskade greift statt einen 0-kWh-Tag zu bauen. Am 2026-07-28 gemessen betrifft das drei der acht wählbaren Werte: ecmwf_ifs04 (HTTP 200, 0 von 72 Stundenwerten gesetzt — das Modell läuft nicht mehr, der Name wird noch akzeptiert) sowie ecmwf_seamless und meteoswiss_seamless (keine gültigen Modellnamen mehr, HTTP-Fehler). Für diese drei rechnet eedc faktisch mit best_match; die Bereinigung der Auswahlliste steht aus.
4.1b Solar Forecast ML (SFML)
Abschnitt betitelt „4.1b Solar Forecast ML (SFML)“Endpoint: GET /api/aussichten/kurzfristig/{anlage_id} (SFML-Werte im gleichen Response)
Service: services/solar_forecast_service.py
Externe API: forecast.solar oder solcast.com (konfigurierbar)
SFML ist eine optionale KI-basierte Prognose-Ergänzung. Sie liefert eine zweite Tages-Prognoselinie neben der eedc-Eigenprognose und den IST-Werten.
Datenfluss
Abschnitt betitelt „Datenfluss“1. Externer SFML-Anbieter liefert kWh-Prognose pro Tag2. Werte werden in DB persistiert (Tabelle: SolarForecastML)3. Endpoint gibt SFML-Werte zusammen mit eedc-Prognose zurückResponse-Felder (pro Tag)
Abschnitt betitelt „Response-Felder (pro Tag)“{ "datum": "2026-03-28", "eedc_prognose_kwh": 12.4, "sfml_prognose_kwh": 11.8, "ist_kwh": 13.1, // null wenn Zukunft "datenquelle": "MS" // Wettermodell-Kürzel}Abweichungsberechnung (Prognose-Vergleich)
Abschnitt betitelt „Abweichungsberechnung (Prognose-Vergleich)“EEDC_Abweichung (%) = (IST - EEDC_Prognose) / EEDC_Prognose * 100SFML_Abweichung (%) = (IST - SFML_Prognose) / SFML_Prognose * 100Beide Abweichungen werden im Frontend als farbige Badges angezeigt (grün = Übererfüllung, rot = Untererfüllung).
4.1c Prognose-Vergleich (Auswertungen → Prognose)
Abschnitt betitelt „4.1c Prognose-Vergleich (Auswertungen → Prognose)“Anzeige: Auswertungen → Prognose (Genauigkeit gegen IST) — fachlich beschrieben im Handbuch Prognosen.
Endpoint: GET /api/aussichten/prognosen/{anlage_id}
Service: api/routes/prognosen.py (in v3.16.6 aus aussichten.py ausgelagert), services/solcast_service.py
Der Prognosen-Tab vergleicht vier Quellen pro Tag/Stunde:
| Quelle | Bedeutung |
|---|---|
| OpenMeteo (OM) | Wetterbasierte Roh-Prognose aus GTI × kWp × (1 − System_Losses). Auch die Stundenkurve „OpenMeteo (roh)” fächert seit v4.0.1 je Orientierungsgruppe auf — vorher rechnete sie die Gesamtleistung so, als hinge sie an einer Dachfläche (Neigung/Ausrichtung der zufällig ersten PV-Zeile, sonst stillschweigend 35° Süd), während der OM-Tageswert derselben Spalte längst auffächerte. Kurve, Summenzeile und Tageswert sind jetzt deckungsgleich. |
| eedc (kalibriert) | die anlagenspezifisch korrigierte Prognose. Legende und Beschriftung sagen seit v4.0.1 „eedc (kalibriert)” statt „eedc (OpenMeteo × Faktor)” — korrigiert wird pro Stunden-Slot auf der Energie, nicht mit einem Tagesfaktor. |
| Solcast | Optionale dritte Quelle, entweder Solcast-API (Free/Paid Key) oder HA-Sensor (BJReplay-Integration). 30-Min-Buckets werden per ceil(bucket_ende) dem Backward-Slot zugeordnet. |
| IST | Tatsächlich gemessener Tageswert aus den Stunden-Snapshots (siehe §6b) |
Abweichung und Σ-Zeile im Stundenvergleich (ab v4.0.6)
Abschnitt betitelt „Abweichung und Σ-Zeile im Stundenvergleich (ab v4.0.6)“Die Tabelle „Stundenvergleich heute” annotiert jeden Prognosewert mit seiner Abweichung zum IST derselben Stunde. Zwei Regeln, beide client-seitig in components/prognose/PrognoseVergleichTeile.tsx (DevBadge bzw. stundenSummeVon), gepinnt in PrognoseVergleichTeile.stundenvergleich.test.tsx:
Δ_Stunde = Prognose_kWh − IST_kWh (angezeigt: |Δ|, Richtung als ▲/▼, „±" wenn |Δ| < 0,05)Δ_relativ = |Δ| / IST × 100 (nur für die Farbskala bzw. die Σ-Zeile; IST > 0,05 vorausgesetzt)- Liegt ein IST vor, wird immer annotiert — auch bei Δ = 0. Eine fehlende Annotation bedeutet damit eindeutig „keine Messung”, nicht „kleine Abweichung”. (Bis v4.0.5 unterdrückte die Anzeige jedes |Δ| < 0,03 kWh; das traf je Spalte unterschiedlich zu und sah in der Zeile aus wie eine Datenlücke — PN Rainer 90004.)
- Die Σ-Zeile summiert über ein gemeinsames Fenster. Obergrenze ist die letzte Stunde mit IST, höchstens
aktuelle_stunde(Slotaktuelle_stunde + 1läuft noch, s. Backward-Konvention). Stunden ohne IST bleiben in allen vier Spalten außen vor — die vier Summen meinen damit paarweise dieselben Stunden. (Bis v4.0.5 stand dort die 24-Stunden-Prognosesumme nebenist_heute_kwh: mittags z. B. 78,1 gegen 26,1 — die Abweichung maß die Tageszeit.) - Ohne jedes IST (Zukunftstag) zeigt die Σ-Zeile die volle Prognosesumme und kein Δ — ADR-002/P4: kein 0-%-Ergebnis auf einer nicht vorhandenen Referenz. Der Vollständigkeits-Grenzfall „alle 24 Stunden gemessen” verhält sich wie vorher (keine
bis HH:00-Kennzeichnung).
Nicht zu verwechseln mit der Tagesprognose derselben Spalte (openmeteo_heute_kwh etc.) in der Kennzahl-Matrix — die bleibt der ganze Tag, ebenso verbleibend_* (= IST bisher + Σ Prognose-Slots der Reststunden).
Lernfaktor (saisonale MOS-Kaskade, ab v3.16.15)
Abschnitt betitelt „Lernfaktor (saisonale MOS-Kaskade, ab v3.16.15)“Die eedc-Prognose ist die korrigierte OpenMeteo-Prognose; der Skalar-Lernfaktor unten ist die
gröbste Stufe der Korrektur-Kaskade (feiner: Sonnenstand × Wetter je Stunden-Slot, siehe
Prognosen §5.3). Der Lernfaktor wird aus historischen (Prognose, IST)-Tag-Paaren berechnet — nur Tage mit gültiger OpenMeteo-Prognose UND IST-Ertrag > 0.5 kWh fließen ein (Schlechtwetter-Tage mit ~0 kWh würden den Faktor sonst verzerren).
faktor = Σ(IST_kWh) / Σ(EEDC_Roh_Prognose_kWh)Seit v3.16.15 nutzt eedc eine saisonale Kaskade mit den jeweils vorhandenen Daten:
| Stufe | Bedingung | Bezugszeitraum |
|---|---|---|
| Monatsfaktor | ≥ 15 gültige Tage im selben Kalendermonat | Tage des Kalendermonats über alle Jahre |
| Quartalsfaktor | ≥ 15 gültige Tage im selben Quartal | Tage des Quartals über alle Jahre |
| 30-Tage-Fenster | ≥ 7 gültige Tage | Letzte 30 Kalendertage |
| Inaktiv | < 7 Tage | Lernfaktor = 1.0, eedc-Spalte gedämpft mit — und Tooltip-Verweis |
Die aktive Stufe wird im Status-Banner und im KPI-Card-Header angezeigt.
Restzeit-Banner (v3.22.0): Wenn die 7-Tage-Schwelle noch nicht erreicht ist, zeigt das Banner: „X von 7 Tagen, noch Y Tage” — Y berücksichtigt nur Tage mit gültiger Prognose UND IST > 0.5 kWh, also dieselbe Filterregel wie der Faktor selbst.
Persistierung: Lernfaktor pro Quelle separat gecacht. Backfill-Kandidat-Felder (pv_prognose_kwh, Solcast-Tageswerte) werden seit v3.16.14 alle 45 Min automatisch aus dem Prefetch-Job in TagesZusammenfassung geschrieben — vorher hing die Persistierung als Nebeneffekt am Dashboard-Besuch und der Lernfaktor konnte ohne Nutzer-Interaktion nicht berechnet werden.
Genauigkeits-Tracking: MAE + MBE getrennt (v3.22.0, #151)
Abschnitt betitelt „Genauigkeits-Tracking: MAE + MBE getrennt (v3.22.0, #151)“Über alle Tage mit gleichzeitig verfügbarer Prognose und IST werden zwei Kennzahlen pro Quelle (OM, eedc, Solcast) berechnet, auf vorzeichenbehafteten relativen Fehlern:
err_rel(tag) = (Prognose_kWh - IST_kWh) / IST_kWh
MAE = Ø |err_rel| # Mean Absolute Error — StreuungMBE = Ø err_rel # Mean Bias Error — systematischer Bias| Kennzahl | Aussage |
|---|---|
| MAE | Wie weit liegen Prognose und IST im Schnitt auseinander, unabhängig von der Richtung? Maß für Streuung/Schwankungsbreite. |
| MBE | Liegt die Quelle im Mittel über (positiv) oder unter (negativ) dem IST? Bias ist neutral gefärbt — Vorzeichen ist Information, keine Wertung. |
Asymmetrie-Diagnostik (v3.23.3, #151 Variante B)
Abschnitt betitelt „Asymmetrie-Diagnostik (v3.23.3, #151 Variante B)“MAE/MBE bleiben blind für Asymmetrie: eine Quelle, die in 50 % der Tage 30 % zu hoch und in 50 % der Tage 30 % zu niedrig liegt, hat MAE = 30 % und MBE ≈ 0 % — sie sieht „im Mittel ausgewogen” aus, ist aber nicht mit einem einzigen Lernfaktor korrigierbar. Im Diagnostisch-Modus splittet das Backend die signed errors an 0:
darüber: nur Tage mit err_rel > 0 over_count = Anzahl over_avg_prozent = Ø err_rel * 100
darunter: nur Tage mit err_rel ≤ 0 under_count = Anzahl under_avg_prozent = Ø |err_rel| * 100Response-Schema AsymmetrieEintrag mit Feldern over_count, over_avg_prozent, under_count, under_avg_prozent — pro Quelle als openmeteo_asymmetrie / eedc_asymmetrie / solcast_asymmetrie zurückgegeben.
VM/NM-Split an Solar Noon (v3.22.0)
Abschnitt betitelt „VM/NM-Split an Solar Noon (v3.22.0)“Tageshälften (Vormittag/Nachmittag) werden nicht hart bei 12:00 Uhr Clockzeit gesplittet, sondern an der astronomischen Tagesmitte (Solar Noon, via Equation of Time + Standortlängengrad). Die Abweichung von 12:00 kann je nach Standort und Datum bis ~30 min betragen. Slots, die Solar Noon enthalten, werden proportional auf VM und NM verteilt — konsistent zum solar_forecast_service.
IST-Slot-Behandlung
Abschnitt betitelt „IST-Slot-Behandlung“- Backward-Slot-Konvention (siehe §6b): Slot N enthält Energie aus dem Intervall
[N-1, N). - Gerade abgeschlossene Stunde (v3.23.0): wird nicht als Lücke geflaggt — HA Long-Term Statistics schreibt die Stunden-Row erst am Ende der Stunde, das Zeitfenster zwischen Stundenwechsel und HA-Stats-Write (typisch ~5–60 Min) wird mit
<(statt<=) toleriert. - Echte Lücken (>1 h alt) werden mit ⚠ markiert. Klick auf das Symbol öffnet einen Reparatur-Popover mit „Tag neu berechnen” (
POST /api/energie-profil/{anlage_id}/reaggregate-tag) und einem Fallback-Link zur Datenquellen-Zuordnung.
4.2 Langfrist-Prognose (12 Monate)
Abschnitt betitelt „4.2 Langfrist-Prognose (12 Monate)“Endpoint: GET /api/aussichten/langfristig/{anlage_id}
PVGIS_kWh = PVGISPrognose.monatswerte[monat].e_m (Fallback: TMY * kWp * 0.85)
Monat_PR = Ø(IST / SOLL) für diesen Monat aus historischen DatenGesamt_PR = Ø(alle monatlichen Performance Ratios)
Trend_kWh = PVGIS_kWh * Monat_PRKonfidenz_Min = Trend_kWh * 0.85 (15% Band)Konfidenz_Max = Trend_kWh * 1.15
Trend-Richtung: > 1.05 → "positiv" < 0.95 → "negativ" sonst → "stabil"4.3 Trend-Analyse & Degradation
Abschnitt betitelt „4.3 Trend-Analyse & Degradation“Endpoint: GET /api/aussichten/trend/{anlage_id}
Degradation (2 Strategien)
Abschnitt betitelt „Degradation (2 Strategien)“Primär: Vollständige Jahre (12 Monate)
Wenn >= 2 vollständige Jahre vorhanden: Änderung = (Letztes_Jahr_kWh - Erstes_Jahr_kWh) / Erstes_Jahr_kWh * 100 Degradation = Änderung / Anzahl_JahreFallback: TMY-Ergänzung (>= 6 Monate pro Jahr)
Wenn >= 2 Jahre mit jeweils >= 6 Monaten Daten: 1. Performance-Ratio aus vorhandenen Monaten berechnen 2. Fehlende Monate mit TMY * PR ergänzen 3. Degradation aus ergänzten Jahreswerten ableiten4.4 Finanz-Prognose & Amortisation
Abschnitt betitelt „4.4 Finanz-Prognose & Amortisation“Endpoint: GET /api/aussichten/finanzen/{anlage_id}
Bisherige Erträge (historisch)
Abschnitt betitelt „Bisherige Erträge (historisch)“Bisherige_Erträge = Σ(Einspeisung * Vergütung / 100) (PV) + Σ(Eigenverbrauch * Netzbezug_Preis / 100) (EV) + WP_Ersparnis (vs. Gas) + E-Auto_Ersparnis (vs. Benzin) + BKW_Ersparnis (Eigenverbrauch) + Sonstige_Netto (alle Investitionstypen)WP-Ersparnis (historisch, in Finanzen)
Abschnitt betitelt „WP-Ersparnis (historisch, in Finanzen)“Für jeden Monat mit WP-Daten: Gas_Preis = Monatsdaten.gaspreis_cent_kwh # ab v3.21.0, wenn pro Monat gepflegt ∨ Investition.parameter.alter_preis_cent_kwh # Fallback statisch Gas_Kosten = (Heizung + WW) / 0.9 * Gas_Preis / 100 + alternativ_zusatzkosten_jahr / 12 # Zusatzkosten anteilig pro Monat WP_Netzkosten = Strom * 0.5 * WP_Preis / 100 # 50% Netzanteil-Annahme Ersparnis = Gas_Kosten - WP_NetzkostenMonats-Gaspreis (v3.21.0): Wenn
Monatsdaten.gaspreis_cent_kwhpro Monat gepflegt ist, wird er Monat für Monat verwendet — ein Tarifwechsel ändert dann nicht mehr rückwirkend die ganze Historie. Ohne Eintrag bleibt es beim statischenalter_preis_cent_kwhder Investition. Pflege in der assistiertenMonatsdatenForm(überBEDINGTE_BASIS_FELDERmitbedingung_basis: hat_waermepumpe) — in V4 der EINE Erfassungsweg; der frühere Monatsabschluss-Wizard ist als V4-Fläche stillgelegt und läuft nur noch über die V3-Route (bis zum Flip).
E-Auto-Ersparnis (historisch, in Finanzen)
Abschnitt betitelt „E-Auto-Ersparnis (historisch, in Finanzen)“Benzin_Liter = Σ(km) / 100 * Vergleich_L_100kmBenzin_Kosten = Benzin_Liter * BenzinpreisNetzstrom_Kosten = Σ(ladung_netz_kwh) * Strompreis / 100Ersparnis = Benzin_Kosten - Netzstrom_KostenMonatsprognose (zukünftig)
Abschnitt betitelt „Monatsprognose (zukünftig)“Für jeden Prognosemonat:
PV_kWh = PVGIS_Monatswert (oder TMY * kWp * 0.85)Basis_EV = PV_kWh * Basis_EV_Quote (historisch ermittelt, 15-70%)Speicher_Beitrag = Ø_Speicher_Entladung * PV_FaktorV2H_Beitrag = Ø_V2H_Entladung (konstant)WP_PV_Anteil = WP_Strom * 0.5 * sqrt(PV_Faktor)
Eigenverbrauch = min(Basis_EV + Speicher + V2H + WP_PV, PV_kWh)Einspeisung = PV_kWh - EigenverbrauchNetto_Ertrag = Einspeisung * Vergütung/100 + Eigenverbrauch * Preis/100WP saisonal gewichtet:
WP_SAISON_FAKTOREN = {Jan: 1.8, Feb: 1.6, Mär: 1.3, Apr: 0.8, Mai: 0.4, Jun: 0.2, Jul: 0.2, Aug: 0.2, Sep: 0.4, Okt: 0.8, Nov: 1.3, Dez: 1.7}WP_Strom_Monat = WP_Strom_Durchschnitt * Saison_FaktorAmortisation
Abschnitt betitelt „Amortisation“SoT: core/berechnungen/amortisation.py::berechne_amortisations_fortschritt
(Berechnungs-Layer, ADR-001) — dieselbe Formel speist die Kachel
„Amortisations-Fortschritt” in Auswertungen → ROI.
Investition = Σ max(0, Anschaffung − Alternative) # relevante Kosten, §3.6
Jahres_Netto_Ertrag = PV_Einspeise_Erlös + EV_Ersparnis + WP_Ersparnis + E-Auto_Ersparnis + BKW_Ersparnis + Sonstige_Netto - Betriebskosten_Jahr [- USt_Eigenverbrauch]
ROI_Fortschritt (%) = Bisherige_Erträge / Investition * 100Amortisation_erreicht = Bisherige_Erträge >= Investition
Wenn nicht amortisiert: Rest_Betrag = Investition - Bisherige_Erträge Monate_bis_Amort = Rest_Betrag / (Jahres_Netto_Ertrag / 12) Prognose_Jahr = Heute + Monate_bis_AmortDer Fortschritt ist reine Messung —
Jahres_Netto_Ertraggeht ausschließlich in die Restlaufzeit ein. Ist er ≤ 0 (Anlaufjahr), gibt es keine Prognose statt einer geratenen; sind die relevanten Kosten 0, gibt es nichts zu amortisieren (0 %, nicht „fertig”). Der Fortschritt ist nicht bei 100 % gedeckelt: eine Anlage, die sich doppelt bezahlt gemacht hat, darf das sagen.
5. Tarif-System (Spezialtarife)
Abschnitt betitelt „5. Tarif-System (Spezialtarife)“Funktion: lade_tarife_fuer_anlage() in strompreise.py
Funktionsweise
Abschnitt betitelt „Funktionsweise“- Alle gültigen Tarife laden (
gueltig_ab <= heuteUND (gueltig_bis IS NULLODERgueltig_bis >= heute)) - Nach
verwendunggruppieren (neuester zuerst) - Fallback-Kette:
waermepumpe → waermepumpe-Tarif || allgemeinwallbox → wallbox-Tarif || allgemeinallgemein → allgemein-Tarif || Hardcoded Defaults (30.0 / 8.2)Verwendung in Berechnungen
Abschnitt betitelt „Verwendung in Berechnungen“| Komponente | Tarif-Key | Preis-Feld |
|---|---|---|
| PV Einspeisung/EV | allgemein | einspeiseverguetung_cent_kwh, netzbezug_arbeitspreis_cent_kwh |
| Wärmepumpe Strom | waermepumpe | netzbezug_arbeitspreis_cent_kwh |
| Wallbox/E-Auto Ladung | wallbox | netzbezug_arbeitspreis_cent_kwh |
| Grundpreis | allgemein | grundpreis_euro_monat |
Hardcoded Defaults (wenn kein Tarif)
Abschnitt betitelt „Hardcoded Defaults (wenn kein Tarif)“Netzbezug_Preis = 30.0 ct/kWhEinspeisevergütung = 8.2 ct/kWhGrundpreis = 0 EUR/MonatDynamischer Tarif / Monatlicher Ø-Strompreis
Abschnitt betitelt „Dynamischer Tarif / Monatlicher Ø-Strompreis“Für Nutzer mit dynamischem Stromtarif (z.B. Tibber, aWATTar) kann der tatsächliche monatliche Durchschnittspreis verwendet werden statt des festen Tarifpreises.
Fallback-Kette für netzbezug_preis_cent:
1. Monatsdaten.netzbezug_durchschnittspreis_cent (manuell pro Monat)2. HA-Sensor strompreis (via Datenquellen-Zuordnung) (automatisch aus HA)3. Strompreis.netzbezug_arbeitspreis_cent_kwh (fester Tarif)4. Hardcoded Default: 30.0 ct/kWhKonfiguration:
- In der Datenquellen-Zuordnung kann ein HA-Sensor (oder MQTT-Topic) für das Feld
strompreiszugeordnet werden - Im Monatsdaten-Formular wird der Ø-Preis als Vorschlag angezeigt und ist dort manuell editierbar
Der Jahres-Ø ist mengengewichtet. Die Jahres-/Gesamt-Kachel „Ø-Preis Netz” (und ebenso die Ø-Einspeisevergütung) rechnet
Σ(Preis_Monat × Menge_Monat) / Σ Menge_Monat— nicht das arithmetische Mittel der Monatspreise. Gewichtet wird der effektive Monatspreis (also der Ø-Bezugspreis vor dem Tarif-Arbeitspreis, dieselbe Kette wie oben); sonst fiele ein Jahr mit dynamischem Tarif auf den Referenzpreis zurück, obwohl die Kosten darunter mit dem Stundenpreis gerechnet sind. Monate ohne Menge fallen aus Zähler und Nenner; gibt es im Zeitraum überhaupt keine Menge, bleibt das arithmetische Mittel als Rückfall stehen, statt die Kachel zu leeren. Ohne die Gewichtung passte der Kopfwert nicht zu den kWh und Euro darunter — ein teurer Winter- und ein billiger Sommermonat wogen gleich viel (Forum simon42 #89667/67).
6. Investitionstyp-spezifische Berechnungen (ROI-Dashboard)
Abschnitt betitelt „6. Investitionstyp-spezifische Berechnungen (ROI-Dashboard)“Endpoint: GET /api/investitionen/roi/{anlage_id}
PV-System-Gruppierung (3-Pass)
Abschnitt betitelt „PV-System-Gruppierung (3-Pass)“Pass 1: Alle Wechselrichter identifizieren → pv_systeme[wr_id]Pass 2: PV-Module via parent_investition_id zuordnen, DC-Speicher via parent_investition_id zuordnenPass 3: Verbleibende Investitionen → standalone
PV-Einsparung wird proportional nach kWp auf Module verteilt: Modul_Einsparung = Gesamt_PV_Einsparung * (Modul_kWp / Gesamt_kWp)Hochrechnung bei unvollständigen Jahren
Abschnitt betitelt „Hochrechnung bei unvollständigen Jahren“Wenn weniger als 12 Monate Daten: 1. Versuche PVGIS-gewichtete Hochrechnung: Faktor = PVGIS_Jahressumme / PVGIS_Summe_vorhandene_Monate 2. Fallback: Lineare Hochrechnung: Faktor = 12 / Anzahl_Monate6b. Energieprofil-Berechnungen (Tages-Aggregation)
Abschnitt betitelt „6b. Energieprofil-Berechnungen (Tages-Aggregation)“Service: services/energie_profil_service.py, services/sensor_snapshot_service.py
Trigger: Scheduler stündlich :05 (Snapshot) und :55 (Live-Preview), täglich 00:15 (Vortag-Aggregation) + Monatsabschluss (Backfill + Rollup)
Snapshot-basierte Architektur (ab v3.19.0, #135)
Abschnitt betitelt „Snapshot-basierte Architektur (ab v3.19.0, #135)“Stunden-kWh werden nicht mehr aus 10-Min-Leistungs-Samples integriert (±5–15 % Drift), sondern als Differenz kumulativer Zähler-Snapshots berechnet — analog zum HA Energy Dashboard.
1. Stündlicher Snapshot-Job (Cron :05) schreibt pro Anlage und gemapptem kWh-Sensor den aktuellen Zählerstand in die Tabelle `sensor_snapshots`. Quellen: HA Long-Term Statistics (Add-on) oder MQTT-Energy-Snapshots (Standalone/Docker).2. :55-Live-Preview (v3.21.0) schreibt zum Stundenende einen Zählerstand für die anstehende volle Stunde — laufende Stunde sofort sichtbar statt erst um (h+1):05.3. Tagesaggregation (00:15) bildet Differenzen: kWh[h] = snap[h] - snap[h-1] für h = 0..23 (Snapshot-Range -1..23, damit Slot 0 aus Vortag-23:00 fließt).Snapshot-Lücken-Interpolation (v3.20.0, #145):
Wenn ein Snapshot fehlt (Scheduler-Ausfall, HA-Statistics-Timeout, MQTT-Cache leer), interpoliert eedc linear zwischen den vorhandenen Nachbar-Stunden:
Beispiel: snap[10] = 1500 kWh, snap[11] = None, snap[12] = 1505 kWh → interpoliert: snap[11] = 1502.5 kWh → kWh[11] = 2.5, kWh[12] = 2.5 (statt fälschlich kWh[11]=0, kWh[12]=5 als Spike)Ränder (h0 fehlend am Tagesanfang, h24 am Tagesende) werden nicht extrapoliert — der Wert bleibt None und die betroffene Stunde fällt aus der Delta-Bildung. Tagessumme bleibt in jedem Fall korrekt (snap[24] − snap[0]).
HA-Statistics-Toleranz (v3.20.0, #145): Reduziert von 120 min auf 10 min. Wenn die Zielstunde in HA-Statistics noch nicht vorhanden ist, schreibt der Job nichts (statt einen Nachbar-Wert zu liefern, der Slot N als 0 und Slot N+1 als 2-Stunden-Delta entstehen ließ). Der nächste aggregate_day-Lauf 15 Min später holt den Wert via Self-Healing nach.
Restart-Recovery (v3.23.0): Beim Scheduler-Start läuft sensor_snapshot_startup_recovery() im Hintergrund — holt für die letzten 6 Stunden je Anlage HA-Statistics-Snapshots (idempotent dank Upsert) plus für die laufende Stunde einen Live-Snapshot, anschließend aggregate_today_all.
Tagesreset-Heuristik (v3.23.0): HA-utility_meter-Sensoren mit täglichem Reset werfen um Mitternacht ein stark negatives Delta. Erkannt am Muster s1 < 0.5 ∧ s0 > 0.5, eedc nimmt dann max(0, s1) als Slot-0-Wert (Energie seit Reset, typ. ≈ 0 nachts). Bei untypischen negativen Deltas mitten am Tag bleibt die Reset-Warnung wie bisher.
Phase D Cleanup (v3.21.0, #138): Seit v3.21.0 ist der Zähler-Snapshot-Pfad die einzige kWh-Quelle. Der frühere W-Integration-Fallback (_val()-Helper, else-Branch in backfill_from_statistics) und das Feature-Flag EEDC_ENERGIEPROFIL_QUELLE sind entfernt. Auf Anlagen ohne kumulative Zähler erscheinen Stunden-kWh-Felder als NULL statt geschätzter Werte.
Backward-Slot-Konvention (ab v3.20.0, #144)
Abschnitt betitelt „Backward-Slot-Konvention (ab v3.20.0, #144)“Alle Stunden-Slots im Energieprofil und in den Prognose-Quellen folgen seit v3.20.0 der Backward-Konvention:
| Konvention | Slot N enthält Energie aus … |
|---|---|
| Backward (eedc, ab v3.20.0) | [N-1, N) — „die letzte Stunde”. Slot 0 = Energie 23:00–24:00 des Vortags. |
| Forward (Strompreis, weiterhin) | [N, N+1) — „gilt ab jetzt”. Industrieüblich für aWATTar/Tibber/EPEX. |
Industriestandard für Energie: HA Energy Dashboard, SolarEdge, SMA, Fronius, Tibber.
Migration auf Backward (v3.20.0):
sensor_snapshot_service.get_hourly_kwh_by_category: Deltasnap[h] − snap[h-1]→ Slot h (vorher:snap[h+1] − snap[h]→ Slot h)solcast_service(API + HA-Sensor): 30-Min-Buckets perceil(bucket_ende)→ richtigen Backward-Slot. Ein Bucket am Tagesübergang[23:00, 23:30)heute landet damit korrekt in Slot 0 des Folgetags, nicht in Slot 0 von heute.- Nach Update auf v3.20.0 nötig: einmal „Verlauf nachberechnen + überschreiben” auslösen, damit alle historischen Stundenwerte umverteilt werden. Tagessummen und alle abgeleiteten Kennzahlen (Autarkie, PR, Lernfaktor) sind konventionsunabhängig korrekt.
Die Konvention endet nicht am Backend (v4.0.6). Wer eine Stunde beschriftet oder einen Messwert
in eine Chart-Spalte einsortiert, folgt derselben Regel — Client-SoT ist
frontend/src/lib/stundenSlot.ts (Spiegel von core/berechnungen/slot_konvention.py, Regressionstest
stundenSlot.test.ts):
| Funktion | Wofür |
|---|---|
slotZeitspanne(h) | Beschriftung — Slot 11 → „10:00–11:00 Uhr”. Jeder Tooltip einer Stundengrafik. |
slotAusIntervallStart(h) | Messreihen mit Slot-Beginn-Stempel (10-Min-Punkte des Tagesverlaufs) → Slot h+1. Rückgabe 24 = Slot 0 des Folgetags, kein Modulo. |
slotAusZeitpunkt("HH:MM") | Zeitpunkt-Marker (Sonnenaufgang, Solar Noon) in den Slot, der ihn enthält — 05:56 → Slot 6. |
Auslöser war Rainer (PN 90106, 01.08.2026): der Live-Block „Wetter heute” baute seine Zeitspanne selbst
und vorwärts ([h, h+1)), während die Prognose-Sicht rückwärts beschriftete; zusätzlich bündelte er
die 10-Minuten-Punkte der Stunde h in Spalte h statt h+1. Gemessen gegen die Live-Box lag die
IST-Kurve dadurch genau eine Spalte neben der Prognose im selben Chart. Klasse: nicht das
Beschriftungs-Symptom patchen, sondern die Stelle, die die Zuordnung konstruiert
(feedback_aggregations_drift).
Auch die Verbrauchs-Seite folgt ihr (v4.0.6). Die gestrichelte Verbrauchs-Prognose im Live-Chart
kommt aus dem individuellen Verbrauchsprofil der letzten 7 vollen Tage
(services/live_verbrauchsprofil_service.py), und das hat drei Quellen mit Vorrang in dieser
Reihenfolge:
| Quelle | Wann sie greift | Slot-Herkunft |
|---|---|---|
EEDC-DB (TagesEnergieProfil.stunde) | sobald der Scheduler mindestens zwei Tage aggregiert hat | schon backward (Aggregator schreibt über lts_boundary_index) |
| HA-History (Leistungsmittel je Stunde) | frische Installation, noch keine DB-Historie | _slot_fenster → backward_slot_aus_period_start |
| MQTT-Snapshots (Zähler-Delta je Stunde) | Standalone-Betrieb ohne HA | dieselbe Stelle |
Bis v4.0.5 bündelten die beiden Fallback-Quellen forward: die Energie aus [h, h+1) landete unter
Index h, das Profil lag also eine Stunde zu früh — unsichtbar für jeden, der schon DB-Historie hat,
sichtbar bei Neuinstallation und im Standalone-Betrieb. Seit v4.0.6 ordnet eine Stelle im
Modul zu (_slot_fenster), und zwar über den Backend-SoT core/berechnungen/slot_konvention.py;
das Abfrage-Fenster beginnt dafür eine Stunde vor Mitternacht, weil Slot 0 des ersten Tages das
Intervall [Vortag 23:00, 00:00) trägt. Gepinnt in
tests/test_verbrauchsprofil_slot_konvention.py — je eine Regression pro Fallback-Pfad plus ein
Symmetrie-Test „gleiche Wirklichkeit, drei Messarten ⇒ ein Profil”
(feedback_aggregator_symmetrie).
Wann eine Stunde als unvollständig gilt (v4.0.6)
Abschnitt betitelt „Wann eine Stunde als unvollständig gilt (v4.0.6)“Die Zuordnung allein genügt nicht — die drei Quellen müssen sich auch einig sein, wann eine Stunde überhaupt gemessen wurde. Eine unvollständige Stunde liefert in allen dreien keine Stichprobe; sie wird ausgelassen, nicht geschätzt und nicht als 0 kW gezählt:
| Quelle | Stunde gilt als gemessen, wenn … | sonst |
|---|---|---|
| EEDC-DB | eine TagesEnergieProfil-Zeile mit verbrauch_kw IS NOT NULL existiert | keine Zeile ⇒ keine Stichprobe (galt schon immer) |
| HA-History | mindestens ein Netz-Sensor (Bezug, Einspeisung oder Kombi) in dieser Stunde einen Messpunkt hat | Stunde wird übersprungen |
| MQTT-Snapshots | für jeden gelieferten Zähler an beiden Intervallgrenzen ein Stand vorliegt (höchstens 6 Minuten alt) | Stunde wird übersprungen |
Zu den beiden Fallbacks im Einzelnen:
- MQTT misst über die Intervallgrenzen, nicht über die zufällig in der Stunde liegenden Snapshots. Bis v4.0.5 war das Stundendelta „letzter minus erster Snapshot innerhalb der Stunde”; das letzte Snapshot-Intervall fiel damit jede Stunde heraus — bei 5-Minuten-Takt rund 8 % zu wenig, und ein Verbrauch, der erst gegen Ende der Stunde anfiel, ging ganz verloren. Der Randwert ist der letzte Zählerstand bei oder vor der Grenze; benachbarte Stunden lesen denselben Wert, deshalb geht an der Grenze nichts verloren und nichts wird doppelt gezählt. Der Scheduler schreibt alle 5 Minuten, aber nicht auf die volle Stunde gerastert — daher die 6 Minuten Toleranz (Reserve für Verzug, aber nicht genug für einen ausgefallenen Snapshot).
- HA zählt eine Stunde ohne Historie nicht mehr als gemessene Null. Bis v4.0.5 hängte jede Stunde
eine Stichprobe an, auch wenn der Recorder nichts geliefert hatte: aus unbekannt wurde war
nichts, und ein einziger Ausfalltag drückte jeden Werktags-Slot auf 4/5 des wahren Werts. Beleg ist
bewusst der Netzanschluss und dort
anystattall: PV- und WP-Sensoren melden nachts bzw. im Stillstand stundenlang keine Zustandsänderung, ohne dass Daten fehlen, und von Bezug und Einspeisung bewegt sich immer genau einer. Liefert im ganzen Fenster kein Netz-Sensor, gibt es kein HA-Profil — sonst stünde die reine PV-Kurve als vermeintlicher Verbrauch da.
Ein Slot ohne jede Stichprobe fehlt im Ergebnis-Dict. Der Konsument
(api/routes/live_wetter.py::_berechne_verbrauchsprofil) erkennt das und setzt seine
Standard-Grundlast ein, statt still 0 kW anzunehmen — die lokale Ausprägung von
ADR-002/P4.
Stündliche Berechnung (aggregate_day)
Abschnitt betitelt „Stündliche Berechnung (aggregate_day)“PV_kWh = snap_pv[h] - snap_pv[h-1] # für jede gemappte PV-InvestitionEinspeisung_kWh = snap_einspeisung[h] - snap_einspeisung[h-1]Netzbezug_kWh = snap_netzbezug[h] - snap_netzbezug[h-1]Bat_Ladung_kWh = snap_ladung[h] - snap_ladung[h-1]Bat_Entladung_kWh = snap_entladung[h] - snap_entladung[h-1]
Verbrauch_kWh = PV + Netzbezug + Bat_Entladung - Einspeisung - Bat_LadungÜberschuss_kWh = max(0, PV - Verbrauch_kWh - Bat_Ladung)Defizit_kWh = max(0, Verbrauch_kWh + Bat_Ladung - PV)Strikte NULL-Semantik: Wenn ein Zähler nicht gemappt ist, bleibt das zugehörige Feld NULL (statt aus Leistungs-Samples zu schätzen). Im Frontend zeigt eedc ein ⚠-Badge bei Datenlücken — siehe Reparatur-Popover in §4.1c.
Peaks aus W-Integration (für Spitzenwerte):
Peak_PV_kW = max(W-Sample) / 1000 # 10-Min-AuflösungPeak_Netzbezug_kW = max(W-Sample) / 1000Peak_Einspeisung_kW = max(W-Sample) / 1000Peaks brauchen die Leistungssamples — die kWh-Aggregation läuft separat über die Snapshots.
Zusätzliche Daten pro Stunde:
- Temperatur + Globalstrahlung + GTI: Open-Meteo Historical API (Archiv) bzw. Forecast API (heute), inkl.
global_tilted_irradiancemit Modul-Tilt/Azimut - Stunden-Aggregation IST von Wetter-Samples: seit v3.23.6 als arithmetisches Mittel der 10-Min-Slots (vorher „last”) — konsistent mit der Mean-Konvention der Open-Meteo-Stundenwerte, behebt einen ~25-min-Versatz im Live-Heute-Chart.
- Batterie-SoC: HA Sensor History (Stundenmittel) — gefiltert auf
inv.typ == "speicher", siehe Vollzyklen-Hinweis unten - Strompreis (zwei Felder):
strompreis_cent(Endpreis aus HA-Sensor) +boersenpreis_cent(EPEX, immer befüllt)
Tageszusammenfassung (TagesZusammenfassung)
Abschnitt betitelt „Tageszusammenfassung (TagesZusammenfassung)“Überschuss_kWh = Σ(Überschuss_kWh) alle 24 StundenDefizit_kWh = Σ(Defizit_kWh)Peak_PV_kW = max(PV_kW) über alle StundenPeak_Netzbezug_kW = max(Netzbezug_kW)Peak_Einspeisung_kW = max(Einspeisung_kW)Temperatur_Min/Max = min/max(Temperatur_C) aus Open-MeteoStrahlung_Summe_Wh_m2 = Σ(Globalstrahlung_W/m²) × 1hGTI_Summe_Wh_m2 = Σ(global_tilted_irradiance) × 1h # ab v3.20.0Batterie-Vollzyklen (v3.22.0 verschärft):
Δ_SoC_Summe = Σ |SoC[h] - SoC[h-1]| für h = 1..23, AUSSCHLIESSLICH aus Investitionen mit typ == "speicher"Vollzyklen = Δ_SoC_Summe / 200 # 0→100→0 = 200 % = 1 VollzyklusE-Auto-SoC-Trennung: Vor v3.22.0 nahm
_get_soc_historyden erstenlive.soc-Sensor aus den Investitionen — bei Anlagen mit E-Auto landete dessen SoC zuerst in der Liste, der eigentliche stationäre Speicher wurde nicht angefasst. Folge:batterie_vollzyklenreflektierten den ΔSoC des Autos. Seit v3.22.0 filtern beide Selektions-Pfade (_get_soc_history, Bulk-Fetch inbackfill_from_statistics) aufinv.typ == "speicher". Nach Update auf v3.22.0: einmal „Verlauf nachberechnen + überschreiben” auslösen.
Performance Ratio (v3.20.0 auf GTI):
Theoretisch_kWh = GTI_Wh_m2 × kWp / 1000 # ab v3.20.0Performance_Ratio = PV_Ertrag_kWh / Theoretisch_kWh
# Vor v3.20.0 (deprecated, GHI-basiert):# Theoretisch_kWh = Strahlung_Wh_m2 × kWp / 1000 # horizontale GlobalstrahlungBei Multi-String-Anlagen werden GTI-Werte pro Orientierungsgruppe parallel abgerufen und kWp-gewichtet kombiniert (analog Live-Wetter-Pfad). Ohne gemappte PV-Module bleibt PR bewusst None statt einen verzerrten GHI-Wert zu melden.
Validation Winterborn 2025-12-28: GHI 1317 Wh/m² vs. GTI Süd35° 3358 Wh/m² (Faktor 2.55×). PR vorher 2.16 (physikalisch unmöglich), nachher 0.85 (plausibel für einen kalten Wintertag). Betrifft historische
TagesZusammenfassung.performance_ratio,MonatsAuswertungResponse.performance_ratio_avgund die PR-Spalte im PDF-Jahresbericht — nach Update einmalig „Verlauf nachberechnen + überschreiben” auslösen. PV-kWh-Werte selbst bleiben unverändert.
§51 EEG (Negativpreis-Analyse):
boersenpreis_avg = Ø(boersenpreis_cent[h])boersenpreis_min = min(boersenpreis_cent[h])neg_stunden = Anzahl h mit boersenpreis_cent[h] < 0einspeisung_neg_preis_kwh = Σ(Einspeisung_kWh[h]) für h mit boersenpreis_cent[h] < 0Datengrundlage für die §51-Sektion in Cockpit → Monat (die Monats-Darstellungen des alten Energieprofils sind dorthin gehoben — siehe Handbuch Energieprofil).
WP-Kompressor-Starts (v3.24.0, #136):
TagesEnergieProfil.wp_starts_anzahl[h] = snap_starts[h] - snap_starts[h-1] # Summe aller WP-Investitionen pro StundeTagesZusammenfassung.komponenten_starts = {"wp_starts_anzahl": {"<inv_id>": <int>, ...}} # Tages-Differenz pro WP-InvestitionArchitektur trennt Counter-Felder strikt von kWh-Feldern in KUMULATIVE_COUNTER_FELDER, damit reine Counter nicht versehentlich in die Energie-Bilanz fließen. Vollbackfill aus HA Long-Term Statistics greift für Tages-Summen (Faktor 1.0 statt 0.001 bei unbekannter Einheit). Stunden-Detail wird ab Live-Erfassung gefüllt.
Day-Ahead-Stundenprofil-Snapshot (v3.23.4, intern):
Zwei JSON-Felder in TagesZusammenfassung (pv_prognose_stundenprofil, solcast_prognose_stundenprofil) speichern den ersten OpenMeteo-/Solcast-Forecast des Tages als 24-Werte-Liste in kWh (Backward-Slot). First-write-wins: spätere Aufrufe am selben Tag überschreiben das Profil nicht. Reine Hintergrund-Datensammlung für künftige Diagnostik (Korrekturprofil-Konzept). Speicher ~80 KB/Jahr/Anlage.
Monats-Rollup (rollup_month)
Abschnitt betitelt „Monats-Rollup (rollup_month)“Aggregiert alle TagesZusammenfassung eines Monats in Monatsdaten-Felder:
| Monatsdaten-Feld | Aggregation | Beschreibung |
|---|---|---|
ueberschuss_kwh | Σ(Tages-Überschuss) | Monatlicher PV-Überschuss |
defizit_kwh | Σ(Tages-Defizit) | Monatliches Energie-Defizit |
batterie_vollzyklen | Σ(Tages-Vollzyklen) | Monatliche Batterie-Zyklen |
performance_ratio | Ø(Tages-PR) | Durchschnittliche Performance Ratio |
peak_netzbezug_kw | max(Tages-Peak) | Maximaler Netzbezug im Monat |
Auslöser: Wird beim Monatsabschluss nach backfill_range() aufgerufen, um fehlende Tage nachzuberechnen (begrenzt durch HA-History ~10 Tage).
7. Debugging-Leitfaden
Abschnitt betitelt „7. Debugging-Leitfaden“Häufige Fehlerquellen
Abschnitt betitelt „Häufige Fehlerquellen“| Symptom | Mögliche Ursache | Prüfung |
|---|---|---|
| Autarkie zu hoch/niedrig | Falsche Einspeisung/Netzbezug-Werte | Monatsdaten prüfen - sind die Zählerwerte plausibel? |
| EV-Quote > 100% | Speicher-Entladung > PV-Erzeugung | InvestitionMonatsdaten für Speicher prüfen |
| Netto-Ertrag = 0 | Kein Tarif angelegt | Strompreis-Tabelle prüfen |
| WP-Ersparnis fehlt | Kein WP-Spezialtarif, falscher Gas-Preis | Tarife prüfen; hardcodierter Gas-Preis 10ct im Cockpit |
| ROI weicht ab (Cockpit vs Investitionen) | Verschiedene Berechnungswege | Cockpit: kumuliert; Investitionen: p.a. mit calculations.py |
| SOLL überhöht | Teil-Jahr ohne faire Vergleichsbasis | months_with_data prüfen (ab v2.3.2 behoben) |
| PV-Erzeugung = 0 | Legacy-Feld statt InvestitionMonatsdaten | Prüfen ob PV-Module als Investitionen angelegt sind |
| Dienstl. Wallbox in E-Mob | ist_dienstlich nicht gesetzt | Investition.parameter["ist_dienstlich"] prüfen |
| USt wird nicht abgezogen | Steuerliche Behandlung falsch | Anlage.steuerliche_behandlung muss regelbesteuerung sein |
| Spezialtarif greift nicht | Falsche verwendung oder abgelaufen | Strompreis.verwendung und gueltig_ab/bis prüfen |
Datenfluss nachverfolgen
Abschnitt betitelt „Datenfluss nachverfolgen“Schritt 1: Eingabedaten prüfen
API: GET /api/monatsdaten/aggregiert/{anlage_id}→ Zeigt Monatsdaten + InvestitionMonatsdaten zusammenSchritt 2: Tarife prüfen
API: GET /api/strompreise?anlage_id={id}&aktuell=true→ Zeigt alle gültigen Tarife mit VerwendungSchritt 3: Berechnungsergebnis prüfen
API: GET /api/cockpit/uebersicht/{anlage_id}?jahr=2025→ Alle aggregierten KPIs für ein Jahr
API: GET /api/investitionen/roi/{anlage_id}→ ROI pro Komponente mit Detail-BerechnungSchritt 4: Prognose-Basis prüfen
API: GET /api/cockpit/prognose-vs-ist/{anlage_id}?jahr=2025→ PVGIS SOLL vs tatsächliche IST-Werte
API: GET /api/cockpit/pv-strings/{anlage_id}?jahr=2025→ SOLL-IST pro PV-Modul (mit Performance Ratio)Bekannte Fallstricke
Abschnitt betitelt „Bekannte Fallstricke“- JSON-Felder in SQLAlchemy: Änderungen an
verbrauch_datenoderparameterwerden nur persistiert mitflag_modified(obj, "feldname") - 0-Werte:
if val:wertet 0 als False aus → immerif val is not None:verwenden - Legacy-Felder:
Monatsdaten.batterie_*ist deprecated.Monatsdaten.pv_erzeugung_kwhist es nicht — kein Schreibziel für neuen Code und nur als Eingang vonresolve_pv_je_modulzu lesen (Anlagen-Aggregat, s. Schicht 1); Pro-Modul-Werte kommen ausInvestitionMonatsdaten(Typ: pv-module) - PVGIS E_m vs e_m: Ältere Prognosen verwenden
E_m(Großbuchstabe), neueree_m - Grundpreis: Wird zu den Netzbezugskosten addiert, NICHT vom Netto-Ertrag abgezogen. Er ist auch der Grund, warum „Kosten ÷ kWh” nicht den Ø-Preis ergibt — dafür gibt es
netzbezug_arbeitspreis_kosten_euro(s. §3.1) - Cockpit vs ROI-Dashboard: Cockpit berechnet inline (vereinfacht), ROI-Dashboard nutzt
calculations.py(detaillierter)
Letzte Aktualisierung: 2026-07-25 (v4.0)