Zum Inhalt springen

Architektur

Stand: 2026-08-07 — Frontend-Kapitel (3 · 6 · 9) gegen den Baum neu erhoben. Nachgezogen am 2026-08-11: der Berechnungs-Layer im Schichtenbild (Kapitalrechnung + Ertrags-Zerlegung).

Dieses Dokument trägt bewusst keine Versionsnummer. Versions-SoT ist CHANGELOG.md bzw. eedc/backend/core/config.py::APP_VERSION; scripts/release.sh bumpt docs/ nicht. Der vorige Kopf stand auf „v3.24.1, April 2026” und beschrieb eine Oberfläche, die es seit v4.0.0 nicht mehr gibt.

Wo dieses Dokument nicht die letzte Instanz ist:

FrageSoT
Informationsarchitektur, Oberflächen-Invarianten I1–I16, RedirectsKONZEPT-IA-V4.md
Darstellung (Farben, Komponenten, Typografie, Charts), Regel 0/0aKONZEPT-STYLE-GUIDE.md
Wo eine Aggregat-Formel definiert wirdADR-001
Was ein Wert behaupten darf, woher er kommt (P1–P10)ADR-002
die Monatszeile als eine Schicht (P10)KONZEPT-MONATS-FAKTEN.md
die Fläche Wärme/Klima (Heizen · Warmwasser · Kühlen), #263 als Kapitel 8KONZEPT-WAERME-KLIMA.md
Formeln je KennzahlBERECHNUNGEN.md
Einrichtung, Befehle, Gates, DateibestandDEVELOPMENT.md

Dieses Dokument beschreibt die Gesamtsicht: Schichten, Datenmodell, Services, Design-Entscheidungen. Wo eine Regel woanders steht, wird sie verwiesen, nicht kopiert — eine zweite Fassung daneben ist der Anfang der nächsten Drift.


  1. Übersicht
  2. Technologie-Stack
  3. Projektstruktur
  4. Datenmodell
  5. API-Architektur
  6. Frontend-Architektur
  7. Services
  8. Design-Entscheidungen
  9. Entwickler-Workflow

  1. Standalone-First: eedc funktioniert ohne externe Abhängigkeiten — Home Assistant ist eine Option, keine Voraussetzung. Kernfunktionen dürfen nicht an HA hängen; HA-only-Funktionen sind sichtbar als solche gekennzeichnet und werden ausgeblendet, wenn kein HA da ist.
  2. Lokale Datenspeicherung: SQLite unter /data/eedc.db, alle Daten bleiben beim Nutzer. Der Community-Server bekommt nur anonymisierte, ausdrücklich geteilte Aggregate.
  3. Vier Zeitebenen, eine Wahrheit je Größe: Live (5-Minuten-Snapshots) · Tag (Stundenprofile) · Monat (Zählerwerte, die abrechnungsrelevante Ebene) · Jahr. Die Monatsebene ist der Kern, aber längst nicht mehr die einzige — Tages- und Stundenebene entstehen aus eigenen Snapshots bzw. der HA-Langzeitstatistik.
  4. Datenquellen getrennt: Monatsdaten trägt Zählerwerte, InvestitionMonatsdaten die Komponenten-Details. Legacy-Felder (Monatsdaten.batterie_*, das computed-Trio) werden nicht gelesen; Monatsdaten.pv_erzeugung_kwh ist ausschließlich Eingang der PV-Auflösung (ADR-002/P7).
  5. Ein Wert, eine Konstruktionsstelle: jede Aggregat-Größe wird an genau einem Ort gebildet (ADR-001) und sagt, woher sie kommt (ADR-002). Zwei Sichten dürfen nicht zwei Zahlen für dieselbe Frage nennen — die häufigste Fehlerklasse dieses Projekts.
  6. Unvollständig heißt unvollständig: eine Teilsumme wird als solche ausgewiesen, nicht als niedriger Wert ausgeliefert (ADR-002/P4). Eine Lücke ist keine 0.
┌─────────────────────────────────────────────────────────────┐
│ Browser │
│ React (HashRouter — HA-Ingress-Pfad ist dynamisch) │
│ v4/ Sichten · components/ SoT-Bausteine · lib/ Ableitungen │
└────────────────────────┬────────────────────────────────────┘
│ HTTP/REST (/api/…)
┌────────────────────────┴────────────────────────────────────┐
│ FastAPI Backend │
│ ┌────────┐ ┌──────────┐ ┌───────────────┐ ┌──────────┐ │
│ │ Routes │→ │ Services │→ │ berechnungen/ │ │ Models │ │
│ │ HTTP │ │ Beschaff.│ │ ADR-001-SoT │ │ Tabellen│ │
│ └────────┘ └──────────┘ └───────────────┘ └──────────┘ │
│ APScheduler: Snapshot- · Prognose- · Korrektur-Jobs │
└────────────────────────┬────────────────────────────────────┘
│ SQLAlchemy (async)
┌────────────────────────┴────────────────────────────────────┐
│ SQLite /data/eedc.db │
│ Monatsdaten · InvestitionMonatsdaten · TagesEnergieProfil │
│ TagesZusammenfassung · sensor_snapshots · MQTT-Snapshots │
└─────────────────────────────────────────────────────────────┘
Externe Quellen (alle optional):
┌────────────┐ ┌────────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐
│ Open-Meteo │ │ Bright Sky │ │ PVGIS │ │ Solcast │ │ EU Oil Bull. │
│ Wetter/ │ │ (DWD) │ │ Jahres- │ │ PV-Prog- │ │ Kraftstoff- │
│ Solar-GTI │ │ │ │ prognose │ │ nose │ │ preise │
└────────────┘ └────────────┘ └──────────┘ └──────────┘ └──────────────┘
┌──────────────────────────────────────────┐ ┌────────────────────────┐
│ Home Assistant │ │ Geräte / Cloud-APIs │
│ REST (States) · WebSocket (LTS) · │ │ 9 Connectors (LAN) │
│ Recorder-DB/-Datei · MQTT in UND out │ │ 12 Cloud-Provider │
└──────────────────────────────────────────┘ └────────────────────────┘
┌────────────────────────┐
│ eedc-community (opt-in)│
│ anonyme Aggregate │
└────────────────────────┘

Drei Wege zur HA-Langzeitstatistik, in dieser Vorrangfolge: externe Recorder-DB (HA_RECORDER_DB_URL, SQL) · eingehängte Recorder-Datei (/config, SQL) · WebSocket recorder/statistics_during_period (braucht nur Token — der einzige Weg für einen Standalone-Container neben HA). Alle drei liefern dieselbe Quelle; verzweigt wird ausschließlich die Zeilen-Beschaffung, nicht die Aggregation.


SoT ist eedc/backend/requirements.txt — dort stehen auch die Obergrenzen, die es aus Erfahrung gibt (FastAPI ist gedeckelt, damit ein Major-Sprung nicht still in ein HA-Add-on-Image gerät).

TechnologieVersionZweck
Python3.11+Programmiersprache
FastAPI≥ 0.109, < 0.137REST-API-Framework (bewusst gedeckelt)
Uvicorn≥ 0.27ASGI-Server
SQLAlchemy≥ 2.0.25ORM, async
aiosqlite≥ 0.19async-SQLite-Treiber (loop-gebunden — sync-Aufrufer brauchen eine Brücke)
SQLite3.xDatenbank
Pydantic≥ 2.5 (+ pydantic-settings)Datenvalidierung, Settings
httpx≥ 0.26HTTP-Client für externe APIs
websockets≥ 12.0HA-WebSocket (Langzeitstatistik ohne DB-Zugang)
aiomqtt≥ 2.0MQTT — Export und Inbound
APScheduler≥ 3.10Snapshot-, Prognose- und Korrektur-Jobs
WeasyPrint + Jinja2≥ 62.0 / ≥ 3.1PDF-Berichte (HTML→PDF, SVG-Charts)
TechnologieVersionZweck
React18.xUI Framework
TypeScript5.xTypsichere Programmierung
Vite5.xBuild Tool & Dev Server
Tailwind CSS3.xStyling
Recharts2.xDiagramme
react-simple-maps3.xChoropleth Deutschlandkarte
Lucide React-Icons
React Router6.xRouting
VarianteTechnologie
Home Assistant Add-onDocker (Ingress)
StandaloneDocker
EntwicklungUvicorn + Vite Dev Server

Den vollständigen Dateibestand führt DEVELOPMENT.md §Projektstruktur — bewusst an einem Ort. Zwei vollständige Listen in zwei Dokumenten driften garantiert auseinander; genau so ist die V3-Liste entstanden, die hier bis 2026-08-07 stand. Dieses Kapitel beschreibt die Schichten und ihre Zuständigkeiten.

RepositoryRolleWird wie gepflegt
eedc-homeassistantSource of Truth — Backend, Frontend, Docs, HA-Add-on-Konfiguration, Websitehier wird gearbeitet
eedcStandalone-Distribution für Nutzer ohne HASpiegel, ausschließlich per scripts/release.sh
eedc-communityanonymer Community-Benchmark-Server (FastAPI + PostgreSQL)unabhängig; bei Datenmodell-Änderungen synchron anpassen

Die Anwendung liegt vollständig unter eedc/ — dasselbe Verzeichnis, das ins Standalone-Repo gespiegelt wird. HA-spezifisch sind nur config.yaml, Dockerfile, run.sh und die Icons.

Die Reihenfolge ist die Abhängigkeitsrichtung — nach oben wird nie gerufen:

SchichtVerzeichnisZuständig fürDarf nicht
Routenapi/routes/HTTP, Validierung, Response-Formrechnen — Aggregat-Formeln gehören in den Layer (ADR-001)
Servicesservices/Beschaffung, Aufbereitung, externe APIs, Persistenz-Abläufeeigene Fassungen einer Formel halten
Berechnungs-Layercore/berechnungen/alle Aggregat-Formeln, je Größe genau eineI/O oder DB-Zugriff
Modellemodels/SQLAlchemy-TabellenFachlogik
Kerncore/Config, Engine, Schema-Nachzug, SoT-Helper—

Drei Stellen sind dabei die tragenden Single Sources of Truth:

  • services/monats_fakten/ — die Monatszeile wird einmal aufgelöst und gefiltert (aktiv · Anschaffung · Stilllegung · Dienstwagen), dann ruft sie die Layer-Formeln. Keine Read-Site faltet InvestitionMonatsdaten mehr selbst (ADR-002/P10, KONZEPT-MONATS-FAKTEN.md).
  • core/investition_kennwerte.py — Nennleistung je Typ, gleich ob sie in der Spalte oder im parameter-JSON liegt (ADR-002/P3-a).
  • core/berechnungen/slot_konvention.py — Slot N ist die Energie in [N-1, N), baumweit (Backward-Konvention, Industriestandard).
SchichtVerzeichnisZuständig für
Sichtenv4/die ausgelieferte Oberfläche: eine Datei je Sicht bzw. Block
Geteilte Bausteinecomponents/SoT-Komponenten (eine Klasse = eine Komponente), Block-Modell, Park-Mechanik
Einstellungs-Flächenpages/Stammdaten, Import-Assistenten, Datenverwaltung — von V4 eingebunden
Ableitungenlib/reine Funktionen + Farb-/Datums-/Einheiten-SoT
Transportapi/ein Modul je Backend-Router, client.ts als Basis
Zustandhooks/, context/useApiData (inkl. SWR-Sicht-Cache), Auswahl, Theme, Status
Wegeroutes/, config/Route-Manifest + Redirects, Einstellungs-Katalog, Version

⚠ eedc/frontend/dist/ ist NICHT versioniert (seit N-246 / v4.0.15) — beide Dockerfiles bauen das Frontend in einer eigenen Stage. Wer lokal mit Demo-Flags baut, muss dist/ deshalb nicht mehr wiederherstellen; gegen einen versehentlich ausgelieferten Demo-Build schützt release.sh::pruefe_nichts_uebrig. Hier stand bis 2026-08-23 das Gegenteil — CLAUDE.md trug den Widerruf seit dem 14.08., diese Zeile und DEVELOPMENT.md nicht.


┌─────────────────────────────────────────────────────────────┐
│ Anlage │
│ id, name, adresse, koordinaten, ausrichtung, neigung │
└──────┬──────────────┬──────────────┬──────────────┬─────────┘
│ │ │ │
│ 1:n │ 1:n │ 1:n │ 1:n
▼ ▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Monatsdaten │ │ Strompreise │ │ Investitionen│ │ InfothekEintrag │
│ (Zählerwerte)│ │ (Tarife) │ │ (Komponenten)│ │ (Verträge, Daten)│
└──────────────┘ └──────────────┘ └──────┬───────┘ └────────┬─────────┘
│ │
│ 1:n │ 1:n
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Investition- │ │ InfothekDatei │
│ Monatsdaten │ │ (Fotos, PDFs) │
└──────────────────┘ └──────────────────┘
Investitionen ◄── N:M ──► InfothekEintrag
(infothek_investition Junction Table)
FeldTypBeschreibung
idINTEGERPrimary Key
anlagennameVARCHAR(255)Bezeichnung
leistung_kwpFLOATAnlagenleistung in kWp
installationsdatumDATEInbetriebnahme (optional)
standort_landVARCHAR(5)Land: DE, AT oder CH
standort_plzVARCHAR(10)Postleitzahl
standort_ortVARCHAR(255)Ort
standort_strasseVARCHAR(255)Adresse
latitudeFLOATBreitengrad (für PVGIS)
longitudeFLOATLängengrad (für PVGIS)
ausrichtungVARCHAR(50)DEPRECATED – jetzt bei PV-Modul Investitionen
neigung_gradFLOATDEPRECATED – jetzt bei PV-Modul Investitionen
wechselrichter_herstellerVARCHAR(50)sma, fronius, kostal, etc.
mastr_idVARCHAR(20)MaStR-ID der Anlage
versorger_datenJSONVersorger & Zähler
wettermodellVARCHAR(30)auto, meteoswiss_icon_ch2, icon_d2, icon_eu, ecmwf_ifs04
sensor_mappingJSONHA-Sensor-Mapping
community_hashVARCHAR(64)Hash für Community-Löschung
steuerliche_behandlungVARCHAR(30)keine_ust oder regelbesteuerung
ust_satz_prozentFLOATUSt-Satz: DE=19, AT=20, CH=8.1
created_atDATETIMEErstellungsdatum
updated_atDATETIMELetztes Update

Wichtig: Diese Tabelle enthält primär Zählerwerte (Einspeisung, Netzbezug).

FeldTypBeschreibung
idINTEGERPrimary Key
anlage_idINTEGERForeign Key → Anlage
jahrINTEGERJahr
monatINTEGERMonat (1-12)
einspeisung_kwhFLOATZählerwert Einspeisung
netzbezug_kwhFLOATZählerwert Netzbezug
direktverbrauch_kwhFLOATBerechnet
eigenverbrauch_kwhFLOATBerechnet
gesamtverbrauch_kwhFLOATBerechnet
globalstrahlung_kwh_m2FLOATWetter-API
sonnenstundenFLOATWetter-API
durchschnittstemperaturFLOATWetter-API
netzbezug_durchschnittspreis_centFLOATØ Strompreis bei dynamischem Tarif (ct/kWh)
kraftstoffpreis_euroFLOATØ Benzinpreis des Monats (€/L, aus EU Oil Bulletin)
sonderkosten_euroFLOATManuelle Eingabe
sonderkosten_beschreibungVARCHAR(500)Beschreibung der Sonderkosten
datenquelleVARCHAR(50)manual, csv, ha_import
notizenVARCHAR(1000)Freitext
created_atDATETIMEErstellungsdatum
updated_atDATETIMELetztes Update

Legacy-Felder (nicht mehr verwenden):

  • pv_erzeugung_kwh → Verwende InvestitionMonatsdaten
  • batterie_ladung_kwh → Verwende InvestitionMonatsdaten
  • batterie_entladung_kwh → Verwende InvestitionMonatsdaten
  • batterie_ladung_netz_kwh → Arbitrage (Legacy)
  • batterie_ladepreis_cent → Arbitrage (Legacy)
FeldTypBeschreibung
idINTEGERPrimary Key
anlage_idINTEGERForeign Key → Anlage
typVARCHAR(50)Investitionstyp (siehe InvestitionTyp Enum)
bezeichnungVARCHAR(255)Name der Komponente
anschaffungsdatumDATEInbetriebnahme
anschaffungskosten_gesamtFLOATKaufpreis + Installation
anschaffungskosten_alternativFLOATAlternativkosten (z.B. neuer Verbrenner)
betriebskosten_jahrFLOATJährliche Betriebskosten
leistung_kwpFLOATLeistung in kWp (PV-Module)
ausrichtungVARCHAR(50)Modulausrichtung (PV-Module)
neigung_gradFLOATModulneigung in Grad (PV-Module)
ha_entity_idVARCHAR(255)HA Entity-ID (für String-IST-Erfassung)
parameterJSONTyp-spezifische Parameter
einsparung_prognose_jahrFLOAT„Ertrag/Jahr” — wiederkehrender Ertrag/Einsparung, pflegbar bei Wallbox/Sonstiges (ERTRAGSFELD_TYPEN)
co2_einsparung_prognose_kgFLOATCO2-Einsparungsprognose
aktivBOOLEANAktiv/Inaktiv
parent_investition_idINTEGERForeign Key → Investitionen (für Parent-Child)
created_atDATETIMEErstellungsdatum
updated_atDATETIMELetztes Update

Investitionstypen:

TypParameter (JSON)
wechselrichter-
pv-moduleanzahl_module, modul_leistung_wp, ausrichtung, neigung_grad
speicherkapazitaet_kwh, arbitrage_faehig
e-autojahresfahrleistung_km, verbrauch_kwh_100km, pv_ladeanteil_prozent, v2h_faehig, ist_dienstlich
waermepumpeeffizienz_modus, jaz, cop_heizung, cop_warmwasser, heizwaermebedarf_kwh, warmwasserbedarf_kwh, leistung_kw, pv_anteil_prozent, alter_energietraeger, alter_preis_cent_kwh, sg_ready
wallboxist_dienstlich
balkonkraftwerkleistung_wp, anzahl, hat_speicher, speicher_kapazitaet_wh
sonstigeskategorie (erzeuger/verbraucher/speicher), beschreibung

Legacy-Felder im parameter JSON (nicht mehr im Formular):

Die folgenden Felder (stamm_*, ansprechpartner_*, wartung_*) wurden aus dem Investitionsformular entfernt. Gerätedaten, Ansprechpartner und Wartungsverträge werden jetzt über die Infothek verwaltet (N:M-Verknüpfung). Bestehende Daten bleiben im parameter-JSON erhalten und können über den Migrations-Service (infothek_migration.py) automatisch in Infothek-Einträge überführt werden.

FeldTypBeschreibung
idINTEGERPrimary Key
investition_idINTEGERForeign Key → Investitionen
jahrINTEGERJahr
monatINTEGERMonat (1-12)
verbrauch_datenJSONTyp-spezifische Messwerte
einsparung_monat_euroFLOATBerechnet
co2_einsparung_kgFLOATBerechnet
created_atDATETIMEErstellungsdatum
updated_atDATETIMELetztes Update

Hinweis: Sonstige Erträge/Ausgaben werden in verbrauch_daten als sonstige_ertraege / sonstige_ausgaben Arrays gespeichert (v2.4.0).

verbrauch_daten Struktur je nach Investitionstyp:

// PV-Module
{
"pv_erzeugung_kwh": 450.5
}
// Speicher
{
"ladung_kwh": 200.0,
"entladung_kwh": 185.0,
"ladung_netz_kwh": 50.0, // Arbitrage
"ladepreis_cent": 15.5 // Arbitrage
}
// E-Auto
{
"km_gefahren": 1200,
"verbrauch_kwh": 216.0,
"ladung_pv_kwh": 130.0,
"ladung_netz_kwh": 86.0,
"ladung_extern_kwh": 50.0,
"ladung_extern_euro": 25.0,
"v2h_entladung_kwh": 25.0
}
// Wärmepumpe
{
"stromverbrauch_kwh": 450.0,
"heizenergie_kwh": 1800.0,
"warmwasser_kwh": 200.0
}
// Wallbox
{
"ladung_kwh": 150.5,
"ladevorgaenge": 10
}
// Balkonkraftwerk (mit optionalem Speicher)
{
"pv_erzeugung_kwh": 45.0,
"eigenverbrauch_kwh": 40.0,
"speicher_ladung_kwh": 10.0,
"speicher_entladung_kwh": 8.0
}
// Sonstiges - Erzeuger (v2.4.0)
{
"erzeugung_kwh": 120.0,
"eigenverbrauch_kwh": 100.0,
"einspeisung_kwh": 20.0
}
// Sonstiges - Verbraucher (v2.4.0)
{
"verbrauch_kwh": 200.0,
"bezug_pv_kwh": 80.0,
"bezug_netz_kwh": 120.0
}
// Sonstiges - Speicher (v2.4.0)
{
"ladung_kwh": 50.0,
"entladung_kwh": 45.0
}
// Sonstige Erträge & Ausgaben (alle Typen, v2.4.0)
{
"sonstige_ertraege": [{"bezeichnung": "Einspeisebonus", "betrag": 15.0}],
"sonstige_ausgaben": [{"bezeichnung": "Versicherung", "betrag": 8.50}]
}

versorger_daten Struktur (Anlage):

{
"strom": {
"name": "Stadtwerke München",
"kundennummer": "12345678",
"portal_url": "https://kundenportal.swm.de",
"notizen": "",
"zaehler": [
{"bezeichnung": "Einspeisung", "nummer": "1EMH0012345678", "notizen": ""},
{"bezeichnung": "Bezug", "nummer": "1EMH0087654321", "notizen": "Zweirichtungszähler"}
]
},
"gas": {
"name": "Stadtwerke München",
"kundennummer": "G-98765",
"zaehler": [{"bezeichnung": "Erdgas", "nummer": "G12345678", "notizen": ""}]
},
"wasser": {
"name": "Wasserwerke XY",
"kundennummer": "W-11111",
"zaehler": [{"bezeichnung": "Kaltwasser", "nummer": "WZ-123", "notizen": ""}]
}
}
FeldTypBeschreibung
idINTEGERPrimary Key
anlage_idINTEGERForeign Key → Anlage
netzbezug_arbeitspreis_cent_kwhFLOATPreis pro kWh Netzbezug
einspeiseverguetung_cent_kwhFLOATVergütung pro kWh Einspeisung
grundpreis_euro_monatFLOATMonatlicher Grundpreis
gueltig_abDATEGültigkeitsbeginn
gueltig_bisDATEGültigkeitsende (NULL = aktuell gültig)
tarifnameVARCHAR(255)Name des Tarifs
anbieterVARCHAR(255)Stromanbieter
vertragsartVARCHAR(50)fix, dynamisch, etc.
verwendungVARCHAR(30)allgemein, waermepumpe oder wallbox
created_atDATETIMEErstellungsdatum
updated_atDATETIMELetztes Update
FeldTypBeschreibung
idINTEGERPrimary Key
anlage_idINTEGERForeign Key → Anlage
bezeichnungVARCHAR(255)Name des Eintrags
kategorieVARCHAR(50)Kategorie (15 Typen, siehe Handbuch)
notizenTEXTFreitext-Notizen (Markdown)
parameterJSONKategorie-spezifische Felder
investition_idINTEGERLegacy 1:1 FK (deprecated, nur Migration)
ansprechpartner_idINTEGERFK → InfothekEintrag (Vertragspartner-Verweis)
sortierungINTEGERReihenfolge
aktivBOOLEANArchiviert = false
in_anlagendokuBOOLEANIn Anlagendokumentation-PDF anzeigen
created_atDATETIMEErstellungsdatum
updated_atDATETIMELetztes Update

15 Kategorien: stromvertrag, einspeisevertrag, gasvertrag, wasservertrag, fernwaerme, brennstoff, versicherung, ansprechpartner, wartungsvertrag, marktstammdatenregister, foerderung, garantie (Komponente/Datenblatt), steuerdaten, messstellenbetreiber, sonstiges

FeldTypBeschreibung
idINTEGERPrimary Key
infothek_eintrag_idINTEGERFK → InfothekEintrag (CASCADE)
investition_idINTEGERFK → Investitionen (CASCADE)
FeldTypBeschreibung
idINTEGERPrimary Key
eintrag_idINTEGERFK → InfothekEintrag (CASCADE)
dateinameVARCHAR(255)Originaler Dateiname
dateitypVARCHAR(10)image oder pdf
mime_typeVARCHAR(50)MIME-Typ
beschreibungVARCHAR(255)Optionale Beschreibung
datenLARGEBINARYDatei-BLOB (Bilder max ~500 KB, PDFs max 10 MB)
thumbnailLARGEBINARYVorschaubild (nur für Bilder)
created_atDATETIMEErstellungsdatum

Kanalstatistik — Summen wie das HA-Energie-Dashboard (erstes Paket der HA-Bauform, Stand E4f)

Abschnitt betitelt „Kanalstatistik — Summen wie das HA-Energie-Dashboard (erstes Paket der HA-Bauform, Stand E4f)“

Drei Tabellen nach dem Vorbild der HA-Langzeitstatistik (Umbau „eedc nach HA-Bauform”, Etappen E1–E4f, Bauplan bauplan-ha-bauform-s1-s2). Seit E4 lesen alle Tages-, Monats- und Jahressummen daraus, wo die Kanäle den Zeitraum voll decken (je Zeitraum und Gruppe eine Wahl, ADR-002/P14); sonst rechnet der bisherige Leser aus Stunden-, Tages- und Monatszeilen. Stundenprofile, Kurven, Energieprofil-Stunden, die stundengepaarten Spalten, die Werkbank und die Prognose-Leser bleiben bis S3 auf dem Bestand; TagesEnergieProfil, TagesZusammenfassung und sensor_snapshots laufen unverändert weiter (bis S5 wird keine Spalte gelöscht, kein Schreiber abgeschaltet).

TabelleInhalt
kanaleine Größe einer Anlage: key im Snapshot-Schema (basis:<feld>, inv:<id>:<feld>, modus:inv:<id>:<betriebsart>), art ∈ sum · mean · stand, einheit
kanal_quelleHerkunft ab gueltig_ab: Familie (spiegel · bestand · mitschrift · abgeleitet), HA-statistic_id bzw. MQTT-Schlüssel, offset (Sensortausch: Kanalwert = Zeile + offset, die Zeile bleibt wörtlich)
kanal_statistikeine Zeile je Kanal und Stunde (start_ts = Stundenbeginn, Unix-Sekunden), sum · state · mean · min · max, eindeutig über (kanal_id, start_ts)

Familien. spiegel = HAs sum/state/mean wörtlich (Zähler, Zählwerke, Mittelwerte mit Langzeitstatistik); mitschrift = was HA nicht dauerhaft hält — die Betriebsart einer Wärmepumpe als Anteil der Stunde je Betriebsart (modus:inv:<id>:<betriebsart>) und ohne HA die eigene Summe aus den MQTT-Rohständen nach HAs Reset-Regel (core/berechnungen/ha_summe.py, der Deckel bleibt dort Schreibfilter); abgeleitet = aus anderen Kanälen je Stunde gerechnet, mit aufbaubar_ab. Die Familie bestand bleibt unbelegt (Entscheid 06.10., Bauplan §3b): die bisherigen Stunden- und Tageszeilen werden nicht umgewandelt — sie bleiben die Quelle für Zeiträume, die die Kanäle nicht voll decken (Lesart 1). Mittelwerte ohne Langzeitstatistik bekommen keinen Kanal und keine Mitschrift (Entscheid 06.10.). Welche Art und Familie ein Feld bekommt, leitet services/kanal/katalog.py aus der Feld-Registry ab (Wächter test_kanal_feld_deckung.py, Baseline 0).

Schreiber. Stündlich :05 nach snapshot_anlage (services/kanal/schreiber.py::schreibe_kanaele_im_stundenlauf, in derselben Sitzung, eigener SAVEPOINT — ein Fehler dort berührt den Bestand nicht): Spiegel fortschreiben, ohne HA die eigene Summe, dann die abgeleiteten Kanäle in dieser Reihenfolge —

  • PV-Anteil der Heimladung je Wallbox/E-Auto ohne gemessene Aufteilung (abgeleitet.py, abgeleitet:inv:<id>: ladung_pv_kwh, Regel der Stunde core/berechnungen/pv_anteil_ladung.py: was nicht aus dem Netz kam; E4c);
  • Strom je Betriebsart der Wärmepumpe (modus_strom.py, abgeleitet:inv:<id>:modus_strom_<betriebsart>_kwh und …:modus_abdeckung_h = Δ Gesamtstrom × Anteil aus der Mitschrift; gemessene Betriebsart-Zähler und getrennte Strommessung haben Vorrang; E4d);
  • Kosten-Summen bei Stundenpreis (kosten.py, sechs Kanäle abgeleitet:basis:kosten_netzbezug · netzbezug_bewertet_kwh · kosten_ev_vermieden · ev_bewertet_kwh · preis_summe · preis_stunden, Regel der Stunde core/berechnungen/kosten_stunde.py; einen Einspeise-Erlös-Kanal gibt es bewusst nicht; E4e). Ein neuer abgeleiteter Kanal beginnt beim ersten Lauf mit dem laufenden Monat (eine Stunde vor seinem Monatsfenster, ab der ersten Stunde mit Vorstand aller Eingänge) — ein abgeschlossener Monat wird nie rückwirkend gedeckt. Die Betriebsart-Mitschrift schreibt aggregate_day. Eine gelöschte Anlage räumt ihre Kanäle (ON DELETE CASCADE) wie ihre Snapshots; eine gelöschte Investition räumt — wie bei den Snapshots — keine.

Nachfüllen (E2). Eine Hintergrund-Aufgabe (services/kanal/nachfuellen.py, nach dem Start für alle Zuordnungen, nach jedem Stundenlauf angestoßen für neu angelegte Kanäle; Marke kanal_nachfuellung je Kanal und Entity — ein Kanal mit Marke wird nicht erneut bei HA angefragt, ein später zugeordneter Sensor oder eine neue Entity bekommt ihre Vorgeschichte beim nächsten Lauf) holt für jeden Spiegel-Kanal die ganze Historie der heutigen HA-Entity — rückwärts vor die erste Kanal-Zeile, in Blöcken von 31 Tagen, je Block eine kurze eigene Schreib-Transaktion; HA wird nur zwischen zwei Transaktionen gelesen, der Stundenlauf wartet also nie auf HA. Ein abgebrochener Lauf setzt an der ersten vorhandenen Zeile fort, ein zweiter schreibt nichts; fällt HA unterwegs aus, bekommt der Kanal keine Marke. Fortschritt und Ergebnis stehen im Aktivitätsprotokoll. Die Familie bestand bleibt unbelegt (Entscheid 06.10., Bauplan §3b): die bisherigen Stunden- und Tageszeilen werden nicht in Kanäle umgewandelt — sie bleiben die Quelle für Zeiträume, die die Kanäle nicht voll decken (je Tag bzw. Monat EINE Quellenwahl).

Konsistenzlauf (E2). Täglich 02:45 (services/kanal/konsistenz.py, eigener Job neben dem unveränderten Tages-Nachlauf): je Spiegel-Kanal und Quelle vergleicht er die gespeicherten Zeilen mit HA (letzte Zeile, Zeilenzahl und Vorsumme), sucht bei Abweichung die erste abweichende Stunde per Halbierung und spiegelt ab dort neu — so zieht eine „Summe anpassen” in HA (Versatz auf alle Folgezeilen) oder eine nachgereichte Stunde nach. Ersetzt werden nur Spiegelzeilen; spätere Quellen bekommen die Änderung in ihren offset, jedes Δ nach einem Sensortausch bleibt, wie HA es nennt. Liefert HA für eine Quelle nichts mehr — oder nur noch ab einem späteren Zeitpunkt —, bleibt der Spiegel davor stehen; ist HA unterwegs nicht erreichbar, ändert der Lauf nichts. Mit jeder Korrektur verwirft er die abgeleiteten Kanäle ab der korrigierten Stunde (abgeleitet.verwerfe_ab, alle abgeleitet:%); der Stundenlauf baut sie von dort neu auf.

Stunde und Slot. Eine Stundenzeile des Bestands (Rückwärts-Slot h = [h−1, h), #144) wird an genau einer Stelle zu start_ts: core/berechnungen/slot_konvention.py::slot_start_ts, die Umkehrung von lts_boundary_index (doppelte Herbststunde ⇒ die spätere, fehlende Frühjahrsstunde ⇒ kein start_ts); das Tagesfenster [Vortag 23:00, 23:00) ist tagesfenster_start_ts. Auch die Betriebsart-Mitschrift rechnet darüber. Der Wächter test_kanal_symmetrie.py prüft auf den Datenständen beider Abnahme-Matrizen je Tag Kanal-Δ ≡ komponenten_kwh (Toleranz 0,005 kWh, benannte und gezählte Ausnahmeklassen); seit E3 liest er über die Lese-Schicht.

Lese-Schicht (E3). services/kanal/lesen.py hat vier Lesefunktionen, je auch als Stapel über viele Kanäle in einem Aufruf: zeitraum (Δ über [von, bis)), reihe (dasselbe je Intervall, über n+1 Randstände), stunden (Zuwachs je belegter Stunde wie HAs change) und mittel (mean/min/max wörtlich in der Einheit des Kanals). Zeitgrenzen sind absolute Zeit; der Stand vor t ist die letzte Zeile mit start_ts < t, wie in HA. Kanalwert: sum ⇒ sum + offset, stand ⇒ state roh (offset nur als Brücke für das Δ über eine Quellgrenze). Jedes Ergebnis trägt seine Abdeckung (gedeckt_von, gedeckt_bis, rand_spanne, voll, Grund): voll gedeckt ist ein Zeitraum, wenn der Kanal einen Stand vor von hat, sein Stand das Ende erreicht (im laufenden Zeitraum mit Toleranz für den Schreibverzug des Stundenlaufs) und der Zeitraum nicht feiner ist als die Spanne zweier Stände, in der einer seiner Ränder liegt. Eine Lücke im Inneren ist keine fehlende Abdeckung — ihre Menge steht in der Folgestunde, wie in HA —; sie nennt stunden je Stunde. zeitraum und reihe lesen dafür nur die Randstände: zwei Index-Schritte je Kanal und Grenze, eine SQL-Anweisung je Aufruf, gleich wie lang der Zeitraum ist. Ohne volle Abdeckung gibt es keinen Wert, nur eine benannte Teilsumme. fenster.py bildet Tages- und Monatsfenster so, wie der Bestand sie hat ([Vortag 23:00, 23:00), Monat = Tage des Monats), dazu den Kalendermonat des HA-Monatslesers ([1. 00:00, 1. 00:00)) für die Leser, die heute mit ihm rechnen; quellenwahl.py sagt je Zeitraum „Kanal” (jeder benötigte Kanal deckt voll) oder „Bestand” (mit Grund je Kanal); monatsraster.py liefert je Monat Δ, Abdeckung und diese eine Wahl — die Überlagerung „gespeichert schlägt gerechnet” bleibt Sache der Monats-Fakten. Außerhalb services/kanal/ importieren die Schicht nur die Monats-Fakten und benannte Tages-Leser (Wächter test_kanal_lesen_waechter.py). Seit E4f fasst zellen_stapel den Zuwachs mehrerer Zeiträume je (Wochentag, Uhrstunde) in einer Anweisung zusammen — die Gewichte eines Zeitfenster-Tarifs.

Quellenwahl je Zeitraum (ADR-002/P14) und die Leser je Gruppe. Je Tag, Monat (Monatsfenster des Bestands, [Vortag 23:00, letzter Tag 23:00)) bzw. Kalendermonat ([1. 00:00, 1. 00:00)) und Gruppe EINE Wahl: kanal nur, wenn jeder Eingang der Formel voll deckt (in einer Entweder-oder-Gruppe der erste deckende, W2-R4), sonst bestand für den ganzen Zeitraum. Die Gruppen sind getrennt — eine kann decken, die andere nicht:

GruppeLeserWas er liefert
Bilanz (Netz · PV/Balkonkraftwerk samt Anlagenzähler · Speicher · Erzeuger hinter dem Zähler)bilanz_leser.pyje Tag KanalTag, je Monat TagesMonatsSumme über die Komposition core/berechnungen/bilanz_zeitraum.py (Regeln W2-R1…R5; Wandlungsverluste geführt; Geld, USt und CO₂ ziehen sie ab, wenn der Messpunkt-Vertrag hält (ADR-002/P15, verluste_grund), die Bilanz trägt sie); Kalendermonat je Sensor (kanal_kalendermonate)
E-Mobilität (Lademengen, gemessene Aufteilung, abgeleiteter PV-Anteil) · Sonstigesgeraete_leser.pyje Monat bzw. Kalendermonat die Zeilen {feld: Δ} je Gerät und der PV-Anteil; Wallbox-Regel, Pool, Dienstwagen entscheiden die Monats-Fakten zur Lesezeit (P10)
Wärmepumpe (Strom, Wärme je Funktion, Betriebsart-Zähler, Strom je Betriebsart)wp_leser.pyje Monat bzw. Kalendermonat die Zeile je Gerät in der Form einer Abschluss-Zeile; gemessene 0 ist Betrieb ohne Wärme
Preis (sechs Kosten-Kanäle)preis_leser.pyje Monat ein StrompreisAggregat (gewichtet = Δ Kosten ÷ Δ bewertete kWh); die Netzbezugs-Gewichte des Zeitfenster-Tarifs je (Wochentag, Uhrstunde)

Umschaltstellen. Die Monats-Fakten (services/monats_fakten/laden.py: lade_monats_summen, geraete_monate, wp_monate; gespeichert schlägt gerechnet je Gruppe, P8), Cockpit → Monat (Tagesebene und HA-Weg des Kalendermonats: _ha_statistik_aus_kanaelen_mit_marke, _geraete_aus_kanaelen, _wp_aus_kanaelen), der Preis-Leser strompreis_aggregator.lade_preis_aggregate_je_monat/berechne_monats_durchschnittspreis/wirksamer_arbeitspreis_cent, die Tages-Leser (tage_werte, energie_profil/tag, tage, monat) und die Kalendermonats-Schreibwege („Aus HA laden”, alle Monatswerte, Import-Vorschau, Sammelimport, Monatsabschluss-Vorschlag) über EINE Stelle, bilanz_leser.monatswerte_mit_kanaelen: Bilanz-, E-Mob- und Sonstiges-Sensoren bekommen das Kanal-Δ, wo ihre Gruppe den Kalendermonat deckt (seit E4f auch E-Mob und Sonstiges); Wärmepumpen- und Preis-Sensoren bleiben dort beim HA-Leser. Einen gespeicherten Vorbestand ersetzt der Sammelimport nur mit „überschreiben”. Kein Deckel, kein Rücksprung-Verwurf für den HA-Spiegel: ein Zählersprung steht in Tag und Monat, der Daten-Checker benennt ihn (daten_checker/datenquelle/ha_sprung.py, „Zählerstände – Sprung in Home Assistant”).

Lade-Kontext je Anfrage (E4f). Jede GET-Anfrage bekommt EINEN Kontext der Kanal-Leser (services/kanal/lade_kontext.py, Middleware kanal_lade_kontext in main.py): jeder Randstand (Kanal × Zeitpunkt) wird in der Anfrage einmal gelesen und von allen Gruppen, Fenstern und Fakten-Aufrufen geteilt — ein weiteres Fenster liest nur die Zeitpunkte, die die Anfrage noch nicht kennt —, ebenso Kanäle, Stammdaten, die Quellenwahl je Fenster und die Gewichte des Zeitfenster-Tarifs (alle vorgemerkten Monate in einer Abfrage); die Uhr der Leser steht in der Anfrage still. (Gemessen und verworfen: die Ränder aller Kanäle der Anlage vorab in einer Anweisung — +50 ms je Route.) Jede schreibende Anweisung der Sitzung, commit und rollback leeren ihn; er überdauert die Anfrage nie (kein Modul-Cache). Schreibende Routen, Scheduler, Stundenlauf, Nachfüllen und Konsistenzlauf laufen ohne ihn. Mit und ohne ihn dieselben Zahlen (test_e4f_lade_kontext.py, auch das Abfrage-Budget: Anweisungen gegen kanal_statistik je Route konstant über Monate und Stunden).

Wächter und Proben. test_kanal_lesen_waechter.py (wer die Schicht importiert), test_kanal_feld_deckung.py (Katalog ↔ Registry), test_kanal_symmetrie.py (Kanal-Δ je Tag ≡ Bestand), die Gleichheitsproben test_kanal_{bilanz,geraete,wp,kosten}_gleichheit.py, die Wahl-Proben test_kanal_quellenwahl.py, test_kanal_bilanz_leser_wahl.py, test_kanal_geraete_leser.py und als Richter die Abnahme-Matrizen test_pv_achse_matrix.py und test_achsen_matrix.py. Laufzeit an der Zwölf-Jahres-Kopie: Werkzeug plans/ha-bauform-werkzeug/laufzeit-grundlinie.py (mit und ohne Kanäle, Bericht E4f).

Wechselrichter (Parent)
├── PV-Module (Child) [Pflicht]
└── DC-Speicher (Child) [Optional, für Hybrid-WR]
AC-Speicher [Eigenständig]
E-Auto [Eigenständig]
Wärmepumpe [Eigenständig]
Wallbox [Eigenständig]
Balkonkraftwerk [Eigenständig]
Sonstiges [Eigenständig]

PV-System ROI-Aggregation:

  • Wechselrichter + zugeordnete PV-Module + DC-Speicher = “PV-System”
  • ROI wird auf System-Ebene berechnet
  • Einsparungen werden proportional nach kWp verteilt

Gemessen 2026-08-13. Diese Unterscheidung hat schon zwei Abstürze verursacht (installationsdatum auf einer Investition gelesen — das Feld gibt es dort nicht) und zwei Anwender in eine Sackgasse geschickt. Sie steht deshalb hier.

eedc kennt zwei Datums-Ebenen, die verschiedene Fragen beantworten und nie gegeneinander getauscht werden dürfen:

FrageMaßgeblichOrt
Auswertungsfilter — zählt diese Investition in diesem Monat?aktiv · anschaffungsdatum · stilllegungsdatum der Investitionmodels/investition.py::ist_aktiv_im_zeitraum
Erwartungsrahmen — welcher Monat soll überhaupt erfasst sein?Anlage.installationsdatum, Fallback ältestes Anschaffungsdatum der Erzeuger, dann erste vorhandene Zeilecore/monats_luecken.py::ermittle_start_anker + Spiegel lib/monatsLuecken.ts

Anlage.installationsdatum filtert keine Auswertung. Es ist nachrichtlich (PDF-Berichte, Infothek-Vorbelegung, Backup), setzt den Erwartungsrahmen und geht in den Community-Hash ein. Eine ältere Forums-Formulierung („perspektivisch sollte das harmonisiert werden”, T77723 #520) klingt nach offener Schuld — die Frage ist entschieden, nur in die andere Richtung: die Investitions-Trias ist der alleinige Filter. Siehe feedback_anschaffungsdatum_grenze.

Warum die Erzeuger-Einschränkung im Erwartungsrahmen? Eine Monatszeile ist eine Aussage über die Anlage (Einspeisung/Netzbezug). Ein E-Auto von 2017 begründet keine Einspeisungszeile von 2017. Bis 2026-08-13 zog jede Investition den Anker zurück; ein Anwender hat daraufhin sein Fahrzeug umdatiert und dessen echte Anschaffungshistorie verloren. Fällt ein Erzeuger vor das Anlagendatum, meldet der Daten-Checker das als Diskrepanz — bei allen anderen Typen ist ein früheres Datum der Normalfall und wird bewusst nicht gemeldet.

Alle drei Sichten (Daten-Checker, „nächster offener Monat”, Tabellen-Färbung) lesen denselben Anker; test_monats_luecken_symmetrie.py hält Backend und Frontend deckungsgleich.


Die Prefix-Tabelle steht in DEVELOPMENT.md §API-Routen Übersicht — an einem Ort, erhoben aus backend/main.py. Die Kopie, die hier bis 2026-08-07 stand, war an sechs Stellen falsch (/api/daten-checker und /api/system-logs liegen beide unter /api/system, /api/data-import heißt /api/portal-import, mqtt_gateway und mqtt_presets liegen unter /api/live, ha_export hat gar kein eigenes Prefix).

Was architektonisch daran wichtig ist:

  • Ein Modulname sagt nichts über sein Prefix. Mehrere Module teilen sich eines (/api/live trägt fünf, /api/system zwei, /api/aussichten zwei), und drei Module hängen direkt unter /api (Monatsabschluss, Community, HA-Export). SoT sind die include_router-Aufrufe.
  • HA-only-Router werden bedingt eingehängt (HA_MODE): ha_integration, sensor_mapping, ha_statistics. ha_remote nicht — die Verbindung per Token gehört zum Standalone-Betrieb.
  • Einige System-Endpunkte liegen inline in main.py, nicht in einem Router: /api/health · /api/settings · /api/scheduler (+ /api/scheduler/monthly-snapshot) · /api/updates/check · /api/stats — dazu die SPA-Auslieferung als Catch-all.
  • ⚠ Die SPA beantwortet JEDEN Pfad mit HTTP 200 und HTML. Ein Statuscode belegt daher keinen Endpunkt; wer eine API prüft, prüft den Inhalt und macht die Gegenprobe mit einem frei erfundenen Pfad.
GET /api/cockpit/uebersicht/{anlage_id}?jahr=2025 # Haupt-Dashboard
GET /api/cockpit/prognose-vs-ist/{anlage_id} # PVGIS SOLL vs IST
GET /api/cockpit/nachhaltigkeit/{anlage_id} # CO2-Bilanz — die EINE CO₂-Quelle: Cockpit/Jahr + Auswertungen/CO₂ (ganze Historie, kein ?jahr=)
GET /api/cockpit/komponenten-zeitreihe/{anlage_id} # Komponenten-Zeitreihen
GET /api/cockpit/pv-strings/{anlage_id} # String-Vergleich (Jahres-Ansicht)
GET /api/cockpit/pv-strings-gesamtlaufzeit/{anlage_id} # String-Vergleich (Gesamtlaufzeit)

uebersicht liefert aggregierte Daten für alle Dashboard-Sektionen:

  • Energiebilanz (Erzeugung, Verbrauch, Einspeisung)
  • Effizienz (Autarkie, EV-Quote)
  • Komponenten-Status
  • Finanzen (inkl. jahres_rendite_prozent = Jahres-Ertrag / Investition), CO2

Datenquellen:

  • Monatsdaten: Einspeisung, Netzbezug
  • InvestitionMonatsdaten: Alle Komponenten-Details

Beides kommt aufbereitet aus services/monats_fakten/ (s. §7 „Lese-Schichtung”), nicht als Roh-Faltung im Endpoint — dort greifen auch die Zeitfilter. Der Umbau läuft sichtweise (S2–S6); bis dahin faltet ein Teil der Endpoints noch selbst.

GET /api/aussichten/kurzfristig/{anlage_id} # 7-Tage Wetterprognose
GET /api/aussichten/wetter/{anlage_id} # Wetterdaten für Prognose
GET /api/aussichten/langfristig/{anlage_id} # 12-Monats-Prognose (PVGIS)
GET /api/aussichten/trend/{anlage_id} # Trend-Analyse + Degradation
GET /api/aussichten/finanzen/{anlage_id} # Finanz-Prognose + Amortisation

4 Prognose-Tabs:

  • Kurzfristig: 7-Tage Wetterprognose (Open-Meteo) mit Erzeugungsschätzung
  • Langfristig: 12-Monats-Prognose basierend auf PVGIS und Performance-Ratio
  • Trend: Jahresvergleich, saisonale Muster, Degradationsberechnung
  • Finanzen: Amortisations-Fortschritt, Komponenten-Beiträge, Mehrkosten-Ansatz

ROI-Metrik: amortisations_fortschritt_prozent = Kumulierte Erträge / Investition (Unterscheidet sich von Cockpit jahres_rendite_prozent!)

GET /api/sensor-mapping/{anlage_id}/suggest # Vorschläge aus der HA-Energiekonfiguration (#197)

Seit 2026-08-13 ist das der einzige Endpunkt dieses Routers (Fund N-241). Fünf weitere — Mapping lesen · Sensor-Liste · speichern · löschen · Status — sind stillgelegt: ihre Oberfläche ist mit dem IA-V4-Flip gefallen, und POST schrieb weiter auf Anlage.sensor_mapping, ohne den Historie-Hinweis auszulösen, den die Datenquellen-Fläche seit Konzept #192 B zeigt. Die heutigen Wege sind GET/POST /api/datenquellen/{id}/felder (lesen · speichern) und GET /api/datenquellen/{id}/ha/sensoren (Sensor-Liste).

Der Router bleibt supervisor-gebunden gemountet (main.py, HA_INTEGRATION_AVAILABLE). ⚠ Diese Liste nannte bis dahin ein POST …/init-start-values, das es baumweit nie gab.

Mapping-Strategien:

  • sensor - Direkter HA-Sensor
  • kwp_verteilung - Anteilige Verteilung nach kWp
  • cop_berechnung - COP-basierte Berechnung (Wärmepumpe)
  • ev_quote - Eigenverbrauchsquote-Berechnung
  • manuell - Manuelle Eingabe im Wizard
  • keine - Nicht erfassen
GET /api/monatsabschluss/{anlage_id}/{jahr}/{monat} # Status + Vorschläge
POST /api/monatsabschluss/{anlage_id}/{jahr}/{monat} # Abschluss durchführen
GET /api/monatsabschluss/naechster/{anlage_id} # Nächster offener Monat
GET /api/monatsabschluss/historie/{anlage_id} # Letzte Abschlüsse

VorschlagService liefert intelligente Vorschläge:

  • vormonat (Konfidenz 80%) - Wert vom Vormonat
  • vorjahr (Konfidenz 70%) - Wert vom gleichen Monat im Vorjahr
  • berechnung (Konfidenz 60%) - COP/EV-Quote basierte Berechnung
  • durchschnitt (Konfidenz 50%) - Durchschnitt aller vorhandenen Werte
GET /api/scheduler # Scheduler-Status
POST /api/scheduler/monthly-snapshot # Manueller Monatswechsel-Trigger
GET /api/monatsdaten/aggregiert/{anlage_id}?jahr=2025[&inkl_ohne_zaehlerzeile=true]

Liefert Monatsdaten mit allen Komponenten-Summen:

  • Zählerwerte aus Monatsdaten (Einspeisung, Netzbezug)
  • PV-Erzeugung aggregiert aus allen PV-Modulen
  • Speicher-Daten (Ladung, Entladung)
  • WP/E-Auto/Wallbox-Daten
  • Berechnete Kennzahlen (Direktverbrauch, Eigenverbrauch, Autarkie)

Die Monatsgrößen kommen seit C1a aus der Monats-Fakten-Schicht (§7, ADR-002/P10).

inkl_ohne_zaehlerzeile (Default false, seit N-68) entscheidet über die Zeilenmenge, nicht über die Werte. Eine Monatsdaten-Zeile entsteht erst beim Monatsabschluss; die Schicht kennt einen gelaufenen Monat auch ohne sie (meta.hat_zaehlerzeile is False). Die Trennlinie verläuft zwischen zwei Konsumenten-Arten:

KonsumentFlagWarum
Datensatz-Liste (Auswertungen → Tabelle)auseine Zeile ohne Datensatz kann man weder bearbeiten noch löschen
Zeitreihe (Cockpit → Jahr: Verlauf, Rail, Vorjahr/Ø-Spalten)ansonst zeichnet sie weniger Monate, als die Kennzahl darüber zählt

Zeilen ohne Zählerzeile tragen id: null, keine Zählerwerte (Einspeisung/Netzbezug 0.0 aus der Schicht) und weder globalstrahlung_kwh_m2 noch sonnenstunden noch netzbezug_durchschnittspreis_cent — None, nicht 0. Die aus den InvestitionMonatsdaten gerechneten Größen sind vollständig. Monate ohne jede Spur liefert die Schicht ohnehin nicht, das Flag holt also keine Null-Zeilen ins Bild.

POST /api/import/csv/{anlage_id}
Content-Type: multipart/form-data
file: [CSV-Datei]

Verarbeitung:

  1. CSV parsen (flexible Spalten-Erkennung)
  2. Basis-Felder → Monatsdaten-Tabelle
  3. Investitions-Felder → InvestitionMonatsdaten-Tabelle
  4. Duplikate: Upsert (überschreiben)
  5. flag_modified() für JSON-Felder

Plausibilitätsprüfungen:

  • Legacy-Spalten (PV_Erzeugung_kWh, Batterie_*_kWh) werden validiert
  • Fehler wenn NUR Legacy und PV-Module/Speicher existieren
  • Fehler bei Mismatch Legacy vs. Summe Komponenten-Werte
  • Warnung wenn redundant (±0.5 kWh Toleranz)
  • Negative Werte werden blockiert
  • Plausibilitätswarnungen (Sonnenstunden > 400h, Globalstrahlung > 250)
GET /api/import/export/{anlage_id} # Vollständiger JSON-Export
GET /api/import/template/{anlage_id} # CSV-Template herunterladen
GET /api/import/template/{anlage_id}/download # CSV-Template Download
GET /api/import/pdf/{anlage_id} # PDF-Export
POST /api/import/demo # Demo-Daten erstellen
DELETE /api/import/demo # Demo-Daten löschen

Export - Vollständige Anlage mit allen verknüpften Daten:

  • Anlage-Stammdaten (inkl. versorger_daten, mastr_id, wetter_provider)
  • sensor_mapping - HA Sensor-Zuordnungen
  • Strompreise
  • Investitionen (hierarchisch mit Children)
  • Monatsdaten mit InvestitionMonatsdaten (inkl. durchschnittstemperatur, sonderkosten)
  • PVGIS-Prognosen

Import - Restore aus JSON-Export:

  • Erstellt neue Anlage (oder überschreibt bei gleichem Namen)
  • sensor_mapping wird importiert, aber mqtt_setup_complete=false
  • Rückwärtskompatibel mit Export-Version 1.0
# Typisches Schema-Pattern
class MonatsdatenCreate(BaseModel):
anlage_id: int
jahr: int
monat: int
einspeisung_kwh: float
netzbezug_kwh: float
investitions_daten: Optional[Dict[int, Dict]] = None
class MonatsdatenResponse(MonatsdatenCreate):
id: int
direktverbrauch_kwh: Optional[float]
eigenverbrauch_kwh: Optional[float]
# ... berechnete Felder

Verbindlich für die Informationsarchitektur ist KONZEPT-IA-V4.md (Achsen, Invarianten I1–I16, Redirect-Mechanik), für die Darstellung KONZEPT-STYLE-GUIDE.md (Regel 0/0a). Dieses Kapitel beschreibt, wie das im Code liegt — es definiert keine Regeln daneben.

Seit dem IA-V4-Flip (v4.0.0) ist die Oberfläche nach Fragen geschnitten, nicht nach Geräten:

AchseFrageSektionUnterteilung
ZeitWann?CockpitLive · Tag · Monat · Jahr · Aussicht
GerätWas?Komponentenje Typ: Status → Verlauf → Vergleich → Wirtschaftlichkeit
MethodeWie?AuswertungenFinanzen · ROI · Prognose-vs-IST · CO₂ · Tabelle
—VergleichCommunityÜbersicht · PV-Ertrag · Komponenten · Regional · Trends · Statistiken
—EinrichtungEinstellungenKachel-Übersicht je Kategorie

Eine zeitbezogene Auswertung gehört damit ins Cockpit, eine über die Lebensdauer einer Komponente in den Hub — diese Zuordnung ist die häufigste Entscheidung beim Bau einer neuen Sicht.

HashRouter (nicht BrowserRouter), weil der HA-Ingress-Pfad dynamisch ist: URLs erscheinen als /#/cockpit/live. Die V4-Routen sind prefix-frei — der frühere /v4-Präfix ist mit dem Flip gefallen.

/ → Navigate → /cockpit/live
├── cockpit → Navigate → /cockpit/live
├── cockpit/:zeit → CockpitV4 (live · tag · monat · jahr · aussicht)
├── komponenten → KomponentenV4 (Auswahl)
├── komponenten/:typ → KomponentenV4 (pv-anlage · speicher · bkw · waermepumpe ·
│ wallbox · e-auto · sonstiges)
├── auswertungen → Navigate → /auswertungen/finanzen
├── auswertungen/:sub → AuswertungenV4 (finanzen · roi · prognose · co2 · tabelle)
├── community → Navigate → /community/uebersicht
├── community/:sub → CommunityV4 (uebersicht · pv-ertrag · komponenten ·
│ regional · trends · statistiken)
├── hilfe → HilfeV4
├── einstellungen → Navigate → /einstellungen/stammdaten
└── einstellungen/:kategorie → EinstellungenV4 (stammdaten · komponenten · infothek · daten ·
integration · datenquellen · system)

Alle Alt-Pfade werden umgeleitet, keiner läuft in ein 404. SoT dafür ist src/routes/routeManifest.ts:

  • LEGACY_REDIRECTS — die Alt→Neu-Tabelle, von App.tsx als <Navigate replace> gerendert. Sie trägt drei Klassen: V3-Top-Level (live → /cockpit/live), Achsenwechsel der Gerätepfade (cockpit/speicher → /komponenten/speicher — Geräte sind keine Zeit-Frage mehr) und die Re-Kategorisierung der Einstellungs-Routen (einstellungen/sensor-mapping → /einstellungen/datenquellen).
  • REAL_ROUTE_PATHS — das Inventar der echten Routen; es muss mit den <Route>-Pfaden in App.tsx synchron bleiben.
  • src/routes/redirects.test.tsx prüft beides maschinell: jeder Alt-Pfad landet auf einer echten Route, ohne Redirect-Kette.

Dazu drei Sonderfälle in App.tsx: Splat-Fänger für gelöschte dynamische Alt-Sektionen (aussichten/*, monatsabschluss/*) und eine Versicherung für /v4/*-Bookmarks.

main.tsx
├── ThemeProvider # Light/Dark (context/ThemeContext)
└── AppWithSetup # Ersteinrichtung vor der App
└── App.tsx
└── HashRouter
└── AppErrorBoundary # um den GANZEN Routenbaum (#207)
└── Suspense # LayoutV4 → Cockpit → Live lazy, für langsame Zugänge
└── LayoutV4 (v4/)
├── AnlagenSelektor · ReloadButton
├── GlobalStatusProvider + StatusFusszeile (v4/status/)
└── <Outlet />
└── CockpitV4 · KomponentenV4 · AuswertungenV4 ·
CommunityV4 · EinstellungenV4 · HilfeV4

Eine V4-Sicht ist eine Liste von Blöcken, nicht ein Seitenlayout. Jeder Block ist verschiebbar, fokussierbar (⤢) und parkbar:

BausteinOrtAufgabe
BlockShellcomponents/blocks/Rahmen, Titel, Fokus-Knopf, Parken-Geste
FokusVollbild · FokusKachelcomponents/blocks/ein geteiltes Overlay für alle Sichten
BlockStackSkeletoncomponents/blocks/Ladezustand als Block-Stapel statt Spinner
KpiStrip · VerteilungsBalken · HerkunftZeilecomponents/blocks/wiederkehrende Block-Inhalte
Parkbar · GeparktBlock · ParkContext · ParkFusscomponents/park/Park-Zustand je Sicht (persistiert)
useSectionOrderhooks/Reihenfolge der Blöcke je Sicht (persistiert)

Park-Doktrin (I3): eine Parkbar trägt eine atomare Anzeige. Wer einen Block parkt, parkt genau das, was er sieht — nicht eine Gruppe, aus der später etwas Unerwartetes wieder auftaucht.

Kein globaler Store. Vier Mechanismen, jeder mit klarer Zuständigkeit:

  1. useApiData(fetcher, deps, opts) für Server-State — inklusive SWR-Sicht-Cache (opt-in über swrKey). Ohne ihn fetchte jede Sicht beim Tab-Wechsel neu und zeigte ein Skeleton, obwohl die Daten Sekunden alt waren (#218). Mit swrKey stehen beim Remount die alten Daten sofort, still revalidiert wird trotzdem (reloading). Der Cache ist ein Modul-Singleton mit LRU-Grenze und stirbt mit dem Browser-Tab; Tests leeren ihn über _clearSwrCacheForTests().
  2. Context für Theme (ThemeContext) und den globalen Status (GlobalStatusProvider).
  3. localStorage für Präferenzen: Anlagen-Auswahl, Block-Reihenfolge, Park-Zustand, Spalten-Toggles, Wizard-Fortschritt.
  4. URL für alles, was ein Link tragen muss: Sektion, Zeitraum-Achse, Gerätetyp, Unter-Sicht.
HookZweck
useApiDataasync Fetch mit Loading/Error + SWR-Sicht-Cache
useSelectedAnlageAnlagen-Auswahl mit Auto-Select und Persistierung
useAnlagen · useInvestitionen · useMonatsdaten · useStrompreiseBestandsdaten
useYearSelectionverfügbare Jahre + Auswahl
useSectionOrderBlock-Reihenfolge je Sicht
useEinstellungenStatusStatus-Ampeln der Einstellungs-Kacheln
useHelpKatalog · useFeldHinweiseIn-App-Hilfe und Feld-Hinweise
useHAAvailableHA-only-Funktionen ausblenden, wenn kein HA da ist
useLegendenToggle · useSchmaleAchse · useScrollErhalt · useTouchTitleTooltipAnzeige-Verhalten (auch mobil)
useSetupWizardErsteinrichtung

Eine Komponenten-Klasse = EINE Komponente (I12). Eine zweite Fassung neben einer bestehenden ist ein Regelbruch, nicht eine Variante:

VerzeichnisInhalt
ui/Card · Button · Modal · Table/WerteTabelle · ChartTooltip · Badge · CsvExport …
blocks/ · park/Block-Modell und Park-Mechanik (siehe oben)
charts/Chart-Bausteine (Achsen, Legende) — Konventionen im Style-Guide
forms/Formular-Controls; Roh-Controls sind gewächtert (check:roh-controls, check:form-controls)
live/EnergieFluss (animiertes SVG), EnergieBilanz, WetterWidget …
monatsabschluss/das Monatsabschluss-Formular (seit v4.0.0 ein Formular statt 7 Schritten)
connector/ · import/ · setup-wizard/Einrichtung und Import (sensor-mapping/ fiel mit der V3-Bereinigung 2026-08-13)
gerätespezifischpv/ · speicher/ · waermepumpe/ · wallbox/ · eauto/ · balkonkraftwerk/
fachlichfinanzen/ · roi/ · aussicht/ · prognose/ · tag/ · werte/ · infothek/ · repair/
ModulInhalt
colors.tsFarb-SoT. Eine Datenrolle = eine Farbe; Inline-Hex außerhalb ist gewächtert (check:design)
einheiten.tsZahl-/Einheiten-Formatierung (kWh, €, %, CO₂) — deutsche Konvention, % mit Leerzeichen
datum.tsheuteIso · toIsoDatum · verschiebeIsoTage — lokale Uhr, nie UTC (check:datum-utc)
monatsLuecken.ts„welcher Monat ist offen” inkl. Binnen-Lücken
chartAchse.ts · blockStyle.ts · komponentenStyle.tsAchsen- und Stil-Ableitungen
investitionAktiv.tsdie drei Achsen von „aktiv” (Flag · Anschaffung · Stilllegung)
investitionParameter.tsParameter-Keys gemeinsam mit dem Backend (core/investition_parameter.py)
erzeugerSpalten.ts · pvHerkunft.ts · erfassungZustand.ts · sollErfuellung.tsTabellen- und Zustands-Ableitungen
stundenSlot.tsSlot-Konvention im Client (Backward, wie core/berechnungen/slot_konvention.py)
prognoseAnzeige.ts · prognoseHinweise.tsPrognose-Darstellung und ihre Hinweistexte
calculations.ts · constants.ts · fieldDefinitions.ts · flags.ts · download.tsRest
  • Keine CO₂-Menge konstruieren — sie kommt aus /cockpit/nachhaltigkeit (ADR-001/DI-2, Wächter check:co2-roh).
  • Keine Nennleistung aus der Rohspalte rechnen — Anzeige und Rechnung lesen leistung_kwp_effektiv aus der Response; die Rohspalte gehört Formularen (P3-a, Wächter check:kennwert-roh).
  • Keine Monatsgröße selbst falten — das ist Aufgabe der Monats-Fakten im Backend (P10).

Zwischen den Tabellen und den auswertenden Endpoints liegen drei Schichten, und die Reihenfolge ist keine Geschmacksfrage — sie ist das Ergebnis von zwei Drift-Inventuren (#326 und der Inventur der Lese-Sichten vom 2026-07-31):

Monatsdaten · InvestitionMonatsdaten · Strompreise · TagesZusammenfassung
│
▼ services/monats_fakten/ ← Aufbereitung (ADR-002/P10)
MonatsFakt je (jahr, monat): zaehler · erzeugung · bkw · speicher · emob ·
wp · sonstiges · tarif · eeg · kennzahlen · meta
Hier — und nur hier — greifen die Zeitfilter (aktiv · Anschaffung ·
Stilllegung) und der Dienstwagen-Filter. Nutzt intern pv_monatswerte.py (P7).
│
▼ core/berechnungen/ ← Formeln (ADR-001)
berechne_verbrauchs_kennzahlen · berechne_finanz_aggregat · imd_typ_beitrag ·
bkw_finanz_beitrag · erzeugung_hinter_zaehler_kwh ·
berechne_dienstliche_ladekosten · kapitalrechnung (Nenner + Dauer-Annahme) ·
ertrag_zerlegung (Fortschritt je ROI-Zeile) … (DB-frei, rein)
│
▼ api/routes/… ← Darstellung
Cockpit · Aussichten · HA-Export · PDF · Community

Die frühere Aufteilung — „Monatsdaten und InvestitionMonatsdaten sind die Quellen, jede Read-Site aggregiert daraus selbst” — gilt nicht mehr. Genau sie war die Fehlerursache: der Berechnungs-Layer war fehlerfrei, aber jede Sicht faltete die Rohdaten anders, und dabei fiel jedes Mal etwas anderes weg (V2H, der Erzeuger hinter dem Zähler, der Aggregat-Fallback, der Monatstarif, der Dienstwagen-Filter). Bei einer Anlage, die nur das Anlagen-Aggregat pflegt, standen für denselben Monat 1.000 kWh und 0 kWh auf zwei Seiten derselben Anwendung.

SchichtDarfDarf nicht
services/monats_fakten/laden, filtern, kanonisch auflösen, Layer-Helfer rufenselbst rechnen (das wäre eine Formel-Duplikation)
core/berechnungen/rechneneine Session sehen
api/routes/…darstellen, Zeiträume wählenInvestitionMonatsdaten selbst laden und falten (P10)

Ausgenommen von P10 sind die Schreib-, Import- und Checker-Pfade — sie schreiben oder prüfen die Zeilen, sie leiten nichts ab. Details: docs/KONZEPT-MONATS-FAKTEN.md, docs/ADR-002-WURZELMUSTER.md (P7 · P8 · P9 · P10), docs/ADR-001-BERECHNUNGS-LAYER.md.

Migrationsstand: Die Schicht steht seit S1 mit ihren Einheitstests; die Sichten sind in den Schritten S2–S6 nacheinander umgehängt worden (Reihenfolge und Beweis-Fixture je Schritt in KONZEPT-MONATS-FAKTEN.md §5/§10). Der Bauplan ist mit S6 abgearbeitet — alle sechs Schritte sind gebaut. Seit S5 ist P10 baumweit gewächtert (test_wurzelmuster_konformitaet.py::test_p10_*), nicht mehr nur durch Regressionstests gedeckt. Die danach übrig gebliebene Restschuld ist in vier Nachträgen (C1a–C1d) abgearbeitet und steht seit C1d (2026-08-04) auf 0 — anlagenweit faltet keine Sicht mehr selbst.

Umgehängt (S2, 2026-07-31): api/routes/aussichten/finanzen.py (Finanz-Prognose; die Monats-Fakten lädt seit 18.09.2026 ihre Phase finanz_eingaenge.py), services/pdf/builders/jahresbericht.py und api/routes/investitionen/crud.py (ROI-Dashboard, seit 18.09.2026 investitionen/roi.py) — Befund F-5. Der PDF-Builder lädt InvestitionMonatsdaten seither gar nicht mehr selbst.

Umgehängt (S3, 2026-07-31): cockpit/nachhaltigkeit.py (CO₂-Zeitreihe) und cockpit/social.py (geteilter Monatstext) — Befund F-1. Beide hatten keinen einzigen Zeilen-SoT benutzt und die Eigenverbrauchs-Formel selbst nachgebaut; CO₂ kommt jetzt zusätzlich aus berechne_co2_bilanz (ADR-001/DI-2) statt aus drei lokalen Formeln. Damit ist auch die P7-Ausnahme cockpit/social.py::md gestrichen. Nachtrag (Paket B, 2026-07-31): cockpit/social.py ist danach ganz zurückgebaut worden — die Oberfläche dazu ist beim IA-V4-Flip entfallen, der Endpoint hatte keinen Konsumenten mehr. Von den beiden S3-Sichten bleibt die CO₂-Zeitreihe. Nicht betroffen ist das Community-Teilen (api/routes/community.py) — eine andere Funktion. Nachtrag (Paket B’, 2026-07-31): die CO₂-Zeitreihe hat jetzt auch einen Konsumenten — bis dahin war sie die tote Hälfte des Paares (getNachhaltigkeit stand im api-Client, aber keine Sicht rief es). Sie hängt im Cockpit/Jahr als Block „CO₂-Bilanz” (v4/JahrCo2Chart.tsx), nicht unter Auswertungen: sie ist eine zeitbezogene Verlaufssicht, während die CO₂-Amortisation (AuswertungenCo2V4.tsx, getCO2Amortisation) die Lebensdauer-Frage beantwortet. Der Endpoint bleibt ohne ?jahr= — das Jahr filtert die Sicht. Nachtrag (F-6/N-21, 2026-07-31): die Zeitreihe hat einen zweiten Konsumenten bekommen — Auswertungen → CO₂ (v4/AuswertungenCo2V4.tsx) rechnete bis dahin Erzeugung × 0,38 im Client und stand damit als zweite Zahl neben dem neuen Cockpit-Block; sie liest ihn jetzt über useAuswertungBasis().co2 (EIN Abruf, geteilt mit der Werte-Tabelle). Dasselbe Spiegelbild im Backend war services/energie_profil/tage_werte.py — auch dort jetzt der Kanon, allerdings bewusst nur der PV-Anteil (co2_pv_kg), weil WP-Wärme und E-Mob-Kilometer auf Tagesebene nicht gemessen werden. Client-Wächter: npm run check:co2-roh (Baseline 0).

Umgehängt (S4, 2026-07-31): cockpit/uebersicht.py und ha_export/anlage_energie.py::monatsfakten_und_energie (bis 18.09.2026 ha_export.py::calculate_anlage_sensors) — der Schritt ohne Inventur-Befund. Beide rechneten richtig und wurden trotzdem umgehängt, weil eine selbst faltende Sicht die nächste Drift-Quelle ist. Der Beweis ist entsprechend negativ (Vier-Wege- und CO₂-Symmetrie unverändert grün, Antworten gegen die Demo-DB wertgleich), plus eine gemessene Ladezeit: Cockpit/Übersicht 83 → 60 ms, HA-Export 133 → 115 ms (warm, Median über 10 Läufe) — ein Tarif-Cache statt zwei, §51 als Bulk-Query, vier IMD-Queries weniger. Drei Ränder haben sich dabei doch bewegt, alle drei waren vorher unsichtbar: der Jahres-Filter des Cockpits erfasste lade_pv_je_monat nicht, der HA-Export blendete stillgelegte Komponenten rückwirkend aus (aktiv_jetzt() als Vorfilter über der Historie) und filterte den Dienstwagen nicht aus der V2H-Bilanz.

Umgehängt (S5, 2026-07-31): investitionen/dashboards.py (seit 18.09.2026 dashboard_<typ>.py je Dashboard) — Befunde F-4 (BKW-Wirtschaftlichkeit: 30-ct-Query-Default und bewerteter gemessener Eigenverbrauch → im Normalfall 0 € im Hub) und F-7 (die Datei enthielt keinen einzigen ist_dienstlich-Aufruf). Dazu zwei Funde derselben F-5-Klasse, die vor dem Scharfstellen fallen mussten, weil der Wächter sie sonst gemeldet hätte: die beiden Performance-Ratio-Pfade in aussichten.py (N-1) und der Prognose-vs-IST-Vergleich in cockpit/prognose.py (N-14, in der Inventur nicht erhoben). Neue Layer-Formel bkw_eigenverbrauch_anteil (core/berechnungen/bkw_finanz.py): der Eigenverbrauchs-Anteil eines Balkonkraftwerks, anteilig an der Erzeugung hinter dem Zähler — und ausdrücklich nicht bewertbar, wo mangels Zählerzeile keine Hausbilanz existiert (P4).

Umgehängt (S6, 2026-07-31): services/community_service.py::prepare_community_data — die letzte Sicht des Bauplans und der einzige befristete Eintrag in P10_NOCH_NICHT_MIGRIERT. Sie verlor drei Achsen: die P7-Auflösung der PV (F-5 — eine Anlage, die ihre Erzeugung als Anlagen-Aggregat pflegt, lieferte eine leere Monatsliste, und /community/share bricht darauf mit HTTP 400 ab: diese Anlagen konnten am Benchmark gar nicht teilnehmen), V2H und den Erzeuger hinter dem Zähler in der Autarkie (F-1 — der Server bekam 85,7 %, wo das Cockpit derselben Anlage 90,9 % zeigte) und den Dienstwagen-Filter (km, Ladung und V2H eines dienstlichen Fahrzeugs gingen in den öffentlichen Benchmark ein). ertrag_kwh bleibt bewusst die PV-Achse ohne sonstigen Erzeuger — der Server bildet daraus den spezifischen Ertrag je kWp. Cross-Repo (Konzept §11): das Datenmodell von eedc-community blieb strukturell unverändert (der Payload validiert unverändert gegen backend/schemas.py); geändert hat sich die Bedeutung dreier Feldgruppen, die deshalb im selben Paket als Vertrag in den Docstring von MonatswertInput gewandert ist.

Der Wächter ist funktions-granular, nicht modul-granular: eine ausgenommene Datei ist nicht als Ganzes freigestellt, eine neue Funktion darin ist ein Treffer. Seine Ausnahmen stehen in drei getrennten Kategorien, weil zwei davon Schuld sind und nicht Freispruch:

Umgehängt (C1a, 2026-08-03): monatsdaten.py::list_monatsdaten_aggregiert (N-15) — die sichtbarste der Restschuld-Sichten, denn sie speist Auswertungen → Tabelle und Cockpit → Jahr. Zwei Zahlen bewegen sich: die E-Mob-Ladung wird über summiere_emob_quelle abgeleitet statt roh addiert (wer nur Gesamt-Ladung und PV-Anteil pflegt, verlor die Netzladung still), und die §51-Menge ist ohne Mitschrift None statt 0,0. Der BKW-Akku zählt hier bewusst weiter in die anlagenweite Speicher-Summe (N-28, Altbestand) — still zu vereinheitlichen hätte die Summe genau bei denen gesenkt, die noch auf dem alten Weg pflegen. Zwei additive Erweiterungen der Schicht waren erzwungen: MetaFakten.typen_mit_zeile und SonstigesFakten.anlage_*_euro.

Umgehängt (C1b, 2026-08-03): cockpit/komponenten.py::get_komponenten_zeitreihe (N-17) — Auswertungen → Komponenten. Der Schritt war als deckungsgleich geplant und ist es auch: sechs der sieben neuen Tests sind auch gegen die Fassung vor dem Umbau grün, und die vorhandenen liefen ohne Anpassung durch. Die Route lädt keine InvestitionMonatsdaten und keine Monatsdaten mehr; §51 kostet statt N Rundreisen nur noch eine, und der Tarif-Stichtag löst je Monat einmal auf (geteilter Cache). Eine sichtbare Änderung: ein Monat, dessen einzige Spur ein Dienstwagen ist, erzeugt keine Zeile aus lauter Nullen mehr — der alte Loop legte den Monatseintrag an, bevor er den Dienstwagen übersprang. Seine Finanz-Positionen zählen unverändert weiter und tragen den Monat (#310): er fällt aus dem Energie-Pool, nicht aus der Buchhaltung.

Umgehängt (C1c, 2026-08-03): aktueller_monat.py — Cockpit → Monat. Die einzige Etappe mit einer echten Abgrenzung: die Route mischt vier Datenquellen (saved · connector · mqtt_energy · ha_stats) nach Präzedenz, die Schicht kennt nur die erste. Umgezogen ist deshalb genau der DB-Zweig — _collect_saved_data (der als „Formular füllen” fehlklassifizierte fünfte Eintrag, N-98), _load_vorjahr (N-16a) und die Sonstige-Positionen des Monatsberichts. Die Präzedenz-Regeln, die > 0-Gates beim Setzen und die Teilzeitraum-Logik (#361) sind unberührt. Sichtbar sind fünf Zahlen: die PV des Monatsberichts ist P7-aufgelöst (F-5 an der Sicht, die S1–S6 nicht angefasst haben), der BKW-Eigenverbrauch fällt ohne Messwert nicht mehr auf die volle Erzeugung zurück (D5-Quirk), und der Vorjahresvergleich verliert die drei als „D6 / IST-Stand erhalten” konservierten Divergenzen: PV ohne P7-Auflösung, Eigenverbrauch/Autarkie ohne V2H, E-Mob als max() je Feld statt der kanonischen Trias (#262 — öffentlich geladener Strom zählte im Vorjahr als Heimladung, im laufenden Monat nicht).

Umgehängt (C1d, 2026-08-04): der Komponenten-Detailblock von get_aktueller_monat (N-107) — die letzte anlagenweite Faltung des Baums. Sie hatte zwei Fehler, die die Schicht nicht hat, und beide waren am Datenbestand messbar:

  • E-Auto und Wallbox wurden roh addiert. Beide messen denselben Fluss aus zwei Perspektiven; wo beide gepflegt sind, stand der Netzanteil doppelt — in der Kachel Cockpit → Monat, im T-Konto (dort × Arbeitspreis, also geldwirksam) und in der Jahressumme. Am Demo-Bestand über 25 Monate 5.976 statt 3.831 kWh (+56 %), betroffen war jeder Monat mit Ladung. Die Route hatte die Doppelzählung an anderer Stelle (typ_aggregation) längst benannt und vermieden — im Detailblock nicht.
  • Kein Laufzeit-Filter. Die Batch nahm jede IMD-Zeile ihres Typs, auch aus Monaten vor der Anschaffung (#236-Klasse, die alle anderen Read-Sites seit v3.29 hinter sich haben). Am Demo-Bestand trug die Route vier Monate lang Wärme einer Wärmepumpe, die es noch nicht gab — 3.400 kWh davon im Jahresaggregat 2024, weil JahrAggregat die Monate summiert.

Dafür sind der Schicht die vier Größen additiv nachgezogen worden, die ihr fehlten (SonstigesFakten: Eigenverbrauch, Einspeisung, Bezug PV, Bezug Netz) — samt je_geraet, damit auch die Pro-Gerät-Darstellung nicht mehr selbst faltet. Sie sind kategorie-bewusst aufgelöst wie Erzeugung/Verbrauch: ein Erzeuger trägt keinen Netzbezug, ein Verbraucher speist nicht ein. Gemessen wurde der ganze Umbau als Dump gegen den Vorstand: die einzigen Abweichungen sind die beiden oben — Speicher, Ø-Ladepreis, BKW, alle sechs Sonstiges-Mengen und die Geräteliste sind bitgleich.

KategorieBedeutungStand nach C1d
P10_SCHREIBEN_IMPORT_CHECKERschreibt, prüft oder reicht durch — leitet nichts ab; auf Dauer legitim15 Funktionen
P10_PER_INVESTITIONAggregat je Gerät; die Schicht hat dafür keine Sicht (Register N-2)13 Funktionen
P10_NOCH_NICHT_MIGRIERTfaltet eine anlagenweite Monatszeile selbst — die Klasse, die P10 schließen soll0 — getilgt

Die anlagenweite Restschuld ist getilgt (5 nach S5 → 4 nach S6 → 3 nach C1a → 2 nach C1b → 1 nach C1c → 0 nach C1d). test_p10_offene_schuld_waechst_nicht deckelt sie nicht mehr, sondern hält die Liste leer: ein neuer Eintrag eröffnet die Schuld neu, statt sie unter einer Obergrenze wachsen zu lassen. get_aktueller_monat steht seither in P10_PER_INVESTITION — selbst geladen wird dort nur noch die Zuordnung inv → verbrauch_daten für die Financial-Zeile je Investition. Teilmigriert bleiben aussichten.py und ha_export.py: ihre Monatsgrößen kommen aus der Schicht, die per-Investition-Aggregate (WP-Alternativkosten, E-Auto je Fahrzeug, calculate_investition_sensors) falten sie weiter selbst.

Nachtrag 2026-08-03 (N-121) — die Grundgesamtheit kann jetzt auch die Tagesebene: Die Schicht kannte einen Monat nur mit DB-Spur (Monatsdaten oder InvestitionMonatsdaten); der Docstring sagte es offen („Monate ohne jede Spur fehlen”). Weil es keinen automatischen Monatsabschluss gibt — scheduler.py::monthly_snapshot_job setzt nur einen Log-Zeitstempel —, fehlte dem laufenden Monat die Spur immer, und dem Vormonat bis zum Abschluss. In Cockpit → Jahr stand deshalb eine Kopfzahl über acht Monate über einem Verlauf mit sechs Balken; N-68 hatte nur die Zählerzeilen-Bedingung aufgehoben, nicht die Grundgesamtheit. lade_monats_fakten(..., inkl_nur_tageswerte=True) (Default aus, angefragt allein von der Jahres-Zeitreihe) nimmt solche Monate auf und füllt die Lücken der übrigen feldgruppen-weise — Präzedenz wie P7: was in der DB steht, gewinnt. Gefüllt werden nur Zähler, PV/BKW und Speicher-Mengen; die Netzladung bleibt außen vor, weil sie eine Preis-Aussage ist. Die Herkunft steht in MetaFakten.tageswert_gruppen und geht als aus_tageswerten bis in die Antwort — eine Sicht muss nicht aus einer 0 raten (P4). Es entsteht keine zweite Tag→Monat-Faltung: gefaltet wird mit dem Layer-Helfer bilanz_aus_stundenrows über die Stunden des Monats (Σ ist assoziativ, additive Symmetrie zum Tag bleibt) plus summe_pv_anlage_kwh/summe_bkw_kwh auf komponenten_kwh; rollup_month ist kein Konkurrent, es faltet fünf andere Felder und schreibt sie. Null zusätzliche HA-Last — die Quelle liegt lokal, was die mit L-1 gewonnene Entlastung unangetastet lässt. Vor dem Bau gemessen (Anlage 1, v4.0.8): Δ ≤ 0,8 kWh über sechs Vergleichsmonate, Juli/August deckungsgleich mit der HA-Statistics-Kaskade der Kachel. Lesepfad und Grenzen: services/energie_profil/monats_aus_tagen.py, Konzept-Nachtrag in KONZEPT-MONATS-FAKTEN §4.

Nachtrag 2026-07-31 (Nebenfunde-Runde, Paket A): Die dienstlichen Ladekosten stehen nicht mehr auf dieser Liste. Sie waren der letzte Posten, den aussichten.py je Investition selbst faltete — und er driftete gleich zweifach: der Netzanteil lief dort über den allgemeinen Arbeitspreis statt über den Wallbox-Tarif (N-12), und der HA-Export zog ihn gar nicht ab (N-13). Beide Sichten beziehen die Mengen jetzt aus MonatsFakt.emob.dienstlich_* und bewerten sie über die neue Layer-Formel berechne_dienstliche_ladekosten (core/berechnungen/dienstliche_ladekosten.py, ADR-001). Dabei ist eine dritte, größere Sache mitgefallen — N-18: der PV-Anteil wurde zur Einspeisevergütung abgezogen, während die EV-Ersparnis dieselben kWh zum Netzbezugspreis gutschrieb, sodass ein Dienstwagen der Anlage netto +22 ct je verschenkter kWh einbrachte. Der Abzug bewertet den PV-Anteil seither mit dem Netzbezugspreis; die Energiebilanz (Eigenverbrauch, Quote, Autarkie) bleibt unverändert, korrigiert wurde ausschließlich die Bewertung. Herleitung: BERECHNUNGEN §3.10.

Dateien:

  • backend/services/wetter_service.py – Multi-Provider Orchestrierung
  • backend/services/brightsky_service.py – DWD-Daten via Bright Sky API
  • backend/services/solar_forecast_service.py – Open-Meteo Solar mit GTI

Funktion: Wetterdaten für Globalstrahlung und Sonnenstunden aus verschiedenen Quellen.

Verfügbare Provider:

ProviderRegionBeschreibung
auto (Standard)-Automatische Auswahl: Anlage.standort_land entscheidet, ersatzweise die Koordinaten-Box (nutze_brightsky, #386)
brightskyDeutschlandDWD-Daten via Bright Sky REST API (höchste Qualität). Stationsmessung — liefert ein Monat keinen einzigen Strahlungstag, verwirft der Orchestrator das Ergebnis und geht weiter (sonst still 0,0 kWh/m²)
open-meteoWeltweitOpen-Meteo Archive API
open-meteo-solarWeltweitOpen-Meteo Solar mit GTI für geneigte Module

Fallback-Kette:

  1. Gewählter Provider
  2. Alternative (z.B. Open-Meteo wenn Bright Sky fehlschlägt)
  3. PVGIS TMY (langjährige Durchschnittswerte)
  4. Statische Defaults (Mitteleuropa-Durchschnitt)

API-Endpoints:

GET /api/wetter/monat/{anlage_id}/{jahr}/{monat}?provider=auto
GET /api/wetter/monat/koordinaten/{lat}/{lon}/{jahr}/{monat} # Direkt per Koordinaten
GET /api/wetter/provider/{anlage_id} # Verfügbare Provider
GET /api/wetter/vergleich/{anlage_id}/{jahr}/{monat} # Provider-Vergleich
GET /api/solar-prognose/{anlage_id}?tage=7 # GTI-basierte PV-Prognose

GTI (Global Tilted Irradiance): Open-Meteo Solar berechnet Strahlung für geneigte PV-Module:

  • Neigung und Ausrichtung aus PV-Modul-Konfiguration
  • Temperaturkorrektur (Wirkungsgradminderung bei Hitze)
  • Multi-String-Unterstützung für verschiedene Ausrichtungen

Datei: backend/services/ha_sensors_export.py

Funktion: Definition und Berechnung aller exportierbaren KPIs.

Sensor-Kategorien:

  • ANLAGE_SENSOREN – PV-Gesamt
  • INVESTITION_SENSOREN – ROI, Amortisation
  • E_AUTO_SENSOREN – Mobilität
  • WAERMEPUMPE_SENSOREN – Wärme
  • SPEICHER_SENSOREN – Batterie

Sensor-Definition:

SensorDefinition(
key="pv_erzeugung_kwh",
name="PV Erzeugung",
unit="kWh",
icon="mdi:solar-power",
device_class="energy",
state_class="total_increasing",
formel="Σ(PV-Module.pv_erzeugung_kwh)"
)

⚠ device_class nur setzen, wenn HAs Vertrag es hergibt. Das Beispiel oben ist ein echter Zähler, deshalb energy + total_increasing. Für energy lässt HA ausschließlich total/total_increasing zu, für monetary nur total; jede andere Kombination schließt den Sensor von der Langzeitstatistik aus, ohne dass es an der Anzeige auffällt (F-63). Ein Wert, der kein Zähler ist — Prognose, Rate, Momentanwert —, bekommt kein device_class und state_class="measurement". Gewächtert: backend/tests/test_ha_export_sensor_klassen_vertrag.py.

Datei: backend/services/mqtt_client.py

Funktion: Publizieren von Sensoren via MQTT Auto-Discovery.

Methoden:

  • publish_sensor_discovery() / publish_sensor_value() - Discovery-Config und Wert je Sensor
  • publish_all_sensors() - beides über eine Verbindung für eine ganze Sensorliste
  • remove_sensors() - Rücknahme bestandsgetrieben: zusätzlich zu den Topics der heutigen Definitionen fragt sie über verwaiste_topics() den Broker, was unter dem Präfix dieser Anlage sonst noch retained liegt, und räumt es mit (altlast_topics in der Antwort) — außer dem Zweig …/monatsdaten/ (finale Monatsdaten, keine Sensoren). Damit fallen auch Entitäten früherer Fassungen weg — konkret die number.eedc_*_mwd_*_start aus der Zeit vor 77c6e211 (13.03.2026). ⛔ Nur eigene Topics; fremde bleiben unberührt.
  • verwaiste_topics() - Bestandsaufnahme (Wildcard-Subscribe, retained 1,5 s sammeln)
  • publish_monatsdaten() - Publiziert finale Monatswerte

⛔ Hier standen bis zum 21.09.2026 publish_number_discovery(), publish_calculated_sensor() und update_month_start_value(). Alle drei sind mit 77c6e211 entfallen (MWD-MQTT durch HA-Statistics-DB ersetzt); die Liste hat den Wegfall über ein halbes Jahr nicht mitbekommen.

Topics ({komponente} ist sensor oder binary_sensor — seit S2 gibt es beide):

homeassistant/{komponente}/eedc_{anlage_id}_{key}/config → Discovery
eedc/anlage/{anlage_id}/{key} → State
eedc/anlage/{anlage_id}/{key}/attributes → Attributes
eedc/anlage/{anlage_id}/{key}/availability → nur binary_sensor

⚠ Das vierte Topic gibt es nur beim binary_sensor, und es ist kein Beiwerk (B2b): HA kennt für ihn keinen Leerwert, also ist „nicht verfügbar” die einzige Form, in der ein fehlender Eingang sichtbar wird (ADR-002/P4). Ein sensor bekommt es nicht — dort ist der Leerwert "None" selbst die Aussage (N-405). Alle vier liegen retain=True; MQTTClient.alle_topics ist die eine Stelle, die sie aufzählt, und damit dieselbe für Publish und Rücknahme (#400).

Datei: backend/services/ha_statistics_service.py

Funktion: Zugriff auf die Home-Assistant-Langzeitstatistik.

Drei Transporte, eine Quelle (Reihenfolge = Vorrang):

#TransportVoraussetzung
1HA_RECORDER_DB_URL (SQL)externer Recorder, MariaDB/MySQL
2Recorder-Datei (SQL)Volume-Mapping homeassistant_config (read-only, path: /config) auf /config/home-assistant_v2.db
3WebSocket recorder/statistics_during_periodnur die HA-Verbindung (Supervisor oder Long-Lived-Token)

Die SQL-Wege behalten den Vorrang, wo sie verfügbar sind — synchron, kein Netz, gebündeltes Lesen. Fehlt beides, liefert der WebSocket-Weg dieselbe Quelle: sum · state · mean · min · max aus derselben Recorder-Statistik. Aggregator, Rücksetzer-Behandlung und Slot-Konvention bleiben unberührt; der Unterschied liegt allein in der Zeilen-Beschaffung (backend/services/ha_statistics_ws.py).

Gemessen 2026-08-05 gegen eine produktive Anlage, beide Wege parallel: 27 Monatswerte und 26 Monatsanfangswerte bitgleich, verfügbare Monate identisch bis auf den Tag.

⚠ Warum der dritte Weg gebraucht wird: Wer eedc als eigenen Container neben HA betreibt, hat weder /config noch HA_RECORDER_DB_URL — Tageswerte entstanden dort ausschließlich aus eedcs eigenen 5-Minuten-Snapshots, also ab Installation vorwärts. Das Konfig-Mapping hilft dort auch nicht generell: es setzt denselben Host und eine laufende HA voraus (eine WAL-Datenbank braucht auch als Leser eine schreibbare -shm) und trägt bei MariaDB-Recorder gar nicht.

⚠ Die Grenze, die auch der dritte Weg nicht verschiebt: die LTS reicht nur so weit zurück, wie der Sensor in HA existiert. Für alles davor ist der Datei-Import die Antwort.

Weitere Voraussetzung: Sensor-Mapping konfiguriert.

Hauptfunktionen:

  • get_monatswerte() - Einzelner Monat aus HA-Statistik
  • get_alle_monatswerte() - Bulk-Abfrage aller historischen Monate
  • get_verfuegbare_monate() - Liste aller Monate mit Daten
  • get_monatsanfang_wert() - Zählerstand am Monatsanfang für MQTT-Startwerte

API-Endpoints:

GET /api/ha-statistics/status # Prüft DB-Verfügbarkeit
GET /api/ha-statistics/monatswerte/{anlage_id}/{jahr}/{monat} # Einzelner Monat
GET /api/ha-statistics/verfuegbare-monate/{anlage_id} # Alle Monate mit Daten
GET /api/ha-statistics/alle-monatswerte/{anlage_id} # Bulk-Abfrage
GET /api/ha-statistics/monatsanfang/{anlage_id}/{jahr}/{monat} # Zählerstand am Monatsanfang
GET /api/ha-statistics/import-vorschau/{anlage_id} # Vorschau mit Konflikten
POST /api/ha-statistics/import/{anlage_id} # Import mit Überschreib-Schutz

Datei: backend/services/vorschlag_service.py

Funktion: Intelligente Vorschläge für Monatsabschluss-Wizard.

Vorschlags-Hierarchie:

  1. Vormonat (Konfidenz 80%) - Bester Indikator für kontinuierliche Werte
  2. Vorjahr (Konfidenz 70%) - Gleicher Monat, saisonale Korrelation
  3. Berechnung (Konfidenz 60%) - COP/EV-Quote basiert
  4. Durchschnitt (Konfidenz 50%) - Fallback aus allen vorhandenen Werten

Datei: backend/services/scheduler.py

Funktion: APScheduler-basierte Cron-Jobs für automatische Aufgaben.

Jobs (vollständig aus scheduler.py erhoben, 2026-08-07 — die Job-id ist der Name, unter dem GET /api/scheduler sie ausweist):

Job-IDTaktAufgabe
sensor_snapshotstündlich :05Zählerstände je Sensor in sensor_snapshots — die kWh-Quelle der Stunden-/Tageswerte
sensor_snapshot_previewstündlich :55Vorschau-Snapshot kurz vor dem Stundenwechsel
sensor_snapshot_5minalle 5 min (:30 s)5-Minuten-Snapshots für den Live-Tagesverlauf
sensor_snapshot_5min_cleanuptäglich 00:305-Minuten-Daten aufräumen
energie_profil_heutealle 15 minden laufenden Tag fortschreiben
energie_profil_aggregationtäglich 00:15Vortag aggregieren → TagesEnergieProfil + TagesZusammenfassung
energie_profil_aggregation_recoverytäglich 02:15zweiter Anlauf für Tage, die 00:15 verpasst hat (Neustart, Ausfall)
energie_profil_archiv_nachzugtäglich 02:20den einen Tag neu aggregieren, der die Wetter-Archiv-Grenze gerade passiert hat — damit ersetzt die endgültige Einstrahlung (ERA5) den vorläufigen Forecast-Wert. Der einzige Job, der einen historischen Tag anfasst; er überspringt ihn, wenn die HA-Historie inzwischen weniger Stunden deckt
korrekturprofil_aggregationtäglich 02:30gelernte eedc-Prognose-Korrektur neu bilden
prognose_prefetchalle 45 minWetter-/Prognosedaten vorhalten
connector_daily_polltäglich 03:30Geräte-Connectors im lokalen Netz abfragen
api_cache_cleanuptäglich 04:00api_cache aufräumen
kraftstoffpreistäglich 06:00 + StartlaufEU Weekly Oil Bulletin → TagesZusammenfassung.kraftstoffpreis_euro + Monatsdaten.kraftstoffpreis_euro. ⚠ Lief bis 23.08.2026 wöchentlich (Di) — eine Monatszeile, die zwischen zwei Läufen entsteht, blieb solange ohne Marktpreis (Discussion #394). Der Startlauf (kraftstoffpreis_startup_recovery, 90 s nach dem Hochlauf) holt einen verpassten Lauf nach; ohne offene Zeile geht kein Request raus
monthly_snapshot1. des Monats 00:01⚠ setzt nur einen Log-Zeitstempel — es gibt keinen automatischen Monatsabschluss
mqtt_auto_publishIntervall (konfigurierbar)HA-Export per MQTT, mit Start-Publish beim Hochlauf
mqtt_energy_snapshot · mqtt_live_snapshotje alle 5 minMQTT-Inbound: Energie-Zählerstände bzw. Live-Werte sichern
mqtt_energy_cleanup · mqtt_live_cleanuptäglich 03:00 / 03:05MQTT-Snapshots älter als die Aufbewahrung löschen

⚠ Dass monthly_snapshot nichts abschließt, hat Folgen für jede Monats-Auswertung: eine Monatsdaten-Zeile entsteht erst, wenn der Nutzer den Abschluss macht. Dem laufenden Monat fehlt sie immer, dem Vormonat bis zum Abschluss — Sichten müssen einen gelaufenen Monat also auch ohne Zählerzeile kennen (meta.hat_zaehlerzeile, Flag inkl_ohne_zaehlerzeile).

Datei: backend/services/kraftstoff_preis_service.py

Funktion: Nationale Benzindurchschnittspreise aus dem EU Weekly Oil Bulletin. Die Quelle erscheint wöchentlich (montags); der Nachlauf in eedc läuft täglich plus beim Start — wann eine Monatszeile entsteht, richtet sich nicht nach der Quelle.

Datenquelle: EU-Kommission XLSX (stabile URL, History seit 2005, Euro-Super 95 inkl. Steuern).

Hauptfunktionen:

  • get_kraftstoffpreise(land) — Gibt Preisliste für ein Land zurück (24h Cache)
  • get_monatsdurchschnitt(anlage_id, jahr, monat, db) — Ø aus TagesZusammenfassung
  • backfill_kraftstoffpreise(anlage_id, land, db) — Befüllt TagesZusammenfassung
  • backfill_monatsdaten_kraftstoffpreise(anlage_id, land, db) — Befüllt Monatsdaten

Datei: backend/services/ha_mqtt_sync.py

Funktion: Koordiniert MQTT-Sensoren basierend auf Sensor-Mapping.

Hauptfunktionen:

  • setup_sensors_for_anlage() - Erstellt alle MQTT Entities
  • trigger_month_rollover() - Führt Monatswechsel durch

Dateien: 7 Module in backend/services/

ModulVerantwortlichkeit
live_power_service.py (313 Z.)Orchestrierung: ruft die anderen Module auf
live_sensor_config.pyKonstanten, extract_live_config(), normalize_to_w()
live_kwh_cache.pyTTL-Caches für heute/gestern/profil kWh-Werte
live_history_service.pyHA-History-Abruf, Trapez-Integration, Tages-kWh
live_verbrauchsprofil_service.pyVerbrauchsprofil aus HA-History oder MQTT
live_tagesverlauf_service.pyButterfly-Chart Datenaufbereitung
live_komponenten_builder.pyKomponenten, Gauges, Summenbildung

Datenquellen (Priorität):

  1. HA-Sensoren (via Sensor-Mapping) — direkte REST-API-Abfrage
  2. MQTT-Inbound Cache — In-Memory-Werte von subscribten Topics

Berechnung:

  • Jede Investition wird als LiveKomponente mit erzeugung_kw / verbrauch_kw geliefert
  • Haushalt = Residual: max(0, Σ Quellen - Σ Senken) (keine eigene Messung nötig)
  • SoC-Gauges für Speicher und E-Auto (Key: soc_{invId})
  • Tages-kWh: HA-Statistiken → MQTT-Snapshots → leer (Fallback-Kette)

API-Router (3 Dateien in backend/api/routes/):

RouterEndpoints
live_dashboard.py (356 Z.)GET /api/live/{id}, GET /api/live/{id}/tagesverlauf
live_mqtt_inbound.pyGET/POST/DELETE /api/live/mqtt/*
live_wetter.pyGET /api/live/{id}/wetter + Helfer

Datei: backend/services/mqtt_inbound_service.py

Funktion: Subscribt EEDC-definierte MQTT-Topics und cached Werte im Speicher.

Topic-Struktur:

eedc/{anlage_id}/live/{key} → Echtzeit-Leistung (kW)
eedc/{anlage_id}/energy/{key} → Zählerstände (kWh, monoton steigend)

Hauptfunktionen:

  • MqttInboundCache — Thread-safe In-Memory-Cache für Live- und Energy-Werte
  • get_all_energy_raw() — Aktuelle Zählerstände für Snapshot-Service

Datei: backend/services/mqtt_energy_history_service.py

Funktion: SQLite-basierte Mini-History für Tageswerte aus MQTT Energy-Daten.

Mechanismus:

  • Tabelle mqtt_energy_snapshots speichert Zählerstand-Snapshots alle 5 Minuten
  • Tageswert = Differenz zwischen aktuellem Stand und Mitternacht-Snapshot
  • Retention: 31 Tage, automatischer Cleanup um 03:00

Dateien: backend/services/energie_profil/ (Paket — energie_profil_service.py ist nur noch die Fassade, die die öffentlichen Namen re-exportiert), backend/models/tages_energie_profil.py.

ModulRolle
aggregator.pyaggregate_day() — der einzige Schreiber von TagesEnergieProfil + TagesZusammenfassung
scheduler_jobs.pyaggregate_today_all() (laufender Tag), aggregate_yesterday_all() (Vortag)
backfill.pybackfill_range() — Datumsbereich über aggregate_day (Monatsabschluss: nur fehlende Tage, seit E4f); backfill_from_statistics() / resolve_and_backfill_from_statistics() — Vollbackfill aus HA-LTS (Werkbank)
lts_tagesverlauf.pydie Stunden-Leistungskurve aus HA-LTS (ein gebündelter Read je Bereich)
aggregations_quelle.pyVorbedingung: gibt es überhaupt eine Quelle für diesen Tag?
rollup.pyrollup_month() — Tageszeilen → Monatsdaten-Felder
_helpers.pyWetter-IST, SoC, Betriebsmodus, Strompreis je Stunde

Funktion: Langfristige Persistierung stündlicher Energiedaten. HA-History hat nur ~10 Tage Retention — dieser Service sichert die Daten dauerhaft in SQLite.

Datenmodelle:

TabelleGranularitätInhalt
TagesEnergieProfil24 Zeilen/Tag/AnlageStündliche kW-Werte: PV, Verbrauch, Einspeisung, Netzbezug, Batterie, Überschuss, Defizit + Wetter (Temperatur, Globalstrahlung) + SoC + Komponenten-JSON
TagesZusammenfassung1 Zeile/Tag/AnlageTagessummen (Überschuss/Defizit kWh), Peaks (PV, Netzbezug, Einspeisung kW), Batterie-Vollzyklen, Wetter-Min/Max, Performance Ratio, Datenqualität (stunden_verfuegbar)

Datenfluss-Pipeline:

┌── Mitschrift (läuft dauernd, unabhängig von der Aggregation) ──────────┐
│ sensor_snapshot stündlich :05 Zählerstände → sensor_snapshots │
│ sensor_snapshot_preview stündlich :55 Vorschau kurz vor dem Wechsel │
│ sensor_snapshot_5min alle 5 min Live-Tagesverlauf │
│ mqtt_energy_snapshot alle 5 min dasselbe für den MQTT-Betrieb │
└────────────────────────────────────────────────────────────────────────┘
│
┌── Aggregation (drei Anlässe, EIN Schreiber) ───────────────────────────┐
│ energie_profil_heute alle 15 min → aggregate_today_all() │
│ energie_profil_aggregation täglich 00:15 → aggregate_yesterday_all() │
│ energie_profil_aggregation_recovery 02:15 zweiter Anlauf (#136) │
│ energie_profil_archiv_nachzug täglich 02:20 Wetter-Grenztag (N-388)│
│ Reparatur-Werkbank / Monatsabschluss / Vollbackfill (manuell bzw. │
│ ereignisbezogen) │
└────────────────────────────────────────────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
ZÄHLERPFAD (kWh) LEISTUNGSPFAD (kW) KONTEXT je Stunde
HA-LTS-Deltas get_tagesverlauf() Wetter (Open-Meteo)
└ Fallback: └ Fallback: MQTT SoC · Betriebsmodus
sensor_snapshots Live-Snapshots Strom-/Börsenpreis
│ │ │
└─────────────────────┼─────────────────────┘
▼
aggregate_day()
(idempotent: Delete + Insert)
│ │
▼ ▼
TagesEnergieProfil TagesZusammenfassung
(24 Stunden-Zeilen) (1 Tages-Zeile)
│
▼ (beim Monatsabschluss)
rollup_month()
▼
Monatsdaten-Felder

Die Slot-Konvention — welche Uhr-Stunde eine Zeile beschreibt

Abschnitt betitelt „Die Slot-Konvention — welche Uhr-Stunde eine Zeile beschreibt“

SoT: backend/core/berechnungen/slot_konvention.py. Es gilt durchgängig backward:

Slot h = Energie im Intervall [h-1, h). Slot 0 = [Vortag 23:00, 00:00) · Slot 23 = [22:00, 23:00).

Das ist Industriestandard (HA-Energiedashboard, SolarEdge, SMA, Fronius, Tibber) und keine Geschmacksfrage: Alle Bahnen, die in derselben Zeile landen, müssen dasselbe physische Intervall meinen — sonst führt jede Auswertung, die zwei davon je Stunde zusammenrechnet, quer über einen Versatz. Genau das ist zweimal passiert (2026-06-04 der HA-LTS-Pfad, 2026-09-04 der Leistungspfad); die Tagessummen blieben dabei beide Male unauffällig, weil sich ein Ein-Stunden-Versatz über 24 Slots aufhebt.

⚠ Der Leistungspfad braucht dafür den Vortagsrand. Seine Punkte tragen den Slot-Beginn ("05:00" deckt [05:00, 06:00)), landen also in Slot 6 — und Slot 0 kann folglich nicht aus dem eigenen Tag kommen. get_tagesverlauf(mit_vortagsrand=True) macht deshalb das bestehende Abruf-Fenster eine Stunde weiter auf, statt einen zweiten Tag zu holen: der 15-Minuten-Job würde sonst 96-mal täglich je Anlage einen kompletten Extra-Tag aus HA ziehen. Die Rand-Punkte kommen getrennt zurück ("vortagsrand"), weil ein Punkt nur seine Uhrzeit trägt und "23:00" von gestern sonst nicht von "23:00" von heute zu unterscheiden wäre.

⛔ Drei Größen liegen weiterhin forward und sind im SoT namentlich geführt: soc_prozent, strompreis_cent, boersenpreis_cent. Wer sie anfasst, liest dort zuerst nach — sie stehen neben Backward-Spalten derselben Zeile.

Zehn Mechanismen, jeder mit seinem Anlass. Sie greifen nacheinander, nicht alternativ:

#MechanismusWoWogegen
1Quellen-Vorprüfungaggregations_quelle.pyEin Lauf ohne jede Quelle bricht vor dem Löschen ab, statt eine leere Zeile zu schreiben
2Zwei Zählerpfadelts_aggregator → sensor_snapshot_serviceHA-LTS ist SoT; fehlt sie (Standalone/MQTT), übernimmt der Snapshot-Pfad mit demselben Ausgabe-Vertrag
3Lücken-Interpolationsnapshot/aggregator.py::_fill_gaps_linear (#145)Einzelne fehlende Zählerstände vor der Delta-Bildung. Ränder werden nicht extrapoliert — ohne Anker keine Schätzung
4Either-Or je Fallback-Gruppesnapshot/aggregator.py (#298)Zwei Sensoren für dieselbe Größe zählen nicht doppelt — Auflösung auf Tages-Ebene, nicht je Stunde
5Plausibilitäts-Capsnapshot/aggregator.py, snapshot/fallback.py (#184)Ein Zähler-Rücksprung oder ein Lifetime-großer Stunden-Spike wird gekappt statt eingebucht
6Counter-Σ aus dem Tages-Diffverteile_counter_auf_stundenBei Lücken wird die Stunden-Σ so reskaliert, dass sie auf den Tages-Boundary-Diff führt — eine Quelle je Tag
7Rettung fremdbefüllter Felderaggregator.py (_PROGNOSE_FELDER_RETTEN, #190/#319)aggregate_day löscht und schreibt neu; Prognose- und Kraftstoffpreis-Felder kommen von anderen Schreibern und würden sonst jedes Mal verschwinden
8Drei Konsistenz-Invariantenaggregator.py am Ende des LaufsAchse 1 Stunden- gegen Tagespfad · Achse 2 Leistungs-JSON gegen Zähler-Spalten (#315) · Counter-Daily-Drift. Alle drei melden per warning — sie verwerfen keinen Tag
9Selbstheilung + Nachlaufenergie_profil_aggregation_recovery (02:15, #136) · sensor_snapshot_startup_recovery (letzte 6 h nach Neustart)Verspätete HA-Statistik und verpasste Läufe nach einem Neustart
10Archiv-Nachzug der Wetterzeileenergie_profil/archiv_nachzug.py (02:20, N-388)Die Einstrahlung der letzten fünf Tage ist ein vorläufiger Modellwert — das ERA5-Archiv hinkt 2–5 Tage nach. Ohne Nachzug bleibt er für immer stehen; an bewölkten Tagen war er bis Faktor 8,7 zu klein und trieb die Performance Ratio über 1

⭐ Warum das zusammen genügt und einzelne Ausfälle nichts unterdrücken müssen: aggregate_day ist idempotent (Delete + Insert) — jeder spätere Lauf korrigiert den früheren. Ein Sensor, der mitten am Tag ausfällt, ist deshalb in aller Regel schon geheilt, bevor eine Bilanz ihn sieht. Unterdrückt wird nur der Total-Fall (eine Größe wurde nie gemessen, *_erfasst); eine Teilabdeckung bleibt stehen und wird nicht wegretuschiert.

⚠ Was die Automatik NICHT tut, und das ist Absicht:

  • Sie zieht die Vergangenheit nicht nach. Eine geänderte Sensor-Zuordnung wirkt ab jetzt; die gespeicherten Zeilen tragen die Zuordnung ihres Aggregationslaufs. Der Weg zurück ist die Reparatur-Werkbank (Einstellungen → Daten, „Mehrere Tage neu aggregieren”, bis 31 Tage je Lauf) — bewusst vom Anwender ausgelöst und in Blöcken, kein globaler Heiler-Knopf. ⚠ Für den laufenden Tag gilt das nicht: er wird alle 15 Minuten komplett neu gerechnet und übernimmt eine neue Zuordnung damit rückwirkend für diesen Tag. ⚠ Und seit N-388 gibt es eine zweite, eng begrenzte Ausnahme: der Archiv-Nachzug (02:20) aggregiert genau einen historischen Tag neu — den, der die Wetter-Archiv-Grenze gerade passiert hat. Das ist keine Aufweichung der Regel, sondern ihr Preis: die Wetterzeile der letzten fünf Tage ist vorläufig, und niemand sonst holt sie nach. Damit dabei nichts verlorengeht, prüft der Job vorher, ob die HA-Historie den Tag noch so weit deckt wie beim ersten Lauf — deckt sie weniger, bleibt der Tag stehen. Ältere Tage bleiben unangetastet; für sie ist der Weg wieder die Reparatur-Werkbank.
  • Sie schließt keinen Monat ab. monthly_snapshot setzt nur einen Log-Zeitstempel.
  • Sie überschreibt keine LTS-Lücken. „Lücken aus HA-LTS nachfüllen” ist ausdrücklich additiv (#190); einen Overwrite-Modus gibt es bewusst nicht.

Hauptfunktionen:

FunktionTriggerBeschreibung
aggregate_day()alle AnlässeDer einzige Schreiber. Holt Tagesverlauf + Wetter + SoC + Betriebsmodus + Preise, schreibt 24 Stundenzeilen + Tageszusammenfassung. Idempotent (Delete + Insert)
aggregate_today_all()Scheduler alle 15 minDer laufende Tag für alle Anlagen — er wird dabei jedes Mal vollständig neu gerechnet
aggregate_yesterday_all()Scheduler 00:15 + 02:15Vortag für alle Anlagen mit Sensor-Mapping; 02:15 ist der zweite Anlauf (#136)
archiv_nachzug_all()Scheduler 02:20Der Wetter-Grenztag (heute − ARCHIVE_LAG_TAGE − 1) für alle Anlagen. Ruft denselben aggregate_day — der Endpunkt-Wechsel Forecast→Archiv passiert in _get_wetter_ist von selbst, es gibt keinen zweiten Rechenweg für Einstrahlung, GTI oder PR
backfill_range()MonatsabschlussDatumsbereich über aggregate_day je Tag (Leistungskurve aus dem HA-Verlauf, ~10 Tage). Seit E4f mit nur_fehlende=True: nur Tage ohne Tageszeile, und nur, solange HA sie im Verlauf hat (aggregate_day(nur_mit_verlauf=True))
backfill_from_statistics()Werkbank „Lücken aus HA-LTS nachfüllen”Holt die Stunden-Leistungskurve gebündelt aus HA-LTS (lade_tagesverlauf_aus_lts) und reicht sie je fehlendem Tag durch — nicht limitiert auf die ~10 Tage HA-History, strikt additiv (#190)
rollup_month()MonatsabschlussAggregiert TagesZusammenfassung → Monatsdaten-Felder (Summe/Durchschnitt/Max)

Berechnungsdetails:

  • Überschuss/Defizit: Pro Stunde max(0, PV - Verbrauch) bzw. umgekehrt, Summe = kWh (kW × 1h)
  • Batterie-Vollzyklen: Σ |ΔSoC| / 200 (ein Vollzyklus = 0→100→0 = 200% ΔSoC)
  • Performance Ratio: PV_Ertrag_kWh / (GTI_Wh/m² × kWp / 1000) — Nenner ist die GTI (Global Tilted Irradiance, auf die Modulfläche projiziert, bei mehreren Ausrichtungen kWp-gewichtet), nicht die waagerechte Globalstrahlung. Mit GHI liefen die PR-Werte im Winter künstlich auf 1,5–2,8 (#139); wer die PR gegen die angezeigte Globalstrahlung nachrechnet, bekommt deshalb eine andere Zahl (N-384)
  • Wetter: Open-Meteo — Forecast-Endpoint für heute und die jüngsten Tage (das Archiv hängt 2–5 Tage hinterher), Archive-Endpoint für ältere. Die Stundenwerte sind preceding-hour-Mittel und damit bereits Backward-Slots (s. Slot-Konvention oben)
  • SoC: HA-LTS-Stundenmittel, Fallback auf die State-History. Bei mehreren Speichern kapazitätsgewichtet über alle Geräte (N-239), die Aufschlüsselung steht daneben in soc_je_speicher

Integration mit Monatsabschluss (Nachlauf, services/monatsabschluss_aggregator.py, Stand E4f):

Nach jedem Speichern eines Monats laufen im Hintergrund (Nummern aus Bauplan HA-Bauform §9 B1 / Konzept R-13):

  1. backfill_range(nur_fehlende=True) — nur Tage ohne Tageszeile, und nur, solange Home Assistant sie im Verlauf hat (Grenze an derselben Stelle wie N-596: die Leistungskurve trägt noch einen Wert). Ein vorhandener Tag wird nie neu gerechnet — bis E4f schrieb dieser Schritt jeden Tag des Monats neu, auch gepurgte (#422, N-596).
  2. rollup_month() — Tagesdaten in Monatsdaten verdichten (Vollzyklen, Spitzen, PR hängen an Stundenwerten — bis S3).
  3. entfallen mit E4f: der einmalige Auto-Vollbackfill beim ersten Abschluss nach einem Upgrade (Flag Anlage.vollbackfill_durchgefuehrt bleibt als Spalte, ohne Leser). Lücken füllt die Werkbank auf Knopfdruck.
  4. schreibe_modus_split_monat() — die Aufteilung nach Betriebsart festschreiben; deckt die WP-Gruppe den Monat aus den Kanälen, aus dem abgeleiteten Kanal „Strom je Betriebsart” (E4d), sonst aus den Stundenzeilen.

Daneben (unverändert, monatsabschluss/wizard.py::_post_save_hintergrund): MQTT-Publish, Community-Teilen, Aktivitätseintrag.


Problem: Komplexe HA-Integration erwies sich als problematisch:

  • EVCC liefert andere Datenstrukturen als erwartet
  • Utility Meter können nicht programmatisch zugeordnet werden
  • Jede Haus-Automatisierung ist anders

Lösung: EEDC ist primär Standalone:

  • Datenerfassung: CSV-Import oder manuelles Formular
  • Wetter-Daten: Open-Meteo/PVGIS (HA-unabhängig)
  • HA-Export: Optional, nur für berechnete KPIs

Problem: Ursprünglich wurden Speicher-Daten in Monatsdaten gespeichert.

Probleme:

  • Nicht skalierbar für mehrere Speicher
  • Inkonsistent mit anderen Komponenten
  • Schwer erweiterbar

Lösung: Alle Komponenten-Details in InvestitionMonatsdaten:

  • Monatsdaten = Nur Zählerwerte (Einspeisung, Netzbezug)
  • InvestitionMonatsdaten = Alle Komponenten-Details

Was diese Trennung NICHT bedeutet (Lehre aus der Drift-Inventur 2026-07-31): dass jede Read-Site sich ihre Monatszeile selbst aus beiden Tabellen zusammenfaltet. Genau das war jahrelang der Fall und genau daraus entstanden sechs Befunde mit derselben Ursache. Die Auflösung — welcher Wert gilt, welche Lücke wird gefüllt, welche Investition zählt im Monat überhaupt — liegt seit ADR-002/P10 in services/monats_fakten/ (§7 „Lese-Schichtung”).

Grund: PV-System ROI-Berechnung:

  • Wechselrichter-Kosten müssen auf PV-System verteilt werden
  • Einzelne Module haben keine eigene Amortisation
  • Aggregation ermöglicht realistische ROI-Aussagen

Grund: Flexibilität bei typ-spezifischen Feldern:

  • Speicher braucht kapazitaet_kwh, E-Auto braucht v2h_faehig
  • Schema-Evolution ohne DB-Migrationen
  • SQLite unterstützt JSON-Queries

Achtung: SQLAlchemy erkennt JSON-Änderungen nicht automatisch!

from sqlalchemy.orm.attributes import flag_modified
investition.parameter["key"] = "value"
flag_modified(investition, "parameter")
db.commit()

Problem: Python wertet 0 als False:

if val: # Falsch! 0 wird als False gewertet
if val is not None: # Richtig!

Konsequenz: Überall is not None statt if val.

ROI-Metriken (Wichtig: Unterschiedliche Bedeutungen!)

Abschnitt betitelt „ROI-Metriken (Wichtig: Unterschiedliche Bedeutungen!)“
MetrikWoFormelBedeutung
ROI p.a.Auswertungen → ROIJahres-Einsparung / Relevante Kosten × 100Rendite pro Jahr (p.a.) — das Cockpit zeigt keine Rendite; jahres_rendite_prozent der Übersicht liest nur der Jahresbericht-PDF („Rendite”, N-699)
Amortisations-FortschrittAussichten/FinanzenKum. Erträge / Investition × 100Wie viel % bereits abbezahlt

Mehrkosten-Ansatz für Investitionen: Bei der ROI-Berechnung werden Mehrkosten gegenüber Alternativen berücksichtigt:

  • PV-System: Volle Kosten (keine Alternative)
  • Wärmepumpe: Kosten minus Gasheizung (alternativ_kosten_euro Parameter)
  • E-Auto: Kosten minus Verbrenner (alternativ_kosten_euro Parameter)
EEDC Add-on Community Server
┌──────────────────────┐ ┌──────────────────┐
│ v4/CommunityShare- │ ── POST ──────→ │ /api/submit │
│ Block.tsx │ ── DELETE ────→ │ /api/submit/{h} │
│ v4/CommunityV4.tsx │ ── Proxy ─────→ │ /api/benchmark/ │
│ (+ 6 Unter-Sichten)│ │ anlage/{hash} │
│ "Im Browser öffnen" │ ── Link ──────→ │ /?anlage=HASH │
└──────────────────────┘ └──────────────────┘

Relevante Dateien:

  • backend/services/community_service.py – Datenaufbereitung + Anonymisierung; die Monatswerte kommen aus den Monats-Fakten (ADR-002/P10), nicht aus eigener Faltung
  • backend/services/plz_to_state.py – PLZ→Bundesland (8.308 Einträge, O(1))
  • backend/api/routes/community.py – Routen + Benchmark-Proxy; der Client spricht den Community-Server nie direkt an
  • frontend/src/v4/CommunityShareBlock.tsx – teilen und rückwirkend entfernen
  • frontend/src/v4/CommunityV4.tsx – Benchmark-Sicht mit sechs Unter-Sichten (CommunityUebersichtV4 · PVErtragV4 · KomponentenV4 · RegionalV4 · TrendsV4 · StatistikenV4)
  • frontend/src/api/community.ts – API-Client

⚠ Der Community-Server rechnet nichts nach — er hat die Rohdaten nie gesehen. Was ein Feld bedeutet, steht als Vertrag im Docstring von MonatswertInput im Community-Repo; wer die Bedeutung ändert, ändert sie dort mit. Altbestand heilt beim nächsten Voll-Submit.

Verzeichnis: backend/services/cloud_import/

Architektur: ABC-Pattern mit @register_provider Decorator und Provider-Registry.

Verfügbare Provider (12, SoT ist services/cloud_import/registry.py):

ProviderStatus
Anker SOLIX✅ an einem echten Konto bestätigt
Victron VRM✅ an einem echten Konto bestätigt
Deye / Solarman · EcoFlow PowerOcean · EcoFlow PowerStream · Fronius Solar.web · Growatt · Hoymiles S-Miles · Huawei FusionSolar · SolarEdge · Sungrow iSolarCloud · Viessmann GridBox / E.ON Home⚠ getestet=False

⚠ Viessmann GridBox meldet sich gegen E.ON Home an, und das ist Absicht. Die GridBox ist zum 31.12.2025 samt Daten zu E.ON Home gewechselt; der Viessmann-eigene Auth0-Realm (viessmann-authentication-db) ist seither abgeschaltet, Anmeldeseite ist eon.gridx.de/login. Der Provider trägt deshalb den E.ON-Home-Realm — wer dort „Viessmann” einsetzt, sperrt alle Anwender aus. Die Provider-ID bleibt viessmann_gridbox: sie steht im Provenance-Vokabular (core/source_priority.py), in gespeicherten connector_config-Quellen und in jeder geschriebenen Provenance-Zeile — änderbar ist nur die Anzeige.

⚠ Und die Auth0-Umstellung von 2026 (#410, gemeldet von gridX selbst): Audience ist die API-Basis (https://api.gridx.de), nicht mehr my.gridx, und der Bearer ist der access_token, nicht der id_token. Beide alten Formen werden nur noch während einer Gnadenfrist akzeptiert. Gewächtert von backend/tests/test_viessmann_gridbox.py.

getestet ist eine Aussage über die Wirklichkeit, kein Ausdruck von Zuversicht. Das Flag geht erst auf True, wenn ein Nutzer mit einem echten Konto Einrichtung und Zeitraum-Import gemeldet hat — rot verifizierte Tests genügen nicht: eine Fixture ist eine Behauptung über eine fremde API. Genau daran ist der Solarman-Import zweimal gescheitert (die Fixture erfand eine body-Hülle, die es nicht gibt).

Output: Alle Provider liefern ParsedMonthData als einheitliches Format.

Verzeichnis: backend/api/routes/custom_import/

Funktion: Beliebige CSV/JSON-Dateien mit benutzerdefinierbarem Feld-Mapping importieren.

Features:

  • Automatische CSV-Dialekt-Erkennung (Trennzeichen, Dezimalformat)
  • Auto-Mapping anhand von Spaltenbezeichnungen (deutsch + englisch)
  • Einheiten-Umrechnung (Wh/kWh/MWh)
  • Mapping-Templates in Settings-Tabelle speicherbar
  • 4-Schritt-Wizard: Upload → Mapping → Vorschau → Import

API-Endpoints:

POST /api/custom-import/analyze # Datei analysieren, Spalten erkennen
POST /api/custom-import/preview # Mapping anwenden, Vorschau
GET /api/custom-import/templates # Gespeicherte Templates laden
POST /api/custom-import/templates/{name} # Template speichern
DELETE /api/custom-import/templates/{name} # Template löschen
GET /api/custom-import/fields # Verfügbare EEDC-Zielfelder

SoT für Einrichtung, Befehle und Gates ist DEVELOPMENT.md, für den Release RELEASE-WORKFLOW.md. Hier stand bis 2026-08-07 eine Kopie beider — mit vier statt fünf Versionsdateien und dem Satz „Frontend Tests (noch nicht implementiert)”, während längst eine Vitest-Suite und die check:*-Wächter liefen. Kopierte Befehle veralten unbemerkt; deshalb bleibt hier nur, was architektonisch ist.

eedc-homeassistant (Source of Truth)
├── eedc/backend/ ─── scripts/release.sh ───→ eedc (Standalone-Spiegel)
├── eedc/frontend/ ─── scripts/release.sh ───→ eedc (Standalone-Spiegel)
├── docs/ ── website/scripts/sync-docs.sh ─→ website/ (GitHub Pages)
│ └── scripts/sync-help.sh ──→ In-App-Hilfe (versioniert!)
└── CHANGELOG.md ─── release.sh ────────────→ eedc/CHANGELOG.md
(die Datei, die HA-Nutzer im Store lesen)
eedc-community (unabhängig — bei Datenmodell-Änderungen synchron anpassen)

Regeln, die daraus folgen:

  • Alle Änderungen in eedc-homeassistant, nie direkt im eedc-Spiegel.
  • Immer auf main (Einzelentwickler-Projekt, keine Feature-Branches), kein git subtree.
  • Nur explizite Pfade committen — es laufen parallele Sessions im selben Arbeitsbaum.
  • Zwei Kopien hängen an docs/: die Website-Kopie ist gitignored, die In-App-Hilfe-Kopie ist versioniert und gehört in denselben Commit (scripts/sync-help.sh).

release.sh bumpt fünf Versionsdateien (Backend, Frontend, config.yaml, run.sh, Dockerfile), kopiert den CHANGELOG, committet, taggt und pusht beide Repos. Danach laufen drei Workflows: tests.yml, release.yml (baut die Add-on-Images) und deploy-website.yml.

⚠ Das Add-on zieht ein vorgebautes Image (eedc/config.yaml → ghcr.io/supernova1963/eedc-homeassistant-{arch}). Zwischen Tag und fertigem Image ist die Version im Store sichtbar, aber nicht installierbar ([404] manifest unknown). Der Prüf-Einzeiler samt seiner beiden Pflicht-Header steht in DEVELOPMENT.md §Versionierung.

eedc Add-on Community Server
┌───────────────────────────────┐ ┌────────────────────────┐
│ v4/CommunityShareBlock.tsx │ ─ POST ───→ │ /api/submit │
│ (teilen / rückwirkend weg) │ ─ DELETE ─→ │ /api/submit/{hash} │
│ v4/CommunityV4.tsx │ ─ Proxy ──→ │ /api/benchmark/ │
│ │ │ anlage/{hash} │
└───────────────────────────────┘ └────────────────────────┘

Der Client spricht den Community-Server nie direkt an — alles läuft über api/routes/community.py (Proxy + Aufbereitung). Der Server rechnet nichts nach: er hat die Rohdaten nie gesehen, und was ein Feld bedeutet, steht als Vertrag im Docstring von MonatswertInput (Community-Repo). Wer die Bedeutung ändert, ändert sie dort mit.


Vollständige API-Dokumentation nach dem Start des Backends:

  • Swagger UI: http://localhost:8099/api/docs
  • ReDoc: http://localhost:8099/api/redoc
  • OpenAPI JSON: http://localhost:8099/api/openapi.json

Die Prefix-Übersicht je Modul steht in DEVELOPMENT.md §API-Routen Übersicht — erhoben aus backend/main.py, das der SoT der Prefixe ist.