Architektur
EEDC Architektur-Dokumentation
Abschnitt betitelt „EEDC Architektur-Dokumentation“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.shbumptdocs/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:
Frage SoT Informationsarchitektur, Oberflächen-Invarianten I1–I16, Redirects KONZEPT-IA-V4.md Darstellung (Farben, Komponenten, Typografie, Charts), Regel 0/0a KONZEPT-STYLE-GUIDE.md Wo eine Aggregat-Formel definiert wird ADR-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 8 KONZEPT-WAERME-KLIMA.md Formeln je Kennzahl BERECHNUNGEN.md Einrichtung, Befehle, Gates, Dateibestand DEVELOPMENT.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.
Inhaltsverzeichnis
Abschnitt betitelt „Inhaltsverzeichnis“- Übersicht
- Technologie-Stack
- Projektstruktur
- Datenmodell
- API-Architektur
- Frontend-Architektur
- Services
- Design-Entscheidungen
- Entwickler-Workflow
1. Übersicht
Abschnitt betitelt „1. Übersicht“Architektur-Prinzipien
Abschnitt betitelt „Architektur-Prinzipien“- 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.
- Lokale Datenspeicherung: SQLite unter
/data/eedc.db, alle Daten bleiben beim Nutzer. Der Community-Server bekommt nur anonymisierte, ausdrücklich geteilte Aggregate. - 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.
- Datenquellen getrennt:
Monatsdatenträgt Zählerwerte,InvestitionMonatsdatendie Komponenten-Details. Legacy-Felder (Monatsdaten.batterie_*, das computed-Trio) werden nicht gelesen;Monatsdaten.pv_erzeugung_kwhist ausschließlich Eingang der PV-Auflösung (ADR-002/P7). - 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.
- 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.
System-Architektur
Abschnitt betitelt „System-Architektur“┌─────────────────────────────────────────────────────────────┐│ 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.
2. Technologie-Stack
Abschnitt betitelt „2. Technologie-Stack“Backend
Abschnitt betitelt „Backend“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).
| Technologie | Version | Zweck |
|---|---|---|
| Python | 3.11+ | Programmiersprache |
| FastAPI | ≥ 0.109, < 0.137 | REST-API-Framework (bewusst gedeckelt) |
| Uvicorn | ≥ 0.27 | ASGI-Server |
| SQLAlchemy | ≥ 2.0.25 | ORM, async |
| aiosqlite | ≥ 0.19 | async-SQLite-Treiber (loop-gebunden — sync-Aufrufer brauchen eine Brücke) |
| SQLite | 3.x | Datenbank |
| Pydantic | ≥ 2.5 (+ pydantic-settings) | Datenvalidierung, Settings |
| httpx | ≥ 0.26 | HTTP-Client für externe APIs |
| websockets | ≥ 12.0 | HA-WebSocket (Langzeitstatistik ohne DB-Zugang) |
| aiomqtt | ≥ 2.0 | MQTT — Export und Inbound |
| APScheduler | ≥ 3.10 | Snapshot-, Prognose- und Korrektur-Jobs |
| WeasyPrint + Jinja2 | ≥ 62.0 / ≥ 3.1 | PDF-Berichte (HTML→PDF, SVG-Charts) |
Frontend
Abschnitt betitelt „Frontend“| Technologie | Version | Zweck |
|---|---|---|
| React | 18.x | UI Framework |
| TypeScript | 5.x | Typsichere Programmierung |
| Vite | 5.x | Build Tool & Dev Server |
| Tailwind CSS | 3.x | Styling |
| Recharts | 2.x | Diagramme |
| react-simple-maps | 3.x | Choropleth Deutschlandkarte |
| Lucide React | - | Icons |
| React Router | 6.x | Routing |
Deployment
Abschnitt betitelt „Deployment“| Variante | Technologie |
|---|---|
| Home Assistant Add-on | Docker (Ingress) |
| Standalone | Docker |
| Entwicklung | Uvicorn + Vite Dev Server |
3. Projektstruktur
Abschnitt betitelt „3. Projektstruktur“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.
Die drei Repositories
Abschnitt betitelt „Die drei Repositories“| Repository | Rolle | Wird wie gepflegt |
|---|---|---|
| eedc-homeassistant | Source of Truth — Backend, Frontend, Docs, HA-Add-on-Konfiguration, Website | hier wird gearbeitet |
| eedc | Standalone-Distribution für Nutzer ohne HA | Spiegel, ausschließlich per scripts/release.sh |
| eedc-community | anonymer 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.
Backend-Schichten (eedc/backend/)
Abschnitt betitelt „Backend-Schichten (eedc/backend/)“Die Reihenfolge ist die Abhängigkeitsrichtung — nach oben wird nie gerufen:
| Schicht | Verzeichnis | Zuständig für | Darf nicht |
|---|---|---|---|
| Routen | api/routes/ | HTTP, Validierung, Response-Form | rechnen — Aggregat-Formeln gehören in den Layer (ADR-001) |
| Services | services/ | Beschaffung, Aufbereitung, externe APIs, Persistenz-Abläufe | eigene Fassungen einer Formel halten |
| Berechnungs-Layer | core/berechnungen/ | alle Aggregat-Formeln, je Größe genau eine | I/O oder DB-Zugriff |
| Modelle | models/ | SQLAlchemy-Tabellen | Fachlogik |
| Kern | core/ | 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 faltetInvestitionMonatsdatenmehr selbst (ADR-002/P10, KONZEPT-MONATS-FAKTEN.md).core/investition_kennwerte.py— Nennleistung je Typ, gleich ob sie in der Spalte oder imparameter-JSON liegt (ADR-002/P3-a).core/berechnungen/slot_konvention.py— Slot N ist die Energie in[N-1, N), baumweit (Backward-Konvention, Industriestandard).
Frontend-Schichten (eedc/frontend/src/)
Abschnitt betitelt „Frontend-Schichten (eedc/frontend/src/)“| Schicht | Verzeichnis | Zuständig für |
|---|---|---|
| Sichten | v4/ | die ausgelieferte Oberfläche: eine Datei je Sicht bzw. Block |
| Geteilte Bausteine | components/ | SoT-Komponenten (eine Klasse = eine Komponente), Block-Modell, Park-Mechanik |
| Einstellungs-Flächen | pages/ | Stammdaten, Import-Assistenten, Datenverwaltung — von V4 eingebunden |
| Ableitungen | lib/ | reine Funktionen + Farb-/Datums-/Einheiten-SoT |
| Transport | api/ | ein Modul je Backend-Router, client.ts als Basis |
| Zustand | hooks/, context/ | useApiData (inkl. SWR-Sicht-Cache), Auswahl, Theme, Status |
| Wege | routes/, 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.
4. Datenmodell
Abschnitt betitelt „4. Datenmodell“Entity-Relationship Diagramm
Abschnitt betitelt „Entity-Relationship Diagramm“┌─────────────────────────────────────────────────────────────┐│ 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)Tabellen im Detail
Abschnitt betitelt „Tabellen im Detail“| Feld | Typ | Beschreibung |
|---|---|---|
| id | INTEGER | Primary Key |
| anlagenname | VARCHAR(255) | Bezeichnung |
| leistung_kwp | FLOAT | Anlagenleistung in kWp |
| installationsdatum | DATE | Inbetriebnahme (optional) |
| standort_land | VARCHAR(5) | Land: DE, AT oder CH |
| standort_plz | VARCHAR(10) | Postleitzahl |
| standort_ort | VARCHAR(255) | Ort |
| standort_strasse | VARCHAR(255) | Adresse |
| latitude | FLOAT | Breitengrad (für PVGIS) |
| longitude | FLOAT | Längengrad (für PVGIS) |
| ausrichtung | VARCHAR(50) | DEPRECATED – jetzt bei PV-Modul Investitionen |
| neigung_grad | FLOAT | DEPRECATED – jetzt bei PV-Modul Investitionen |
| wechselrichter_hersteller | VARCHAR(50) | sma, fronius, kostal, etc. |
| mastr_id | VARCHAR(20) | MaStR-ID der Anlage |
| versorger_daten | JSON | Versorger & Zähler |
| wettermodell | VARCHAR(30) | auto, meteoswiss_icon_ch2, icon_d2, icon_eu, ecmwf_ifs04 |
| sensor_mapping | JSON | HA-Sensor-Mapping |
| community_hash | VARCHAR(64) | Hash für Community-Löschung |
| steuerliche_behandlung | VARCHAR(30) | keine_ust oder regelbesteuerung |
| ust_satz_prozent | FLOAT | USt-Satz: DE=19, AT=20, CH=8.1 |
| created_at | DATETIME | Erstellungsdatum |
| updated_at | DATETIME | Letztes Update |
Monatsdaten
Abschnitt betitelt „Monatsdaten“Wichtig: Diese Tabelle enthält primär Zählerwerte (Einspeisung, Netzbezug).
| Feld | Typ | Beschreibung |
|---|---|---|
| id | INTEGER | Primary Key |
| anlage_id | INTEGER | Foreign Key → Anlage |
| jahr | INTEGER | Jahr |
| monat | INTEGER | Monat (1-12) |
| einspeisung_kwh | FLOAT | Zählerwert Einspeisung |
| netzbezug_kwh | FLOAT | Zählerwert Netzbezug |
| direktverbrauch_kwh | FLOAT | Berechnet |
| eigenverbrauch_kwh | FLOAT | Berechnet |
| gesamtverbrauch_kwh | FLOAT | Berechnet |
| globalstrahlung_kwh_m2 | FLOAT | Wetter-API |
| sonnenstunden | FLOAT | Wetter-API |
| durchschnittstemperatur | FLOAT | Wetter-API |
| netzbezug_durchschnittspreis_cent | FLOAT | Ø Strompreis bei dynamischem Tarif (ct/kWh) |
| kraftstoffpreis_euro | FLOAT | Ø Benzinpreis des Monats (€/L, aus EU Oil Bulletin) |
| sonderkosten_euro | FLOAT | Manuelle Eingabe |
| sonderkosten_beschreibung | VARCHAR(500) | Beschreibung der Sonderkosten |
| datenquelle | VARCHAR(50) | manual, csv, ha_import |
| notizen | VARCHAR(1000) | Freitext |
| created_at | DATETIME | Erstellungsdatum |
| updated_at | DATETIME | Letztes Update |
Legacy-Felder (nicht mehr verwenden):
pv_erzeugung_kwh→ Verwende InvestitionMonatsdatenbatterie_ladung_kwh→ Verwende InvestitionMonatsdatenbatterie_entladung_kwh→ Verwende InvestitionMonatsdatenbatterie_ladung_netz_kwh→ Arbitrage (Legacy)batterie_ladepreis_cent→ Arbitrage (Legacy)
Investitionen
Abschnitt betitelt „Investitionen“| Feld | Typ | Beschreibung |
|---|---|---|
| id | INTEGER | Primary Key |
| anlage_id | INTEGER | Foreign Key → Anlage |
| typ | VARCHAR(50) | Investitionstyp (siehe InvestitionTyp Enum) |
| bezeichnung | VARCHAR(255) | Name der Komponente |
| anschaffungsdatum | DATE | Inbetriebnahme |
| anschaffungskosten_gesamt | FLOAT | Kaufpreis + Installation |
| anschaffungskosten_alternativ | FLOAT | Alternativkosten (z.B. neuer Verbrenner) |
| betriebskosten_jahr | FLOAT | Jährliche Betriebskosten |
| leistung_kwp | FLOAT | Leistung in kWp (PV-Module) |
| ausrichtung | VARCHAR(50) | Modulausrichtung (PV-Module) |
| neigung_grad | FLOAT | Modulneigung in Grad (PV-Module) |
| ha_entity_id | VARCHAR(255) | HA Entity-ID (für String-IST-Erfassung) |
| parameter | JSON | Typ-spezifische Parameter |
| einsparung_prognose_jahr | FLOAT | „Ertrag/Jahr” — wiederkehrender Ertrag/Einsparung, pflegbar bei Wallbox/Sonstiges (ERTRAGSFELD_TYPEN) |
| co2_einsparung_prognose_kg | FLOAT | CO2-Einsparungsprognose |
| aktiv | BOOLEAN | Aktiv/Inaktiv |
| parent_investition_id | INTEGER | Foreign Key → Investitionen (für Parent-Child) |
| created_at | DATETIME | Erstellungsdatum |
| updated_at | DATETIME | Letztes Update |
Investitionstypen:
| Typ | Parameter (JSON) |
|---|---|
wechselrichter | - |
pv-module | anzahl_module, modul_leistung_wp, ausrichtung, neigung_grad |
speicher | kapazitaet_kwh, arbitrage_faehig |
e-auto | jahresfahrleistung_km, verbrauch_kwh_100km, pv_ladeanteil_prozent, v2h_faehig, ist_dienstlich |
waermepumpe | effizienz_modus, jaz, cop_heizung, cop_warmwasser, heizwaermebedarf_kwh, warmwasserbedarf_kwh, leistung_kw, pv_anteil_prozent, alter_energietraeger, alter_preis_cent_kwh, sg_ready |
wallbox | ist_dienstlich |
balkonkraftwerk | leistung_wp, anzahl, hat_speicher, speicher_kapazitaet_wh |
sonstiges | kategorie (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.
InvestitionMonatsdaten
Abschnitt betitelt „InvestitionMonatsdaten“| Feld | Typ | Beschreibung |
|---|---|---|
| id | INTEGER | Primary Key |
| investition_id | INTEGER | Foreign Key → Investitionen |
| jahr | INTEGER | Jahr |
| monat | INTEGER | Monat (1-12) |
| verbrauch_daten | JSON | Typ-spezifische Messwerte |
| einsparung_monat_euro | FLOAT | Berechnet |
| co2_einsparung_kg | FLOAT | Berechnet |
| created_at | DATETIME | Erstellungsdatum |
| updated_at | DATETIME | Letztes 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": ""}] }}Strompreise
Abschnitt betitelt „Strompreise“| Feld | Typ | Beschreibung |
|---|---|---|
| id | INTEGER | Primary Key |
| anlage_id | INTEGER | Foreign Key → Anlage |
| netzbezug_arbeitspreis_cent_kwh | FLOAT | Preis pro kWh Netzbezug |
| einspeiseverguetung_cent_kwh | FLOAT | Vergütung pro kWh Einspeisung |
| grundpreis_euro_monat | FLOAT | Monatlicher Grundpreis |
| gueltig_ab | DATE | Gültigkeitsbeginn |
| gueltig_bis | DATE | Gültigkeitsende (NULL = aktuell gültig) |
| tarifname | VARCHAR(255) | Name des Tarifs |
| anbieter | VARCHAR(255) | Stromanbieter |
| vertragsart | VARCHAR(50) | fix, dynamisch, etc. |
| verwendung | VARCHAR(30) | allgemein, waermepumpe oder wallbox |
| created_at | DATETIME | Erstellungsdatum |
| updated_at | DATETIME | Letztes Update |
InfothekEintrag (v3.5.0, N:M v3.15.2)
Abschnitt betitelt „InfothekEintrag (v3.5.0, N:M v3.15.2)“| Feld | Typ | Beschreibung |
|---|---|---|
| id | INTEGER | Primary Key |
| anlage_id | INTEGER | Foreign Key → Anlage |
| bezeichnung | VARCHAR(255) | Name des Eintrags |
| kategorie | VARCHAR(50) | Kategorie (15 Typen, siehe Handbuch) |
| notizen | TEXT | Freitext-Notizen (Markdown) |
| parameter | JSON | Kategorie-spezifische Felder |
| investition_id | INTEGER | Legacy 1:1 FK (deprecated, nur Migration) |
| ansprechpartner_id | INTEGER | FK → InfothekEintrag (Vertragspartner-Verweis) |
| sortierung | INTEGER | Reihenfolge |
| aktiv | BOOLEAN | Archiviert = false |
| in_anlagendoku | BOOLEAN | In Anlagendokumentation-PDF anzeigen |
| created_at | DATETIME | Erstellungsdatum |
| updated_at | DATETIME | Letztes Update |
15 Kategorien: stromvertrag, einspeisevertrag, gasvertrag, wasservertrag, fernwaerme, brennstoff, versicherung, ansprechpartner, wartungsvertrag, marktstammdatenregister, foerderung, garantie (Komponente/Datenblatt), steuerdaten, messstellenbetreiber, sonstiges
InfothekInvestition (Junction Table, N:M)
Abschnitt betitelt „InfothekInvestition (Junction Table, N:M)“| Feld | Typ | Beschreibung |
|---|---|---|
| id | INTEGER | Primary Key |
| infothek_eintrag_id | INTEGER | FK → InfothekEintrag (CASCADE) |
| investition_id | INTEGER | FK → Investitionen (CASCADE) |
InfothekDatei
Abschnitt betitelt „InfothekDatei“| Feld | Typ | Beschreibung |
|---|---|---|
| id | INTEGER | Primary Key |
| eintrag_id | INTEGER | FK → InfothekEintrag (CASCADE) |
| dateiname | VARCHAR(255) | Originaler Dateiname |
| dateityp | VARCHAR(10) | image oder pdf |
| mime_type | VARCHAR(50) | MIME-Typ |
| beschreibung | VARCHAR(255) | Optionale Beschreibung |
| daten | LARGEBINARY | Datei-BLOB (Bilder max ~500 KB, PDFs max 10 MB) |
| thumbnail | LARGEBINARY | Vorschaubild (nur für Bilder) |
| created_at | DATETIME | Erstellungsdatum |
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).
| Tabelle | Inhalt |
|---|---|
kanal | eine Größe einer Anlage: key im Snapshot-Schema (basis:<feld>, inv:<id>:<feld>, modus:inv:<id>:<betriebsart>), art ∈ sum · mean · stand, einheit |
kanal_quelle | Herkunft 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_statistik | eine 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 Stundecore/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>_kwhund…: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äleabgeleitet:basis:kosten_netzbezug·netzbezug_bewertet_kwh·kosten_ev_vermieden·ev_bewertet_kwh·preis_summe·preis_stunden, Regel der Stundecore/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 schreibtaggregate_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:
| Gruppe | Leser | Was er liefert |
|---|---|---|
| Bilanz (Netz · PV/Balkonkraftwerk samt Anlagenzähler · Speicher · Erzeuger hinter dem Zähler) | bilanz_leser.py | je 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) · Sonstiges | geraete_leser.py | je 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.py | je 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.py | je 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).
Parent-Child Beziehungen
Abschnitt betitelt „Parent-Child Beziehungen“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
Welches Datum beantwortet welche Frage?
Abschnitt betitelt „Welches Datum beantwortet welche Frage?“Gemessen 2026-08-13. Diese Unterscheidung hat schon zwei Abstürze verursacht (
installationsdatumauf einerInvestitiongelesen — 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:
| Frage | Maßgeblich | Ort |
|---|---|---|
| Auswertungsfilter — zählt diese Investition in diesem Monat? | aktiv · anschaffungsdatum · stilllegungsdatum der Investition | models/investition.py::ist_aktiv_im_zeitraum |
| Erwartungsrahmen — welcher Monat soll überhaupt erfasst sein? | Anlage.installationsdatum, Fallback ältestes Anschaffungsdatum der Erzeuger, dann erste vorhandene Zeile | core/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.
5. API-Architektur
Abschnitt betitelt „5. API-Architektur“Route-Übersicht
Abschnitt betitelt „Route-Übersicht“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-checkerund/api/system-logsliegen beide unter/api/system,/api/data-importheißt/api/portal-import,mqtt_gatewayundmqtt_presetsliegen unter/api/live,ha_exporthat gar kein eigenes Prefix).
Was architektonisch daran wichtig ist:
- Ein Modulname sagt nichts über sein Prefix. Mehrere Module teilen sich eines (
/api/liveträgt fünf,/api/systemzwei,/api/aussichtenzwei), und drei Module hängen direkt unter/api(Monatsabschluss, Community, HA-Export). SoT sind dieinclude_router-Aufrufe. - HA-only-Router werden bedingt eingehängt (
HA_MODE):ha_integration,sensor_mapping,ha_statistics.ha_remotenicht — 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.
Wichtige Endpoints
Abschnitt betitelt „Wichtige Endpoints“Cockpit (Dashboard-Daten)
Abschnitt betitelt „Cockpit (Dashboard-Daten)“GET /api/cockpit/uebersicht/{anlage_id}?jahr=2025 # Haupt-DashboardGET /api/cockpit/prognose-vs-ist/{anlage_id} # PVGIS SOLL vs ISTGET /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-ZeitreihenGET /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.
Aussichten (Prognosen)
Abschnitt betitelt „Aussichten (Prognosen)“GET /api/aussichten/kurzfristig/{anlage_id} # 7-Tage WetterprognoseGET /api/aussichten/wetter/{anlage_id} # Wetterdaten für PrognoseGET /api/aussichten/langfristig/{anlage_id} # 12-Monats-Prognose (PVGIS)GET /api/aussichten/trend/{anlage_id} # Trend-Analyse + DegradationGET /api/aussichten/finanzen/{anlage_id} # Finanz-Prognose + Amortisation4 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!)
Sensor-Mapping API
Abschnitt betitelt „Sensor-Mapping API“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
POSTschrieb weiter aufAnlage.sensor_mapping, ohne den Historie-Hinweis auszulösen, den die Datenquellen-Fläche seit Konzept #192 B zeigt. Die heutigen Wege sindGET/POST /api/datenquellen/{id}/felder(lesen · speichern) undGET /api/datenquellen/{id}/ha/sensoren(Sensor-Liste).Der Router bleibt supervisor-gebunden gemountet (
main.py,HA_INTEGRATION_AVAILABLE). ⚠ Diese Liste nannte bis dahin einPOST …/init-start-values, das es baumweit nie gab.
Mapping-Strategien:
sensor- Direkter HA-Sensorkwp_verteilung- Anteilige Verteilung nach kWpcop_berechnung- COP-basierte Berechnung (Wärmepumpe)ev_quote- Eigenverbrauchsquote-Berechnungmanuell- Manuelle Eingabe im Wizardkeine- Nicht erfassen
Monatsabschluss API
Abschnitt betitelt „Monatsabschluss API“GET /api/monatsabschluss/{anlage_id}/{jahr}/{monat} # Status + VorschlägePOST /api/monatsabschluss/{anlage_id}/{jahr}/{monat} # Abschluss durchführenGET /api/monatsabschluss/naechster/{anlage_id} # Nächster offener MonatGET /api/monatsabschluss/historie/{anlage_id} # Letzte AbschlüsseVorschlagService liefert intelligente Vorschläge:
vormonat(Konfidenz 80%) - Wert vom Vormonatvorjahr(Konfidenz 70%) - Wert vom gleichen Monat im Vorjahrberechnung(Konfidenz 60%) - COP/EV-Quote basierte Berechnungdurchschnitt(Konfidenz 50%) - Durchschnitt aller vorhandenen Werte
Scheduler API
Abschnitt betitelt „Scheduler API“GET /api/scheduler # Scheduler-StatusPOST /api/scheduler/monthly-snapshot # Manueller Monatswechsel-TriggerAggregierte Monatsdaten
Abschnitt betitelt „Aggregierte Monatsdaten“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:
| Konsument | Flag | Warum |
|---|---|---|
| Datensatz-Liste (Auswertungen → Tabelle) | aus | eine Zeile ohne Datensatz kann man weder bearbeiten noch löschen |
| Zeitreihe (Cockpit → Jahr: Verlauf, Rail, Vorjahr/Ø-Spalten) | an | sonst 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.
CSV Import
Abschnitt betitelt „CSV Import“POST /api/import/csv/{anlage_id}Content-Type: multipart/form-data
file: [CSV-Datei]Verarbeitung:
- CSV parsen (flexible Spalten-Erkennung)
- Basis-Felder → Monatsdaten-Tabelle
- Investitions-Felder → InvestitionMonatsdaten-Tabelle
- Duplikate: Upsert (überschreiben)
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)
JSON Export/Import (Export-Version 1.1)
Abschnitt betitelt „JSON Export/Import (Export-Version 1.1)“GET /api/import/export/{anlage_id} # Vollständiger JSON-ExportGET /api/import/template/{anlage_id} # CSV-Template herunterladenGET /api/import/template/{anlage_id}/download # CSV-Template DownloadGET /api/import/pdf/{anlage_id} # PDF-ExportPOST /api/import/demo # Demo-Daten erstellenDELETE /api/import/demo # Demo-Daten löschenExport - 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
Request/Response Pattern
Abschnitt betitelt „Request/Response Pattern“# Typisches Schema-Patternclass 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 Felder6. Frontend-Architektur
Abschnitt betitelt „6. Frontend-Architektur“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.
Drei Achsen, eine Frage je Achse
Abschnitt betitelt „Drei Achsen, eine Frage je Achse“Seit dem IA-V4-Flip (v4.0.0) ist die Oberfläche nach Fragen geschnitten, nicht nach Geräten:
| Achse | Frage | Sektion | Unterteilung |
|---|---|---|---|
| Zeit | Wann? | Cockpit | Live · Tag · Monat · Jahr · Aussicht |
| Gerät | Was? | Komponenten | je Typ: Status → Verlauf → Vergleich → Wirtschaftlichkeit |
| Methode | Wie? | Auswertungen | Finanzen · ROI · Prognose-vs-IST · CO₂ · Tabelle |
| — | Vergleich | Community | Übersicht · PV-Ertrag · Komponenten · Regional · Trends · Statistiken |
| — | Einrichtung | Einstellungen | Kachel-Ü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.
Routing-Struktur
Abschnitt betitelt „Routing-Struktur“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, vonApp.tsxals<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 inApp.tsxsynchron bleiben.src/routes/redirects.test.tsxprü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.
Komponenten-Hierarchie
Abschnitt betitelt „Komponenten-Hierarchie“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 · HilfeV4Das Block-Modell
Abschnitt betitelt „Das Block-Modell“Eine V4-Sicht ist eine Liste von Blöcken, nicht ein Seitenlayout. Jeder Block ist verschiebbar, fokussierbar (⤢) und parkbar:
| Baustein | Ort | Aufgabe |
|---|---|---|
BlockShell | components/blocks/ | Rahmen, Titel, Fokus-Knopf, Parken-Geste |
FokusVollbild · FokusKachel | components/blocks/ | ein geteiltes Overlay für alle Sichten |
BlockStackSkeleton | components/blocks/ | Ladezustand als Block-Stapel statt Spinner |
KpiStrip · VerteilungsBalken · HerkunftZeile | components/blocks/ | wiederkehrende Block-Inhalte |
Parkbar · GeparktBlock · ParkContext · ParkFuss | components/park/ | Park-Zustand je Sicht (persistiert) |
useSectionOrder | hooks/ | 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.
State Management
Abschnitt betitelt „State Management“Kein globaler Store. Vier Mechanismen, jeder mit klarer Zuständigkeit:
useApiData(fetcher, deps, opts)für Server-State — inklusive SWR-Sicht-Cache (opt-in überswrKey). Ohne ihn fetchte jede Sicht beim Tab-Wechsel neu und zeigte ein Skeleton, obwohl die Daten Sekunden alt waren (#218). MitswrKeystehen 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().- Context für Theme (
ThemeContext) und den globalen Status (GlobalStatusProvider). localStoragefür Präferenzen: Anlagen-Auswahl, Block-Reihenfolge, Park-Zustand, Spalten-Toggles, Wizard-Fortschritt.- URL für alles, was ein Link tragen muss: Sektion, Zeitraum-Achse, Gerätetyp, Unter-Sicht.
Custom Hooks (src/hooks/)
Abschnitt betitelt „Custom Hooks (src/hooks/)“| Hook | Zweck |
|---|---|
useApiData | async Fetch mit Loading/Error + SWR-Sicht-Cache |
useSelectedAnlage | Anlagen-Auswahl mit Auto-Select und Persistierung |
useAnlagen · useInvestitionen · useMonatsdaten · useStrompreise | Bestandsdaten |
useYearSelection | verfügbare Jahre + Auswahl |
useSectionOrder | Block-Reihenfolge je Sicht |
useEinstellungenStatus | Status-Ampeln der Einstellungs-Kacheln |
useHelpKatalog · useFeldHinweise | In-App-Hilfe und Feld-Hinweise |
useHAAvailable | HA-only-Funktionen ausblenden, wenn kein HA da ist |
useLegendenToggle · useSchmaleAchse · useScrollErhalt · useTouchTitleTooltip | Anzeige-Verhalten (auch mobil) |
useSetupWizard | Ersteinrichtung |
Geteilte SoT-Komponenten (src/components/)
Abschnitt betitelt „Geteilte SoT-Komponenten (src/components/)“Eine Komponenten-Klasse = EINE Komponente (I12). Eine zweite Fassung neben einer bestehenden ist ein Regelbruch, nicht eine Variante:
| Verzeichnis | Inhalt |
|---|---|
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ätespezifisch | pv/ · speicher/ · waermepumpe/ · wallbox/ · eauto/ · balkonkraftwerk/ |
| fachlich | finanzen/ · roi/ · aussicht/ · prognose/ · tag/ · werte/ · infothek/ · repair/ |
Utility-Library (src/lib/)
Abschnitt betitelt „Utility-Library (src/lib/)“| Modul | Inhalt |
|---|---|
colors.ts | Farb-SoT. Eine Datenrolle = eine Farbe; Inline-Hex außerhalb ist gewächtert (check:design) |
einheiten.ts | Zahl-/Einheiten-Formatierung (kWh, €, %, CO₂) — deutsche Konvention, % mit Leerzeichen |
datum.ts | heuteIso · toIsoDatum · verschiebeIsoTage — lokale Uhr, nie UTC (check:datum-utc) |
monatsLuecken.ts | „welcher Monat ist offen” inkl. Binnen-Lücken |
chartAchse.ts · blockStyle.ts · komponentenStyle.ts | Achsen- und Stil-Ableitungen |
investitionAktiv.ts | die drei Achsen von „aktiv” (Flag · Anschaffung · Stilllegung) |
investitionParameter.ts | Parameter-Keys gemeinsam mit dem Backend (core/investition_parameter.py) |
erzeugerSpalten.ts · pvHerkunft.ts · erfassungZustand.ts · sollErfuellung.ts | Tabellen- und Zustands-Ableitungen |
stundenSlot.ts | Slot-Konvention im Client (Backward, wie core/berechnungen/slot_konvention.py) |
prognoseAnzeige.ts · prognoseHinweise.ts | Prognose-Darstellung und ihre Hinweistexte |
calculations.ts · constants.ts · fieldDefinitions.ts · flags.ts · download.ts | Rest |
Was der Client NICHT tut
Abschnitt betitelt „Was der Client NICHT tut“- Keine CO₂-Menge konstruieren — sie kommt aus
/cockpit/nachhaltigkeit(ADR-001/DI-2, Wächtercheck:co2-roh). - Keine Nennleistung aus der Rohspalte rechnen — Anzeige und Rechnung lesen
leistung_kwp_effektivaus der Response; die Rohspalte gehört Formularen (P3-a, Wächtercheck:kennwert-roh). - Keine Monatsgröße selbst falten — das ist Aufgabe der Monats-Fakten im Backend (P10).
7. Services
Abschnitt betitelt „7. Services“Lese-Schichtung: von der Tabelle zur Kennzahl
Abschnitt betitelt „Lese-Schichtung: von der Tabelle zur Kennzahl“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 · CommunityDie 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.
| Schicht | Darf | Darf nicht |
|---|---|---|
services/monats_fakten/ | laden, filtern, kanonisch auflösen, Layer-Helfer rufen | selbst rechnen (das wäre eine Formel-Duplikation) |
core/berechnungen/ | rechnen | eine Session sehen |
api/routes/… | darstellen, Zeiträume wählen | InvestitionMonatsdaten 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 Phasefinanz_eingaenge.py),services/pdf/builders/jahresbericht.pyundapi/routes/investitionen/crud.py(ROI-Dashboard, seit 18.09.2026investitionen/roi.py) — Befund F-5. Der PDF-Builder lädtInvestitionMonatsdatenseither gar nicht mehr selbst.Umgehängt (S3, 2026-07-31):
cockpit/nachhaltigkeit.py(CO₂-Zeitreihe) undcockpit/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 ausberechne_co2_bilanz(ADR-001/DI-2) statt aus drei lokalen Formeln. Damit ist auch die P7-Ausnahmecockpit/social.py::mdgestrichen. Nachtrag (Paket B, 2026-07-31):cockpit/social.pyist 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 (getNachhaltigkeitstand 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 dahinErzeugung × 0,38im Client und stand damit als zweite Zahl neben dem neuen Cockpit-Block; sie liest ihn jetzt überuseAuswertungBasis().co2(EIN Abruf, geteilt mit der Werte-Tabelle). Dasselbe Spiegelbild im Backend warservices/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.pyundha_export/anlage_energie.py::monatsfakten_und_energie(bis 18.09.2026ha_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 erfasstelade_pv_je_monatnicht, 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.2026dashboard_<typ>.pyje 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 einzigenist_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 inaussichten.py(N-1) und der Prognose-vs-IST-Vergleich incockpit/prognose.py(N-14, in der Inventur nicht erhoben). Neue Layer-Formelbkw_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 inP10_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/sharebricht 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_kwhbleibt bewusst die PV-Achse ohne sonstigen Erzeuger — der Server bildet daraus den spezifischen Ertrag je kWp. Cross-Repo (Konzept §11): das Datenmodell voneedc-communityblieb strukturell unverändert (der Payload validiert unverändert gegenbackend/schemas.py); geändert hat sich die Bedeutung dreier Feldgruppen, die deshalb im selben Paket als Vertrag in den Docstring vonMonatswertInputgewandert 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 übersummiere_emob_quelleabgeleitet statt roh addiert (wer nur Gesamt-Ladung und PV-Anteil pflegt, verlor die Netzladung still), und die §51-Menge ist ohne MitschriftNonestatt 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_zeileundSonstigesFakten.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 keineInvestitionMonatsdatenund keineMonatsdatenmehr; §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 alsmax()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
JahrAggregatdie Monate summiert.Dafür sind der Schicht die vier Größen additiv nachgezogen worden, die ihr fehlten (
SonstigesFakten: Eigenverbrauch, Einspeisung, Bezug PV, Bezug Netz) — samtje_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.
Kategorie Bedeutung Stand nach C1d P10_SCHREIBEN_IMPORT_CHECKERschreibt, prüft oder reicht durch — leitet nichts ab; auf Dauer legitim 15 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 soll 0 — 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_nichtdeckelt 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_monatsteht seither inP10_PER_INVESTITION— selbst geladen wird dort nur noch die Zuordnunginv → verbrauch_datenfür die Financial-Zeile je Investition. Teilmigriert bleibenaussichten.pyundha_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 (
MonatsdatenoderInvestitionMonatsdaten); der Docstring sagte es offen („Monate ohne jede Spur fehlen”). Weil es keinen automatischen Monatsabschluss gibt —scheduler.py::monthly_snapshot_jobsetzt 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 inMetaFakten.tageswert_gruppenund geht alsaus_tageswertenbis in die Antwort — eine Sicht muss nicht aus einer 0 raten (P4). Es entsteht keine zweite Tag→Monat-Faltung: gefaltet wird mit dem Layer-Helferbilanz_aus_stundenrowsüber die Stunden des Monats (Σ ist assoziativ, additive Symmetrie zum Tag bleibt) plussumme_pv_anlage_kwh/summe_bkw_kwhaufkomponenten_kwh;rollup_monthist 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.pyje 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 ausMonatsFakt.emob.dienstlich_*und bewerten sie über die neue Layer-Formelberechne_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.
Wetter-Service (Multi-Provider)
Abschnitt betitelt „Wetter-Service (Multi-Provider)“Dateien:
backend/services/wetter_service.py– Multi-Provider Orchestrierungbackend/services/brightsky_service.py– DWD-Daten via Bright Sky APIbackend/services/solar_forecast_service.py– Open-Meteo Solar mit GTI
Funktion: Wetterdaten für Globalstrahlung und Sonnenstunden aus verschiedenen Quellen.
Verfügbare Provider:
| Provider | Region | Beschreibung |
|---|---|---|
| auto (Standard) | - | Automatische Auswahl: Anlage.standort_land entscheidet, ersatzweise die Koordinaten-Box (nutze_brightsky, #386) |
| brightsky | Deutschland | DWD-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-meteo | Weltweit | Open-Meteo Archive API |
| open-meteo-solar | Weltweit | Open-Meteo Solar mit GTI für geneigte Module |
Fallback-Kette:
- Gewählter Provider
- Alternative (z.B. Open-Meteo wenn Bright Sky fehlschlägt)
- PVGIS TMY (langjährige Durchschnittswerte)
- Statische Defaults (Mitteleuropa-Durchschnitt)
API-Endpoints:
GET /api/wetter/monat/{anlage_id}/{jahr}/{monat}?provider=autoGET /api/wetter/monat/koordinaten/{lat}/{lon}/{jahr}/{monat} # Direkt per KoordinatenGET /api/wetter/provider/{anlage_id} # Verfügbare ProviderGET /api/wetter/vergleich/{anlage_id}/{jahr}/{monat} # Provider-VergleichGET /api/solar-prognose/{anlage_id}?tage=7 # GTI-basierte PV-PrognoseGTI (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
Sensor-Export Service
Abschnitt betitelt „Sensor-Export Service“Datei: backend/services/ha_sensors_export.py
Funktion: Definition und Berechnung aller exportierbaren KPIs.
Sensor-Kategorien:
ANLAGE_SENSOREN– PV-GesamtINVESTITION_SENSOREN– ROI, AmortisationE_AUTO_SENSOREN– MobilitätWAERMEPUMPE_SENSOREN– WärmeSPEICHER_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_classnur setzen, wenn HAs Vertrag es hergibt. Das Beispiel oben ist ein echter Zähler, deshalbenergy+total_increasing. Fürenergylässt HA ausschließlichtotal/total_increasingzu, fürmonetarynurtotal; 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 keindevice_classundstate_class="measurement". Gewächtert:backend/tests/test_ha_export_sensor_klassen_vertrag.py.
MQTT Client
Abschnitt betitelt „MQTT Client“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 Sensorpublish_all_sensors()- beides über eine Verbindung für eine ganze Sensorlisteremove_sensors()- Rücknahme bestandsgetrieben: zusätzlich zu den Topics der heutigen Definitionen fragt sie überverwaiste_topics()den Broker, was unter dem Präfix dieser Anlage sonst noch retained liegt, und räumt es mit (altlast_topicsin der Antwort) — außer dem Zweig…/monatsdaten/(finale Monatsdaten, keine Sensoren). Damit fallen auch Entitäten früherer Fassungen weg — konkret dienumber.eedc_*_mwd_*_startaus der Zeit vor77c6e211(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 → Discoveryeedc/anlage/{anlage_id}/{key} → Stateeedc/anlage/{anlage_id}/{key}/attributes → Attributeseedc/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).
HA Statistics Service
Abschnitt betitelt „HA Statistics Service“Datei: backend/services/ha_statistics_service.py
Funktion: Zugriff auf die Home-Assistant-Langzeitstatistik.
Drei Transporte, eine Quelle (Reihenfolge = Vorrang):
| # | Transport | Voraussetzung |
|---|---|---|
| 1 | HA_RECORDER_DB_URL (SQL) | externer Recorder, MariaDB/MySQL |
| 2 | Recorder-Datei (SQL) | Volume-Mapping homeassistant_config (read-only, path: /config) auf /config/home-assistant_v2.db |
| 3 | WebSocket recorder/statistics_during_period | nur 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-Statistikget_alle_monatswerte()- Bulk-Abfrage aller historischen Monateget_verfuegbare_monate()- Liste aller Monate mit Datenget_monatsanfang_wert()- Zählerstand am Monatsanfang für MQTT-Startwerte
API-Endpoints:
GET /api/ha-statistics/status # Prüft DB-VerfügbarkeitGET /api/ha-statistics/monatswerte/{anlage_id}/{jahr}/{monat} # Einzelner MonatGET /api/ha-statistics/verfuegbare-monate/{anlage_id} # Alle Monate mit DatenGET /api/ha-statistics/alle-monatswerte/{anlage_id} # Bulk-AbfrageGET /api/ha-statistics/monatsanfang/{anlage_id}/{jahr}/{monat} # Zählerstand am MonatsanfangGET /api/ha-statistics/import-vorschau/{anlage_id} # Vorschau mit KonfliktenPOST /api/ha-statistics/import/{anlage_id} # Import mit Überschreib-SchutzVorschlagService
Abschnitt betitelt „VorschlagService“Datei: backend/services/vorschlag_service.py
Funktion: Intelligente Vorschläge für Monatsabschluss-Wizard.
Vorschlags-Hierarchie:
- Vormonat (Konfidenz 80%) - Bester Indikator für kontinuierliche Werte
- Vorjahr (Konfidenz 70%) - Gleicher Monat, saisonale Korrelation
- Berechnung (Konfidenz 60%) - COP/EV-Quote basiert
- Durchschnitt (Konfidenz 50%) - Fallback aus allen vorhandenen Werten
Scheduler Service
Abschnitt betitelt „Scheduler Service“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-ID | Takt | Aufgabe |
|---|---|---|
sensor_snapshot | stündlich :05 | Zählerstände je Sensor in sensor_snapshots — die kWh-Quelle der Stunden-/Tageswerte |
sensor_snapshot_preview | stündlich :55 | Vorschau-Snapshot kurz vor dem Stundenwechsel |
sensor_snapshot_5min | alle 5 min (:30 s) | 5-Minuten-Snapshots für den Live-Tagesverlauf |
sensor_snapshot_5min_cleanup | täglich 00:30 | 5-Minuten-Daten aufräumen |
energie_profil_heute | alle 15 min | den laufenden Tag fortschreiben |
energie_profil_aggregation | täglich 00:15 | Vortag aggregieren → TagesEnergieProfil + TagesZusammenfassung |
energie_profil_aggregation_recovery | täglich 02:15 | zweiter Anlauf für Tage, die 00:15 verpasst hat (Neustart, Ausfall) |
energie_profil_archiv_nachzug | täglich 02:20 | den 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_aggregation | täglich 02:30 | gelernte eedc-Prognose-Korrektur neu bilden |
prognose_prefetch | alle 45 min | Wetter-/Prognosedaten vorhalten |
connector_daily_poll | täglich 03:30 | Geräte-Connectors im lokalen Netz abfragen |
api_cache_cleanup | täglich 04:00 | api_cache aufräumen |
kraftstoffpreis | täglich 06:00 + Startlauf | EU 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_snapshot | 1. des Monats 00:01 | ⚠ setzt nur einen Log-Zeitstempel — es gibt keinen automatischen Monatsabschluss |
mqtt_auto_publish | Intervall (konfigurierbar) | HA-Export per MQTT, mit Start-Publish beim Hochlauf |
mqtt_energy_snapshot · mqtt_live_snapshot | je alle 5 min | MQTT-Inbound: Energie-Zählerstände bzw. Live-Werte sichern |
mqtt_energy_cleanup · mqtt_live_cleanup | täglich 03:00 / 03:05 | MQTT-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).
Kraftstoffpreis Service (v3.16.16)
Abschnitt betitelt „Kraftstoffpreis Service (v3.16.16)“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 TagesZusammenfassungbackfill_kraftstoffpreise(anlage_id, land, db)— Befüllt TagesZusammenfassungbackfill_monatsdaten_kraftstoffpreise(anlage_id, land, db)— Befüllt Monatsdaten
HA MQTT Sync Service
Abschnitt betitelt „HA MQTT Sync Service“Datei: backend/services/ha_mqtt_sync.py
Funktion: Koordiniert MQTT-Sensoren basierend auf Sensor-Mapping.
Hauptfunktionen:
setup_sensors_for_anlage()- Erstellt alle MQTT Entitiestrigger_month_rollover()- Führt Monatswechsel durch
Live Power Service (v3.0.0, refactored v3.9.0)
Abschnitt betitelt „Live Power Service (v3.0.0, refactored v3.9.0)“Dateien: 7 Module in backend/services/
| Modul | Verantwortlichkeit |
|---|---|
live_power_service.py (313 Z.) | Orchestrierung: ruft die anderen Module auf |
live_sensor_config.py | Konstanten, extract_live_config(), normalize_to_w() |
live_kwh_cache.py | TTL-Caches für heute/gestern/profil kWh-Werte |
live_history_service.py | HA-History-Abruf, Trapez-Integration, Tages-kWh |
live_verbrauchsprofil_service.py | Verbrauchsprofil aus HA-History oder MQTT |
live_tagesverlauf_service.py | Butterfly-Chart Datenaufbereitung |
live_komponenten_builder.py | Komponenten, Gauges, Summenbildung |
Datenquellen (Priorität):
- HA-Sensoren (via Sensor-Mapping) — direkte REST-API-Abfrage
- MQTT-Inbound Cache — In-Memory-Werte von subscribten Topics
Berechnung:
- Jede Investition wird als
LiveKomponentemiterzeugung_kw/verbrauch_kwgeliefert - 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/):
| Router | Endpoints |
|---|---|
live_dashboard.py (356 Z.) | GET /api/live/{id}, GET /api/live/{id}/tagesverlauf |
live_mqtt_inbound.py | GET/POST/DELETE /api/live/mqtt/* |
live_wetter.py | GET /api/live/{id}/wetter + Helfer |
MQTT Inbound Service (NEU v3.0.0)
Abschnitt betitelt „MQTT Inbound Service (NEU v3.0.0)“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-Werteget_all_energy_raw()— Aktuelle Zählerstände für Snapshot-Service
MQTT Energy History Service (NEU v3.0.0)
Abschnitt betitelt „MQTT Energy History Service (NEU v3.0.0)“Datei: backend/services/mqtt_energy_history_service.py
Funktion: SQLite-basierte Mini-History für Tageswerte aus MQTT Energy-Daten.
Mechanismus:
- Tabelle
mqtt_energy_snapshotsspeichert Zählerstand-Snapshots alle 5 Minuten - Tageswert = Differenz zwischen aktuellem Stand und Mitternacht-Snapshot
- Retention: 31 Tage, automatischer Cleanup um 03:00
Energieprofil Service (NEU v3.1.0)
Abschnitt betitelt „Energieprofil Service (NEU v3.1.0)“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.
| Modul | Rolle |
|---|---|
aggregator.py | aggregate_day() — der einzige Schreiber von TagesEnergieProfil + TagesZusammenfassung |
scheduler_jobs.py | aggregate_today_all() (laufender Tag), aggregate_yesterday_all() (Vortag) |
backfill.py | backfill_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.py | die Stunden-Leistungskurve aus HA-LTS (ein gebündelter Read je Bereich) |
aggregations_quelle.py | Vorbedingung: gibt es überhaupt eine Quelle für diesen Tag? |
rollup.py | rollup_month() — Tageszeilen → Monatsdaten-Felder |
_helpers.py | Wetter-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:
| Tabelle | Granularität | Inhalt |
|---|---|---|
TagesEnergieProfil | 24 Zeilen/Tag/Anlage | Stündliche kW-Werte: PV, Verbrauch, Einspeisung, Netzbezug, Batterie, Überschuss, Defizit + Wetter (Temperatur, Globalstrahlung) + SoC + Komponenten-JSON |
TagesZusammenfassung | 1 Zeile/Tag/Anlage | Tagessummen (Ü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-FelderDie 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.
Was einen Wert unterwegs absichert
Abschnitt betitelt „Was einen Wert unterwegs absichert“Zehn Mechanismen, jeder mit seinem Anlass. Sie greifen nacheinander, nicht alternativ:
| # | Mechanismus | Wo | Wogegen |
|---|---|---|---|
| 1 | Quellen-Vorprüfung | aggregations_quelle.py | Ein Lauf ohne jede Quelle bricht vor dem Löschen ab, statt eine leere Zeile zu schreiben |
| 2 | Zwei Zählerpfade | lts_aggregator → sensor_snapshot_service | HA-LTS ist SoT; fehlt sie (Standalone/MQTT), übernimmt der Snapshot-Pfad mit demselben Ausgabe-Vertrag |
| 3 | Lücken-Interpolation | snapshot/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 |
| 4 | Either-Or je Fallback-Gruppe | snapshot/aggregator.py (#298) | Zwei Sensoren für dieselbe Größe zählen nicht doppelt — Auflösung auf Tages-Ebene, nicht je Stunde |
| 5 | Plausibilitäts-Cap | snapshot/aggregator.py, snapshot/fallback.py (#184) | Ein Zähler-Rücksprung oder ein Lifetime-großer Stunden-Spike wird gekappt statt eingebucht |
| 6 | Counter-Σ aus dem Tages-Diff | verteile_counter_auf_stunden | Bei Lücken wird die Stunden-Σ so reskaliert, dass sie auf den Tages-Boundary-Diff führt — eine Quelle je Tag |
| 7 | Rettung fremdbefüllter Felder | aggregator.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 |
| 8 | Drei Konsistenz-Invarianten | aggregator.py am Ende des Laufs | Achse 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 |
| 9 | Selbstheilung + Nachlauf | energie_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 |
| 10 | Archiv-Nachzug der Wetterzeile | energie_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_snapshotsetzt 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:
| Funktion | Trigger | Beschreibung |
|---|---|---|
aggregate_day() | alle Anlässe | Der einzige Schreiber. Holt Tagesverlauf + Wetter + SoC + Betriebsmodus + Preise, schreibt 24 Stundenzeilen + Tageszusammenfassung. Idempotent (Delete + Insert) |
aggregate_today_all() | Scheduler alle 15 min | Der laufende Tag für alle Anlagen — er wird dabei jedes Mal vollständig neu gerechnet |
aggregate_yesterday_all() | Scheduler 00:15 + 02:15 | Vortag für alle Anlagen mit Sensor-Mapping; 02:15 ist der zweite Anlauf (#136) |
archiv_nachzug_all() | Scheduler 02:20 | Der 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() | Monatsabschluss | Datumsbereich ü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() | Monatsabschluss | Aggregiert 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):
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).rollup_month()— Tagesdaten in Monatsdaten verdichten (Vollzyklen, Spitzen, PR hängen an Stundenwerten — bis S3).- entfallen mit E4f: der einmalige Auto-Vollbackfill beim ersten Abschluss nach einem Upgrade (Flag
Anlage.vollbackfill_durchgefuehrtbleibt als Spalte, ohne Leser). Lücken füllt die Werkbank auf Knopfdruck. 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.
8. Design-Entscheidungen
Abschnitt betitelt „8. Design-Entscheidungen“Warum Standalone-First?
Abschnitt betitelt „Warum Standalone-First?“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
Warum InvestitionMonatsdaten statt Monatsdaten?
Abschnitt betitelt „Warum InvestitionMonatsdaten statt Monatsdaten?“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”).
Warum Parent-Child für PV-Module?
Abschnitt betitelt „Warum Parent-Child für PV-Module?“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
Warum JSON für Parameter?
Abschnitt betitelt „Warum JSON für Parameter?“Grund: Flexibilität bei typ-spezifischen Feldern:
- Speicher braucht
kapazitaet_kwh, E-Auto brauchtv2h_faehig - Schema-Evolution ohne DB-Migrationen
- SQLite unterstützt JSON-Queries
Achtung: SQLAlchemy erkennt JSON-Änderungen nicht automatisch!
from sqlalchemy.orm.attributes import flag_modifiedinvestition.parameter["key"] = "value"flag_modified(investition, "parameter")db.commit()Warum 0-Werte explizit prüfen?
Abschnitt betitelt „Warum 0-Werte explizit prüfen?“Problem: Python wertet 0 als False:
if val: # Falsch! 0 wird als False gewertetif 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!)“| Metrik | Wo | Formel | Bedeutung |
|---|---|---|---|
| ROI p.a. | Auswertungen → ROI | Jahres-Einsparung / Relevante Kosten × 100 | Rendite pro Jahr (p.a.) — das Cockpit zeigt keine Rendite; jahres_rendite_prozent der Übersicht liest nur der Jahresbericht-PDF („Rendite”, N-699) |
| Amortisations-Fortschritt | Aussichten/Finanzen | Kum. Erträge / Investition × 100 | Wie 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_euroParameter) - E-Auto: Kosten minus Verbrenner (
alternativ_kosten_euroParameter)
Community-Integration
Abschnitt betitelt „Community-Integration“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 Faltungbackend/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 anfrontend/src/v4/CommunityShareBlock.tsx– teilen und rückwirkend entfernenfrontend/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.
Cloud-Import-Provider (v2.7.0+)
Abschnitt betitelt „Cloud-Import-Provider (v2.7.0+)“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):
| Provider | Status |
|---|---|
| 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 isteon.gridx.de/login. Der Provider trägt deshalb den E.ON-Home-Realm — wer dort „Viessmann” einsetzt, sperrt alle Anwender aus. Die Provider-ID bleibtviessmann_gridbox: sie steht im Provenance-Vokabular (core/source_priority.py), in gespeichertenconnector_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 mehrmy.gridx, und der Bearer ist deraccess_token, nicht derid_token. Beide alten Formen werden nur noch während einer Gnadenfrist akzeptiert. Gewächtert vonbackend/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.
Custom-Import (v2.8.0)
Abschnitt betitelt „Custom-Import (v2.8.0)“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 erkennenPOST /api/custom-import/preview # Mapping anwenden, VorschauGET /api/custom-import/templates # Gespeicherte Templates ladenPOST /api/custom-import/templates/{name} # Template speichernDELETE /api/custom-import/templates/{name} # Template löschenGET /api/custom-import/fields # Verfügbare EEDC-Zielfelder9. Entwickler-Workflow
Abschnitt betitelt „9. Entwickler-Workflow“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.
Der Fluss zwischen den Repositories
Abschnitt betitelt „Der Fluss zwischen den Repositories“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 imeedc-Spiegel. - Immer auf
main(Einzelentwickler-Projekt, keine Feature-Branches), keingit 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).
Was ein Release technisch bedeutet
Abschnitt betitelt „Was ein Release technisch bedeutet“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.
Community-Datenfluss
Abschnitt betitelt „Community-Datenfluss“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.
Anhang: API-Referenz
Abschnitt betitelt „Anhang: API-Referenz“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.