Smart Green Home: PV-Überschuss mit dem PV-Heizstab nutzen
Wir möchten so viel der durch unsere PV-Anlage erzeugte elektrische Energie selbst nutzen und so wenig wie möglich in das öffentliche Stromnetz einspeisen. Dazu haben wir bei einer Anlagengröße von 9,84 kWp, bei der ⅔ der Panels nach Norden ausgerichtet sind, einen Akku mit einer Gesamtkapazität von 15 kWh installieren lassen. Was erst einmal sehr groß klingt, ist im Sommer schnell voll.
Heute beschreibe ich, mit welchen Mechanismen wir sicherstellen, dass wir die Überschüsse nutzen, wie das unsere Wärmepumpe entlastet und wie wir dabei so wenig wie möglich ins öffentliche Stromnetz einspeisen.
PV-Überschuss als Wärme nutzen
Gerade in den Sommermonaten, wenn die Wärmepumpe selten läuft, ist der Akku teilweise schon weit vor der Mittagszeit voll. Der weiterhin erzeugte Strom ginge ins Stromnetz. Gerne würden wir aber auch diese Energie speichern, da die Einspeisevergütung für unsere PV-Anlage sehr gering ist.

Da unsere Anlage noch vor Ende Juli 2025 in Betrieb genommen wurde, erhalten wir als Teileinspeiser pro eingespeister Kilowattstunde 0,0843 €1 Vergütung über den Netzbetreiber. Für später in Betrieb genommene Anlagen liegt die Einspeisevergütung sogar noch niedriger und sinkt weiter.
Es gibt somit ein großes Ungleichgewicht zwischen Vergütung für erzeugten Strom bei Einspeisung und Kosten für aus dem Netz bezogenen Strom – diese liegen zum Zeitpunkt dieses Artikels bei etwa 30 Cent pro Kilowattstunde.
Wir wollen also PV-Überschuss, sobald er entsteht, mit dem wattgenau steuerbaren PV-Heizstab2 verwenden und damit den Pufferspeicher zur Warmwassererzeugung laden. Somit verbrauchen wir die Energie direkt und können im Sommer die Starts der Wärmepumpe weiter reduzieren.
Grundsätzlich sähe die Ansteuerung also so aus:

Wenn am Netzübergabepunkt die Einspeisung oberhalb von 250 W liegt, soll der Heizstab mit einer Leistung von der aktuellen Einspeisung minus 50 Watt angesteuert werden. Beispiel: Bei 450 W Einspeisung in das Netz wird der Heizstab mit einer Leistung von 400 W konfiguriert.
Fällt die Einspeisung auf unter 25 W, wird der Heizstab abgeschaltet. So ergibt sich eine Hysterese von 25 W, welche für meine Steuerungsansprüche ausreichend ist und der Heizstab nicht zu sehr taktet.
Beispiele für die Berechnungen in den Schritten ❶ und ❷ im Diagram:
- Erster Leistungswert, wenn der Heizstab (noch) nicht eingeschaltet ist: Vom aktuellen Wert der Einspeisung werden 50 W abgezogen. Wenn der Wert dann noch größer als Null ist, wird der Wert für den Heizstab bereigestellt.
- Neuer Leistungswert, wenn der Heizstab bereits eingeschaltet ist: Der aktuell eingestellte Wert für den Heizstab wird mit dem aktuellen Wert der Einspeisung addiert. Von diesem Wert werden 50 W abgezogen. Ist dieser Wert größer als Null, wird dieser Wert für den Heizstab bereitgestellt. Ist er Null oder kleiner, wird ein Wert von 0 W für den Heizstab bereitgestellt und der Heizstab damit faktisch abgeschaltet.
Der Wert der Einspeisung am Netzübergabepunkt soll 1× pro Sekunde durch Auslesen des vom Netzbetreiber gesetzten Smart Meters bezogen werden. Dazu soll ein Tasmota-basierter Infrarot-Lesekopf verwendet werden. Durch die Verwendung dieses Zählers als Datenquelle sind wir unabhängig von den Protokollen anderer Geräte, wie beispielsweise dem Wechselrichter.
Die Ansteuerung des Heizstabs erfolgt dann direkt, indem der berechnete Wert per Modbus-TCP geschrieben wird.
Lohnt sich das überhaupt? Ein Rechenexempel
Bevor es nun an die Implementierungsdetails geht, schauen wir erst einmal darauf, ob sich dieses Vorgehen überhaupt »lohnt«. Dazu vergleichen wir zunächst, wieviel Wärme beim Verbrauch einer Kilowattstunde Strom bei Verwendung von Wärmepumpe oder PV-Heizstab entsteht.
Insbesondere die Arbeitszahlen im folgenden Rechenbeispiel sind Annahmen, die ich im kommenden Sommer validieren will. Eventuell muss ich im Nachgang dazu das Beispiel revidieren.
| Wärmeerzeuger | Angenommene Arbeitszahl3 | Wärmemenge beim Verbrauch 1 kWh Strom |
|---|---|---|
| Wärmepumpe | 2,75 | \(1\,\text{kWh} \cdot 2{,}75 = 2{,}75\,\text{kWh}\) |
| PV-Heizstab | 0,98 | \(1\,\text{kWh} \cdot 0{,}98 = 0{,}98\,\text{kWh}\) |
Unter diesen Annahmen kann sich die Verwendung des Heizstabs kaum rechnen, oder? Nun, hier kommt der große Unterschied zwischen Stromkosten und Einspeisevergütung zum Tragen.
Wenn ich eine Kilowattstunde nicht einspeise und stattdessen mit dem Heizstab verbrauche, verzichte ich auf 0,0843 € (s. o.) Einspeisevergütung. Wenn ich eine Kilowattstunde einkaufe, zahle ich dafür ca. 0,30 €. Davon ausgehend, dass die Wärmepumpe im Sommerbetrieb nur zur Warmwasserversorgung läuft, und der Einfachheit halber jeglicher Strom dafür eingekauft werden muss, lässt sich also folgendes am Beispiel für die Erzeugung von 1 kWh Wärme aufstellen:
| Wärmeerzeuger | Erforderliche Strommenge für 1 kWh Wärme | Kosten durch Einkauf oder Verzicht |
|---|---|---|
| Wärmepumpe | \(\frac{1\,\text{kWh}}{2{,}75} \approx 0{,}36\,\text{kWh}\) | \( 0{,}36\,\text{kWh} \cdot \frac{0{,}30\,\text{€}}{1\,\text{kWh}} \approx 0{,}11\,\text{€}\) |
| PV-Heizstab | \(\frac{1\,\text{kWh}}{0{,}98} \approx 1{,}02\,\text{kWh}\) | \( 1{,}02\,\text{kWh} \cdot \frac{0{,}30\,\text{€}}{1\,\text{kWh}} \approx 0{,}09\,\text{€}\) |
Im direkten Vergleich ist es also für mich günstiger, wenn ich jeglichen Strom für die Wärmepumpe zukaufen müsste. Auch das entspricht sicher nicht immer der Realität, denn auch die Wärmepumpe wird gerade im Sommer mit günstigem PV-Strom versorgt.
Da der Heizstab so konfiguriert wird, dass er den Warmwasserpufferspeicher bis über 70 °C aufheizen darf, kommt die Wärmepumpe in den Sommermonaten kaum noch zum Zuge. Dadurch werden also nicht nur Kosten gespart, sondern auch die Lebensdauer der Wärmepumpe durch eine Vermeidung von Kompressorstarts gesteigert.
Datenquelle »Smart Meter«
Damit steht das Konzept. Für die praktische Umsetzung brauchen wir die Messwerte am Netzübergabepunkt. Wie schon angesprochen gibt es hier nun einen modernen Zweirichtungs-Smart Meter, in unserem Falle von Landis+Gyr vom Typ E320. Der Zähler verfügt auch über ein Smart Meter Gateway, jedoch ist dieses nicht geeignet, um echtzeitnahe Werte zu gewinnen. Daher bekommt dieser Zähler einen Infrarot-Lesekopf, der per WLAN auslesbar ist.

Für den bitShake-Lesekopf gibt es vom Hersteller vorbereitete Skripte für diverse Zählermodelle4. Unser Zähler war dabei und so war die Konfiguration kein Problem. Jedoch bekam ich noch nicht die erwarteten Daten: Die aktuelle Leistung fehlte komplett, und von den Zählwerken 1.8.0 (Bezug) und 2.8.0 (Einspeisung) bekam ich nur die Stände in vollen Kilowattstunden ohne Dezimalstellen.
Die meisten smarten Zähler, die von einem Netzbetreiber oder Messstellenbetreiber gesetzt werden, benötigen erfordern PIN (in der Regel eine vierstellige Zahlenkombination), um mehr oder detailliertere Daten vom Display abzulesen oder über die Infrarotschnittstelle auszulesen. Sie dient dazu, die als schützenswert eingestuften Detaildaten vor der direkten Einsicht durch Dritte zu schützen.
Auch wenn es keine explizit formulierte Pflicht dazu gibt, geben die meisten Betreiber die PIN auf Anfrage über die Hotline oder ein Kontaktformular an den jeweiligen Energiekunden heraus.
In unserem Fall bietet der Netzbetreiber ein spezielles Kontaktformular zur Anforderung der Zähler-PIN an. Dort hinterlässt man neben Namen und Anschrift noch die Zählernummer. Innerhalb weniger Tage bis Wochen erhält man per Briefpost die PIN.
Eigentlich ist damit alles ganz einfach:
- In den »Info-Modus« des Zählers wechseln
- PIN eingeben
- PIN-Abfrage deaktivieren
- Erweiterten »Info-Modus« aktivieren
Soweit zur Theorie. Doch schon Schritt 2 wird zur Gedulds-Herausforderung, denn der Landis+Gyr E320 hat keine Bedientaste. Für die PIN-Eingabe sendet man bei diesem Zählermodell Licht-Impulse aus einer hellen Taschenlampe oder dem Handy-Blitz auf die Infrarot-Schnittstelle. Dabei muss man bestimmte Blink-Frequenzen einhalten. Zu lange Abstände: Der Zähler wechselt automatisch auf die nächste Ziffer der PIN. Bei zu kurzen Intervallen kann es sein, dass man noch die Ziffer beeinflusst, die man nicht mehr verändern wollte. Nach einigen frustrierenden Fehlversuchen mit nicht mehr treffenden Lichtkegeln, unerwartetem Wechsel der PIN-Stelle und verpassten Optionswechseln habe ich mich an eine App erinnert, die mir ein Kollege empfohlen hat: Zähler Blitzdingsbums5. Damit war die PIN-Eingabe endlich fehlerfrei möglich.
Wenn man vergisst, die Option »PIN-Abfrage deaktivieren« zu setzen, ist das Auslesen von Detaildaten zwar möglich, aber nur für kurze Zeit, bis sich der Zähler selbst wieder »sperrt« und erneut auf die Eingabe der PIN besteht.
Wenn alle Schritte erfolgreich durchgeführt wurden, wird der bitShake-Infrarot-Lesekopf nun Daten live auslesen können:

Diese Daten können nun auch über die Webservice-Schnittstelle des Lesekopfs ausgelesen werden:
1curl --user admin "http://<IP-des-bitShake-Lesekopfs>/cm?cmnd=status%208"
Nachdem curl noch nach dem Passwort des Users amin gefragt hat, bekommen wir eine schöne JSON-formatierte
Antwort:
1{
2 "StatusSNS": {
3 "Time": "2025-12-14T11:57:53",
4 "E320": {
5 "E_in": 1234.567,
6 "E_out": 123.456,
7 "Power": 123,
8 "Meter_Number": "0deadc0ffeedeadc0de"
9 }
10 }
11}
Damit kann man etwas anfangen. Ein kurzer Lasttest zeigt, dass der bitShake-Kopf mit sekündlicher Abfrage problemlos umgehen kann.
An den Code
Hier wird die Anbindung einer ersten Datenquelle und die Verwendung der gewonnen Daten zum Zwecke des Monitorings und zur Steuerung gezeigt. Damit haben wir etwas Greifbares, mit dem wir in einem späteren Artikel dieser Serie die Integration in eine »Gesamtlösung« diskutieren können, die dann eine Vielzahl von Datenquellen anbindet.
Auslesen des Infrarot-Lesekopfs mit Go
Das Auslesen ist also »nur« ein simpler Abruf eines Webservices. Es braucht keine besonderen Clients, es reichen vollkommen vorhandene Go-Bordmittel. Aus der oben gezeigten Antwort lässt sich dieses Response-Daten-Struct ableiten:
1type TastkopfData struct {
2 StatusSNS struct {
3 E320 struct {
4 EIn float64 `json:"E_in"`
5 EOut float64 `json:"E_out"`
6 Power int `json:"Power"`
7 MeterNumber string `json:"Meter_Number"`
8 } `json:"E320"`
9 } `json:"StatusSNS"`
10}
In der Antwort vom Tastkopf ist noch ein Zeitstempel enthalten. Dieser entspricht jedoch nicht der Standard-Form, die vom Standard-Go-JSON-Unmashaller verstanden wird. Wir müssten hier also mit einem benutzerdefinierten Unmarshaller arbeiten. Da wir für unsere Zwecke den Zeitstempel aus der Antwort jedoch nicht brauchen, sparen wir uns den zusätzlichen Code und ignorieren den Wert.
Das eigentliche Laden übernimmt der Go-eigene HTTP-Client, den wir mit einem Timeout von 10 Sekunden versehen, damit Requests automatisch abgebrochen werden können, wenn sie nicht abgeschlossen werden können.
1type TastkopfClient struct {
2 username string
3 password string
4 client *http.Client
5 fetchURL string
6}
7
8// NewClient creates a new TastkopfClient, configured to fetch data from the given baseURL with the given credentials.
9func NewClient(baseURL string, username string, password string) *TastkopfClient {
10 return &TastkopfClient{
11 username: username,
12 password: password,
13 client: &http.Client{
14 Timeout: 10 * time.Second,
15 },
16 fetchURL: fmt.Sprintf("%s/cm?cmnd=status%%208", baseURL),
17 }
18}
Initialisiert wird der Client mit der Basis-URL (im Beispiel: http://<IP-des-bitShake-Lesekopfs>), dem Benutzernamen
(im Beispiel: admin) und dem Passwort. Aus der Basis-URL wird die eigentliche Abruf-URL erstellt.
Nun brauchen wir nur noch eine Methode, mit der im jeweiligen Kontext die Daten des konfigurierten Tastkopfes gelesen werden können:
1// GetState fetches the current state of the Tastkopf.
2func (c *TastkopfClient) GetState(ctx context.Context) (*TastkopfData, error) {
3 httpRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, c.fetchURL, nil)
4 if err != nil {
5 return nil, fmt.Errorf("failed to create http request: %w", err)
6 }
7 httpRequest.SetBasicAuth(c.username, c.password)
8 httpResponse, err := c.client.Do(httpRequest)
9 if err != nil {
10 return nil, fmt.Errorf("failed to send http request: %w", err)
11 }
12 defer func() {
13 _ = httpResponse.Body.Close()
14 }()
15 if httpResponse.StatusCode != http.StatusOK {
16 return nil, fmt.Errorf("http request failed with status %d", httpResponse.StatusCode)
17 }
18 var response TastkopfData
19 if err := json.NewDecoder(httpResponse.Body).Decode(&response); err != nil {
20 return nil, fmt.Errorf("failed to decode http response: %w", err)
21 }
22 return &response, nil
23}
Zuletzt sehen wir eine Close-Methode vor, um Verbindungen im Leerlauf im Verbindungs-Pool des HTTP-Clients zu
schließen. Die Methode implementiert das Closer-Interface6 (daher error als Rückgabe-Typ):
1// Close closes the underlying HTTP client's idle connections.
2func (c *TastkopfClient) Close() error {
3 c.client.CloseIdleConnections()
4 return nil
5}
Mehr braucht es nicht, und so können wir nun auf diesem Weg Daten aus dem Tastkopf erhalten:
1func main() {
2 c := infrarotLesekopf.NewClient(
3 "http://<IP-des-bitShake-Lesekopfs>",
4 "admin",
5 "password",
6 )
7 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
8 defer cancel()
9 r, err := c.GetState(ctx)
10 if err != nil {
11 log.Fatal(err)
12 return
13 }
14 fmt.Printf("aktueller Zählerzustand: %+v\n", r)
15}
Bei der Ausführung erhalten wir folgende Ausgabe:
aktueller Zählerzustand: &{StatusSNS:{E320:{EIn:123.456 EOut:12.34 Power:-12 MeterNumber:0deadc0ffeedeadc0de}}}
Schreiben der Werte in die Influx-Datenbank
Da ich im Rahmen des Gesamtkonzepts das Schreiben in die Influx-DB noch einmal detaillierter beschreiben werde, hier nur ein kurzes Code-Beispiel zur Veranschaulichung, welches sich an den Zugangsdaten aus dem Docker-Compose-File der Devcontainer-Entwicklungsumgebung orientiert und keinerlei Fehlerbehandlung implementiert:
1func WriteToInflux(td *TastkopfData) {
2 // InfluxDB-Host aus dem Docker-Compose-File: influx, Token: DeveloperInfluxAdminToken==
3 influxClient := influxdb2.NewClient("influx", "DeveloperInfluxAdminToken==")
4 defer influxClient.Close()
5 // Organisation aus unserem Beispiel: developer; Bucket: energy
6 writeAPI := influxClient.WriteAPI("developer", "energy")
7 defer writeAPI.Flush()
8 // Die Werte des Smart Meters werden als Messung "SmartMeter_OeffentlichesNetz" geschrieben, die Seriennummer wird als Device-Tag genutzt
9 p := influxdb2.NewPointWithMeasurement("SmartMeter_OeffentlichesNetz").AddTag("device", td.StatusSNS.E320.MeterNumber).SetTime(time.Now())
10 p.AddField("e_in", td.StatusSNS.E320.EIn).AddField("e_out", td.StatusSNS.E320.EOut).AddField("power", td.StatusSNS.E320.Power)
11 writeAPI.WritePoint(p)
12}
Re-Publizieren der Messwerte als Modbus-TCP-Service
Damit der Heizstab nun mit diesen Daten etwas anfangen kann, müssen wir die Leistung (Feld »Power«) entsprechend des obigen Schemas umrechnen und wieder per Modbus-TCP bereitstellen. Die Umrechnung ist schnell umgesetzt:
1package hvp
2
3import "sync"
4
5type HeizstabValueProvider struct {
6 mutex sync.RWMutex
7 currentValue int
8}
9
10const (
11 minPowerExportToStart int = 50
12 minPowerExportToScale int = 25
13 maxPossiblePower int = 3600
14)
15
16func (p *HeizstabValueProvider) Set(smartMeterPower int) {
17 p.mutex.Lock()
18 defer p.mutex.Unlock()
19 baseValue := p.currentValue
20 if baseValue < 0 /* Heizstab ist aktiv */ {
21 availablePower := baseValue + smartMeterPower + minPowerExportToScale
22 if availablePower > 0 {
23 // Heizstab stoppen - es ist kein export mehr da nach Abzug der Skalierung
24 p.currentValue = 0
25 } else {
26 if availablePower < -maxPossiblePower {
27 p.currentValue = -maxPossiblePower
28 } else {
29 p.currentValue = availablePower
30 }
31 }
32 } else /* Heizstab ist inaktiv */ {
33 if smartMeterPower < -minPowerExportToStart {
34 targetValue := smartMeterPower + minPowerExportToStart
35 if targetValue < -maxPossiblePower {
36 targetValue = -maxPossiblePower
37 }
38 // Heizstab starten
39 p.currentValue = targetValue
40 }
41 }
42}
43
44func (p *HeizstabValueProvider) Get() int {
45 p.mutex.RLock()
46 defer p.mutex.RUnlock()
47 return p.currentValue
48}
49
50func NewProvider() *HeizstabValueProvider {
51 return &HeizstabValueProvider{currentValue: 0}
52}
Dazu definieren wir noch einen Test, der die Abfolge verschiedener Messwerte exemplarisch durchspielt:
1package hvp_test
2
3import (
4 "blog-test-parts/heizstab"
5 "testing"
6)
7
8func TestProvider(t *testing.T) {
9 testSteps := []struct {
10 smValue int
11 expected int
12 }{
13 {0, 0},
14 {10, 0},
15 {20, 0},
16 {25, 0},
17 {26, 0},
18 {0, 0},
19 {-10, 0},
20 {-20, 0},
21 {-25, 0},
22 {-26, 0},
23 {50, 0},
24 {51, 0},
25 {0, 0},
26 {-50, 0},
27 {-51, -1},
28 {-25, -1},
29 {-30, -6},
30 {-26, -7},
31 {-126, -108},
32 {18, -65},
33 {1250, 0},
34 {-4550, -3600},
35 {1000, -2575},
36 {-50, -2600},
37 {-25, -2600},
38 {-25, -2600},
39 {-25, -2600},
40 {-25, -2600},
41 {-25, -2600},
42 {-25, -2600},
43 {-1500, -3600},
44 {3000, -575},
45 }
46 p := heizstab.NewProvider()
47 for step, stepData := range testSteps {
48 p.Set(stepData.smValue)
49 if v := p.Get(); v != stepData.expected {
50 t.Errorf("[step #%03d] Expected %d, got %d", step, stepData.expected, v)
51 }
52 }
53}
Anbindung des my-PV Heizstabs
Der Heizstab wird nun so konfiguriert, dass er sich den aktuellen Leistungswert am Netzübergabepunkt aus den von uns bereitgestellten Daten holt und Überschüsse ohne weitere eigene Umrechnung als Heizleistung verwendet.

Interessant?
Ich freue mich, per E-Mail von Dir zu hören, wenn Du eigene Erfahrungen diskutieren möchtest oder gerade selbst ein ähnliches Projekt planst.
Dieser Artikel wurde von einem Menschen geschrieben, anschließend mit Hilfe von KI korrigiert und teilweise umformuliert.
KI hat keinerlei Inhalte selbst erzeugt oder Fakten beigetragen.
-
Quelle: Bundesnetzagentur; zum Zeitpunkt des Artikels waren bereits neue Vergütungssätze gültig. Die davor gültigen und für unsere Anlage relevanten Sätze sind hier archiviert worden: Vergütungssätze Februar bis Juli 2025 (Excel-Tabelle) ↩︎
-
Wir verwenden einen my-PV AC ELWA 2 , der uns vom Heizungsbauer direkt in den Warmwasserpeicher eingebaut wurde. ↩︎
-
Für die Warmwasserbereitung benötigt die Wärmepumpe eine höhere Zieltemperatur als im Heizungsbetrieb, so dass ich von einer Arbeitszahl von etwa 2,75 ausgehe. Der Heizstab hingegen wandelt nahezu direkt Strom in Wärme um – jedoch benötigt er selbst Strom für seine eigene Elektronik und arbeitet nicht völlig verlustfrei, so dass ich von einer Arbeitszahl von etwa 0,98 ausgehe. ↩︎
-
Die Skript-Liste eignet sich auch, um vor dem Kauf zu bestimmen, ob der Lesekopf mit dem eigenen Zählermodell umgehen kann: Skripte aus der bitShake-Dokumentation ↩︎
-
Die App gibt es zum Zeitpunkt dieses Artikels kostenlos für iOS . Eine Version für Android ist mir nicht bekannt. Mehr Infos zur App gibt es direkt beim Entwickler . ↩︎
Smart Green Home Go Entwicklung Monitoring Photovoltaik Wärmepumpe Smart Meter
Zuletzt geändert: 2026-03-07 19:56:34
