Thinking House Lab
DE

Sprache

  • English
  • Deutsch
  • Français · folgt
  • Español · folgt
  • 中文 · folgt

Power Flow Logic · Folge 1 · Stufe 3

Nachbauen: das Akku-Konto als Code

Die Regeln aus Folge 1 als kleiner, lesbarer Python-Code: ein Konto, das weiß, was jede Kilowattstunde im Akku gekostet hat, und eine Regel, die mit dem Preis der richtigen Quelle zwischen Wärmepumpe und Gas entscheidet. Jedes Beispiel aus den Videos ist ein Test, den du selbst laufen lassen kannst.

NachbauenKostenlos14 Min. Lesezeit

Das Problem in einem Absatz

Ein Heimspeicher lädt aus zwei Quellen: aus der Sonne, die nichts mehr extra kostet, wenn die Module bezahlt sind, und aus dem Netz, das so viel kostet, wie der Tarif in diesem Moment verlangt. Bei einem dynamischen Tarif ändert sich das jede Viertelstunde. Bewertet ein System den Akku mit der falschen Zahl, entscheidet es falsch. In Folge 1 war der Akku zu 99 % mit fast kostenlosem Sonnenstrom gefüllt, und trotzdem schaltete das System die Wärmepumpe ab: Es schaute auf den Netzpreis des Moments (98 ct) statt auf den Preis dessen, was wirklich im Akku war. Die Lösung ist Buchhaltung: den Akku wie ein Konto führen, das festhält, was hineinging und was es gekostet hat, und für jede Entscheidung den Preis der Quelle nehmen, aus der der Strom wirklich käme.

Kernfakten

Das Akku-Konto auf einen Blick
FrageAntwort
Was es istEin Konto mit zwei Zahlen: der Energie im Akku (kWh) und dem, was genau diese Energie gekostet hat (€). Der Preis je kWh ist Kosten ÷ Energie.
Wann es buchtBei jeder Ladung: Sonnenstrom zu 0 ct, Netzstrom zum Preis dieses Moments. Bei jeder Entnahme: Energie und Kosten sinken, zum Durchschnittspreis. Ein laufendes System bucht etwa einmal pro Minute aus Leistungswerten.
Was es nie buchtEine Ladung, deren Quelle unbekannt ist. Akkuverschleiß: Er ist eine eigene Zahl und nie Teil des Preises. Lade- und Entladeverluste sind nicht abgebildet.
Warum es zähltJede Entscheidung, die Akkustrom nutzt, braucht dessen echten Preis: jetzt Wärmepumpe oder Gas, später auch, ob aus dem Netz geladen wird.
Worauf es wirktAuf die Wärmepreis-Entscheidung: Preis der Quelle ÷ COP gegen Gaspreis ÷ Kesselwirkungsgrad. Gas gewinnt erst oberhalb des Kipppunkts, Gas-Wärmepreis × COP.
EingängeAkkuleistung, PV-Leistung, Netzleistung und Netzpreis. Jeder Wert darf unbekannt sein, und unbekannt wird nie wie null behandelt.
AusgängeDer Preis je kWh im Akku (oder kein Preis, wenn er leer ist), die Quelle der nächsten kWh (Sonne, Akku oder Netz) und ob die Wärmepumpe laufen darf.

Die Rechnung

Das Konto ist ein gewichteter Mittelwert. Jede Kilowattstunde bringt den Preis mit, zu dem sie gekauft wurde, und der Preis des ganzen Inhalts ergibt sich aus zwei Summen:

Preis = Kosten ÷ Energie = Σ (kWh × bezahlter Preis) ÷ Σ kWh

Laden mischt die neue Energie unter das, was schon drin ist. Entnehmen nimmt sie zum aktuellen Durchschnitt heraus, deshalb bleibt der Preis genau, wo er war:

nach einer Ladung: neuer Preis = (Energie × alter Preis + geladene kWh × ihr Preis) ÷ (Energie + geladene kWh)

nach einer Entnahme von x kWh: Energie − x, Kosten − x × Preis → Preis unverändert

Das Konto durch Folge 1, Schritt für Schritt
  1. kein Preis

    0 kWh (Sonne 0 kWh, Netz 0 kWh)

    Leerer Akku

  2. 0 ctje kWh

    10 kWh (Sonne 10 kWh, Netz 0 kWh)

    +10 kWh Sonne

  3. 10 ctje kWh

    15 kWh (Sonne 10 kWh, Netz 5 kWh)

    +5 kWh Netz zu 30 ct

  4. 10 ctje kWh

    5 kWh (Sonne 3,3 kWh, Netz 1,7 kWh)

    −10 kWh ins Haus

  5. 20 ctje kWh

    10 kWh (Sonne 3,3 kWh, Netz 6,7 kWh)

    +5 kWh Netz zu 30 ct

  • Sonne · 0 ct
  • Netz · 30 ct

Beispielwerte aus Folge 1. Die Balken zeigen, was im Akku ist, nach Herkunft; die Zahl über jedem Balken ist der Kontopreis. Die Website rechnet sie mit denselben Regeln wie das Code-Paket.

Achte auf den vierten Schritt: Das Haus hat zwei Drittel der Energie entnommen, und der Preis hat sich nicht bewegt. Erst der fünfte Schritt ändert ihn, weil neue Energie zu einem anderen Preis hinzukommt. Derselbe Effekt erklärt den fast leeren Akku aus Stufe 2: 9 kWh Sonne plus 1 kWh aus dem Netz zu 40 ct ergeben 4 ct je kWh, 1 kWh Sonne plus 9 kWh zu 40 ct ergeben 36 ct. Was schon drin ist, entscheidet.

Bei Wärme wird Wärme mit Wärme verglichen. Eine Wärmepumpe macht aus einer Kilowattstunde Strom COP Kilowattstunden Wärme; ein Gaskessel macht aus einer Kilowattstunde Gas etwas weniger als eine Kilowattstunde Wärme:

Wärme aus der Wärmepumpe = Preis der Quelle ÷ COP · Gaswärme = Gaspreis ÷ Wirkungsgrad

Kipppunkt = Gas-Wärmepreis × COP

Die Zahlen aus Folge 1 (COP 3, Gaswärme etwa 10 ct je kWh Wärme)
FallStromWärme aus der WärmepumpeErgebnis
Akku zu 10 ct10 ct3,3 ctWärmepumpe
Akku aus dem Netz nachgeladen zu 42 ct42 ct14 ctGas
Kipppunkt bei COP 330 ct10 ctGleichstand: Wärmepumpe läuft
Kipppunkt bei COP 5,9 (mildes Wetter)59 ct10 ctGleichstand: Wärmepumpe läuft

Gas gewinnt nur, wenn der Strom wirklich mehr kostet als der Kipppunkt.

Der Code, erklärt

Das Paket hat zwei Dateien in src/akkukonto/. Die Ausschnitte unten stammen direkt aus dem Download und stimmen Zeile für Zeile mit ihm überein. Code und Kommentare sind englisch. Nur Standardbibliothek, keine weiteren Pakete.

Das Konto: BatteryAccountsrc/akkukonto/konto.py · Zeilen 38–93
@dataclass
class BatteryAccount:
    """Bookkeeping for the energy in a battery, in kWh and in euros."""

    energy_kwh: float = 0.0
    """Stock: energy booked into the battery right now (goes up and down)."""

    cost_eur: float = 0.0
    """Stock: what that energy cost (goes up and down with it)."""

    solar_in_kwh: float = 0.0
    """Counter: everything ever charged from the sun (never goes down)."""

    grid_in_kwh: float = 0.0
    """Counter: everything ever charged from the grid (never goes down)."""

    def charge(self, kwh: float, price_eur_kwh: float) -> None:
        """Book kwh at price_eur_kwh. Every kilowatt hour brings its price."""
        if kwh <= 0:
            return
        self.energy_kwh += kwh
        self.cost_eur += kwh * price_eur_kwh

    def charge_solar(self, kwh: float, price_eur_kwh: float = 0.0) -> None:
        """Charge from the sun. The panels are paid for: price 0 by default."""
        if kwh <= 0:
            return
        self.charge(kwh, price_eur_kwh)
        self.solar_in_kwh += kwh

    def charge_grid(self, kwh: float, price_eur_kwh: float) -> None:
        """Charge from the grid at the price that applied when it came in."""
        if kwh <= 0:
            return
        self.charge(kwh, price_eur_kwh)
        self.grid_in_kwh += kwh

    def discharge(self, kwh: float) -> float:
        """Take kwh out at the average price; returns the kWh actually booked.

        The price stays exactly where it was: only the amount changes.
        An empty account books nothing (there is no price to take out at).
        """
        price = self.price()
        if kwh <= 0 or price is None:
            return 0.0
        taken = min(kwh, self.energy_kwh)
        self.energy_kwh -= taken
        self.cost_eur = self.energy_kwh * price
        return taken

    def price(self) -> float | None:
        """Price per kWh in EUR, or None if the account is empty."""
        if self.energy_kwh <= EMPTY_KWH:
            return None
        return self.cost_eur / self.energy_kwh
  • Zwei Bestände (energy_kwh, cost_eur) steigen und fallen mit dem Akku. Zwei Zähler (solar_in_kwh, grid_in_kwh) wachsen nur und liefern den PV-Anteil über die Zeit.
  • charge() addiert die Kilowattstunden und ihre Kosten. Mehr Mischen gibt es nicht: Der gewichtete Mittelwert ergibt sich aus der Division in price().
  • discharge() liest zuerst den Preis, senkt die Energie und setzt die Kosten auf Energie × genau diesen Preis. Der Preis kann sich dabei gar nicht bewegen.
  • price() liefert bis 0,01 kWh None. Ein leerer Akku hat keinen Preis, und None zwingt jeden Aufrufer, diesen Fall zu behandeln, statt still mit null weiterzurechnen.
Buchen aus Leistungswerten: MeterBookkeeper.book_interval()src/akkukonto/konto.py · Zeilen 167–208
def book_interval(self, *, hours: float, battery_w: float | None,
                  solar_w: float | None, grid_import_w: float | None,
                  grid_price_eur_kwh: float | None) -> Booking:
    """Book one interval of length hours. Returns what was booked."""
    if hours <= 0:
        return Booking("idle", 0.0, reason="no time has passed")
    if battery_w is None:
        return Booking("skipped", 0.0, reason="battery power unknown")

    kwh = self._smooth(battery_w) * hours / 1000.0

    if kwh < 0:
        taken = self.account.discharge(-kwh)
        return Booking("discharge", -taken,
                       reason="price unchanged" if taken else "account empty")
    if kwh == 0:
        return Booking("idle", 0.0, reason="battery at rest")

    # Charging: where does this energy come from?
    if solar_w is None or grid_import_w is None:
        return Booking("skipped", 0.0,
                       reason="source unknown, nothing booked")
    solar = max(solar_w, 0.0)
    grid = max(grid_import_w, 0.0)
    total = solar + grid
    # Split in proportion to solar power and grid import right now.
    # Nothing measured on either side: counted as grid (the expensive,
    # careful choice).
    solar_share = solar / total if total > 0 else 0.0
    solar_kwh = kwh * solar_share
    grid_kwh = kwh - solar_kwh

    price = None
    if grid_kwh > 0:
        if grid_price_eur_kwh is None:
            return Booking("skipped", 0.0, reason="grid price unknown")
        price = min(grid_price_eur_kwh, self.settings.max_grid_price_eur_kwh)
        self.account.charge_grid(grid_kwh, price)
    if solar_kwh > 0:
        self.account.charge_solar(solar_kwh, self.settings.solar_price_eur_kwh)
    return Booking("charge", kwh, solar_kwh, grid_kwh, price,
                   reason="booked by source")
  • Ein laufendes System sieht nie kWh aus der Sonne, nur Leistungswerte. Diese Methode macht aus den Messwerten eines Intervalls eine Buchung.
  • Zuerst wird die Akkuleistung geglättet (ema_alpha 0,3, also 30 % neuer Messwert). Ohne Glättung bucht das Flackern der Sensoren nachts langsam Energie, die nie hineinkam.
  • Entladen braucht keine Quelle, denn es ändert den Preis nie.
  • Laden braucht den PV- und den Netz-Messwert. Fehlt einer, wird nichts gebucht: Eine Lücke in der Buchhaltung schadet weniger als eine falsche Buchung, die tagelang nachwirkt.
  • Eine Ladung wird im Verhältnis von PV-Leistung und Netzbezug in diesem Moment aufgeteilt. Misst keine Seite etwas, zählt die Ladung als Netz, die vorsichtige Wahl. Netzpreise über 1 € je kWh gelten als Datenfehler und werden gekappt.
Woher käme die nächste kWh? source_price()src/akkukonto/quellenpreis.py · Zeilen 79–103
def source_price(*, surplus_w: float | None, soc_pct: float | None,
                 battery_may_discharge: bool, account_price_eur_kwh: float | None,
                 grid_price_eur_kwh: float,
                 settings: SourceSettings | None = None) -> SourcePrice:
    """Where would the next kWh come from, and what does it cost?"""
    s = settings if settings is not None else SourceSettings()

    if surplus_w is not None and surplus_w >= s.min_surplus_w:
        return SourcePrice(0.0, SOLAR, "solar surplus, would otherwise be exported")

    soc_ok = soc_pct is not None and soc_pct > s.battery_floor_pct
    if soc_ok and battery_may_discharge and account_price_eur_kwh is not None:
        return SourcePrice(account_price_eur_kwh, BATTERY,
                           "battery above its floor delivers the kWh")

    if soc_pct is None:
        why = "battery level unknown"
    elif not battery_may_discharge:
        why = "battery may not discharge"
    elif not soc_ok:
        why = "battery at or below its floor"
    else:
        # An unknown account price is not a price of zero: count as grid.
        why = "account price unknown"
    return SourcePrice(grid_price_eur_kwh, GRID, f"{why}: the kWh comes from the grid")
  • Drei Antworten, in dieser Reihenfolge geprüft: PV-Überschuss ergibt Preis 0; der Akku liefert den Kontopreis, wenn er über seinem Boden liegt, entladen darf und seinen Preis kennt; sonst gilt der Netzpreis dieses Moments.
  • Der Boden (battery_floor_pct) ändert nur, welcher Preis in die Entscheidung geht, nie den Akku selbst.
  • Ein unbekannter Kontopreis zählt als Netz. Unbekannt ist nicht null.
Wärmepumpe oder Gas: decide_heat_source()src/akkukonto/quellenpreis.py · Zeilen 146–163
def decide_heat_source(*, source: SourcePrice, cop: float | None,
                       gas_heat_eur_kwh: float | None,
                       fallback_blocked: bool = False) -> HeatDecision:
    """Block the heat pump only if its heat costs more than gas heat."""
    if source.source == SOLAR:
        return HeatDecision(False, source, 0.0, gas_heat_eur_kwh, True,
                            "solar surplus never blocks the heat pump")

    hp_heat = source.eur_kwh / cop if cop is not None and cop > 0 else None
    if hp_heat is None or gas_heat_eur_kwh is None:
        missing = "COP" if hp_heat is None else "gas heat price"
        return HeatDecision(fallback_blocked, source, hp_heat, gas_heat_eur_kwh, False,
                            f"{missing} missing: fallback decides")

    blocked = (hp_heat - gas_heat_eur_kwh) > TIE_TOLERANCE_EUR_KWH
    verdict = "gas is cheaper" if blocked else "heat pump is cheaper or equal"
    return HeatDecision(blocked, source, hp_heat, gas_heat_eur_kwh, True,
                        f"{verdict} ({source.source})")
  • PV-Überschuss sperrt die Wärmepumpe nie.
  • Ohne COP oder Gaspreis gibt es keinen Vergleich. Es wird nichts erfunden; deine bisherige Regel entscheidet (fallback_blocked).
  • Die Wärmepumpe wird nur gesperrt, wenn ihre Wärme mehr als 0,1 ct teurer ist als Gaswärme. Bei Gleichstand läuft sie.

Ausführen und Tests prüfen

Du brauchst Python 3.10 oder neuer, sonst nichts. Zip entpacken und im Ordner akkukonto ein Terminal öffnen:

Beispiele und Tests
python examples/episode1_walkthrough.py     # episode 1, step by step
python examples/meter_day.py            # a made-up day, booked from power readings
python -m pip install pytest            # only needed for the tests
python -m pytest                        # ends with: 49 passed

Der Rundgang gibt Folge 1 mit den Zahlen aus dem Video aus:

Ausgabe von episode1_walkthrough.py
Start: empty battery                     0.0 kWh   0.00 EUR  -> no price per kWh
Sun puts in 10 kWh (0 ct)               10.0 kWh   0.00 EUR  -> 0.00 ct per kWh
Grid adds 5 kWh at 30 ct                15.0 kWh   1.50 EUR  -> 10.00 ct per kWh
House takes out 10 kWh                   5.0 kWh   0.50 EUR  -> 10.00 ct per kWh
Grid adds 5 kWh at 30 ct again          10.0 kWh   2.00 EUR  -> 20.00 ct per kWh

Winter, COP 3, gas heat 10.00 ct per kWh of heat
  Battery at 10 ct                     heat 3.33 ct vs gas 10.00 ct -> heat pump
  Battery refilled from grid at 42 ct  heat 14.00 ct vs gas 10.00 ct -> gas

Tipping point winter (COP 3.0): electricity above 30.00 ct makes gas cheaper
Tipping point mild weather (COP 5.9): electricity above 59.00 ct makes gas cheaper

Die 49 Tests decken jedes Beispiel aus beiden Videos ab und dazu die Randfälle: leeres Konto, unbekannte Messwerte, gekappte Preise, Gleichstand, unplausible COP-Werte. Fünf davon kannst du von Hand nachrechnen:

Fünf Fälle zum Nachrechnen
Nr.FallErwartet
110 kWh Sonne + 5 kWh Netz zu 30 ct15 kWh für 1,50 € → 10 ct
2danach entnimmt das Haus 10 kWh5 kWh für 0,50 € → weiterhin 10 ct
3danach noch 5 kWh aus dem Netz zu 30 ct10 kWh für 2,00 € → 20 ct
4COP 3, Akku zu 10 ct, Gaswärme 10 ctWärme 3,3 ct → Wärmepumpe
5Akku zu 42 ct nachgeladen, COP 3Wärme 14 ct, mehr als 10 ct → Gas

Alle fünf sind Tests im Paket, benannt nach der Szene, die sie prüfen.

Fall 1 als Testtests/test_konto.py · Zeilen 10–16
def test_episode1_sun_and_grid_give_ten_cents():
    acc = BatteryAccount()
    acc.charge_solar(10)            # sun: 10 kWh for nothing
    acc.charge_grid(5, 0.30)        # grid: 5 kWh at 30 ct = 1.50 EUR
    assert acc.energy_kwh == pytest.approx(15)
    assert acc.cost_eur == pytest.approx(1.50)
    assert acc.price() == pytest.approx(0.10)
  • pytest.approx vergleicht Kommazahlen mit einer winzigen Toleranz, damit Rundung in der letzten Stelle keinen Test scheitern lässt.

Variablen

Die wichtigsten Namen mit Einheit und typischem Bereich. Alle Bereiche sind Beispielbereiche, keine Werte einer echten Anlage. Die vollständige Liste mit allen Einstellungen steht in VARIABLEN.md im Paket.

Akku-Konto und Buchen aus Messwerten
NameEinheitBedeutungTypischer BereichHerkunft
energy_kwhkWhEnergie, die gerade im Konto steht0 … nutzbare Größe deines AkkusLadungen minus Entnahmen
cost_eur€Was genau diese Energie gekostet hat0 … wenige € je 10 kWhkWh × Preis jeder Ladung
price()€/kWhKosten ÷ Energie; None, wenn das Konto leer istfast 0 (Sommer) … etwa 0,40 (Winter, aus dem Netz)berechnet
solar_share()0 … 1Anteil der Sonne an allem, was geladen wurde0,5 … 1,0 mit PV-Anlagedie beiden Zähler
battery_wWAkkuleistung: + Laden, − Entladeneinige Hundert … mehrere TausendMesswert von Akku oder Wechselrichter
solar_wWPV-Leistung in diesem Moment0 … Spitzenleistung deiner ModuleMesswert des PV-Wechselrichters
grid_import_wWNetzleistung: + Bezug, − Einspeisunghängt von Haus und Tarif abNetzzähler
grid_price_eur_kwh€/kWhIn diesem Intervall bezahlter Preis, inkl. Umlagen und Steuernetwa 0,15 … 0,45 bei dynamischem Tarifdein Tarif, je Viertelstunde
ema_alpha–Glättung: Anteil des neuesten Akku-Messwerts0,3 bei Messwerten einmal pro MinuteEinstellung
max_grid_price_eur_kwh€/kWhPlausibilitätsgrenze; höhere Netzpreise werden gekappt1,0Einstellung
Preis der Quelle und Wärme-Entscheidung
NameEinheitBedeutungTypischer BereichHerkunft
surplus_wWPV-Leistung, die über den Bedarf des Hauses hinaus übrig ist0 … Spitzenleistung deiner ModuleEnergiemanager oder Netzzähler
soc_pct%Ladestand des Akkus0 … 100Batteriemanagement
battery_floor_pct%Auf oder unter diesem Stand zählt die nächste kWh als NetzstromBeispiel 55Einstellung
cop–Wärme je Einheit Strom (Leistungszahl)etwa 2,5 bei −6 °C … über 6 bei mildem Wetterchoose_cop()
gas_price_eur_kwh€/kWhGaspreis je kWh Gasetwa 0,08 … 0,12Gastarif
efficiency–Wirkungsgrad des Kessels, gehalten zwischen 0,5 und 0,990,90 … 0,98Datenblatt oder eigene Messung
gas_heat_eur_kwh€/kWh WärmeGaspreis ÷ Wirkungsgrad (+ optionaler Aufschlag)etwa 0,10gas_heat_price()
heat_pump_heat_eur_kwh€/kWh WärmePreis der Quelle ÷ COP0 … 0,15berechnet

An dein Haus anpassen

  • Du bekommst Geld für eingespeisten Sonnenstrom? Dann kostet eine PV-kWh im Akku, was du für sie hättest bekommen können: MeterSettings(solar_price_eur_kwh=…) auf deine Einspeisevergütung setzen.
  • Viertelstundenpreise: jedem book_interval()-Aufruf den Preis der aktuellen Viertelstunde mitgeben. Den Rest erledigt das Konto.
  • Verluste: nicht abgebildet. Ein einfacher Weg: die am Akku gemessene Energie buchen und den Netzpreis durch den Ladewirkungsgrad teilen.
  • Konto über einen Neustart retten: Es lebt nur im Arbeitsspeicher. Die vier Zahlen (energy_kwh, cost_eur, solar_in_kwh, grid_in_kwh) speichern und beim Anlegen des Kontos wieder übergeben.
  • Das Alter jedes Messwerts prüfen, bevor er übergeben wird. Ein Messwert, der auf seinem letzten Stand eingefroren ist, sieht aus wie ein echter; ein eingefrorener PV-Wert in der Nacht sähe aus wie kostenloses Laden.
  • Andere Wärmequelle oder Klimaanlage: Derselbe Vergleich funktioniert, Preis der Quelle ÷ COP gegen den Preis der anderen Quelle.

Arbeitest du mit einem KI-Assistenten? Gib ihm das Paket, VARIABLEN.md und die Namen deiner eigenen Messwerte und lass ihn den Adapter zwischen deinen Messwerten und book_interval() schreiben. Die Tests zeigen dir danach, ob die Logik noch stimmt.

Download

Code-Paket: Akku-Konto

Quellcode, zwei Beispiele, 49 Tests, die Variablenliste und Hinweise zur Herkunft. Nur Python-Standardbibliothek.

Datei
akkukonto-1.0.0.zip
Version
1.0.0 · 6. Oktober 2026
Größe
22 KB · 13 Dateien
Lizenz
MIT · siehe Hinweis im Impressum
Preis
Kostenlos
SHA-256
dba504842c478434a95c46e7353d2054283b2294b419a5a1bf87686207b323a0

Zip herunterladen (22 KB)Prüfsummendatei

Zum Prüfen des Downloads die SHA-256-Prüfsumme vergleichen: certutil -hashfile akkukonto-1.0.0.zip SHA256 unter Windows, shasum -a 256 akkukonto-1.0.0.zip unter macOS, sha256sum akkukonto-1.0.0.zip unter Linux.

Stand und Herkunft

Quellen

Links geprüft am 6. Oktober 2026.

Deine Erfahrung zählt

Welches System läuft bei dir, und wo hakt es? Ich freue mich über deine Erfahrungen, Bedenken und Ideen in den Kommentaren unter den Videos. Gute Ideen greife ich auf und setze sie öffentlich um: als eigenes Video oder als Ergänzung zu einem bestehenden.

Zum Kanal