- Systemübersicht
- Hardware & Voraussetzungen
- Ladelogik & Strategie
- Modbus-Register Referenz
- evcc Koordination
- PV-Prognose
- Installation
- Konfiguration
- Web-Dashboard
- Betrieb & Monitoring
- Fehlerbehebung
- Deployment-Optionen
- VRM Forecast API
┌─────────────────────────────────────────────────────────┐
│ battery_manager.py │
│ │
│ ForecastManager VictronModbus EvccMonitor │
│ (Open-Meteo API) (Modbus TCP) (REST API) │
│ │ │ │ │
│ └──────────────────┼────────────────┘ │
│ │ │
│ ChargeController │
│ (Ladeentscheidung 60s-Zyklus) │
│ │ │
│ Flask Dashboard :5000 │
└──────────────────────────┼──────────────────────────────┘
│ Modbus TCP Port 502
Cerbo GX (Venus OS)
│
┌────────────┴────────────┐
Multiplus II MPPT / PV-WR
(ESS, DVCC) (AC-gekoppelt)
│
LFP Akku 14 kWh
| Quelle | Protokoll | Was |
|---|---|---|
| Cerbo GX | Modbus TCP (lesen) | SOC, Spannung, Strom, Leistung, PV, Last, Netz |
| Cerbo GX | Modbus TCP (schreiben) | MaxChargeCurrent (Reg 2705, DVCC) |
| Cerbo GX | Modbus TCP (lesen) | ESS MinSoc (Reg 2901, evcc-Erkennung) |
| Open-Meteo / Solcast | HTTPS REST | Stündliche PV-Prognose |
| evcc | HTTP REST | Lademodus, Wallbox-Leistung (optional) |
| Flask | HTTP :5000 | Web-Dashboard für Browser |
| Komponente | Details |
|---|---|
| Wechselrichter | Victron Multiplus II (ESS-Modus) |
| Steuereinheit | Victron Cerbo GX (Venus OS) |
| Batterie | LFP 14 kWh, 48V |
| PV-Anlage | 10 kWp, AC-gekoppelt über PV-Wechselrichter |
| Komponente | Version |
|---|---|
| Raspberry Pi OS | Bookworm (Python 3.13) |
| Python | 3.9+ |
| python3-venv | via apt |
| Modbus TCP am Cerbo | aktiviert (Port 502) |
| DVCC am Multiplus | aktiviert |
Cerbo GX:
Einstellungen → Dienste → Modbus TCP → Ein
Multiplus II / ESS:
Einstellungen → DVCC → Ein
DVCC → Maximaler Systemladestrom: 50A
- LFP-Schonung: SOC bevorzugt zwischen 20–80% halten
- Sommer-Optimierung: Morgens nicht unnötig laden, auf PV-Überschuss warten
- Zellbalancing: Spätestens alle 10 Tage Vollladung auf 98%
- Dynamisch: Täglich neues Ziel-SOC basierend auf prognostiziertem Nachtverbrauch
- Optimal-Fenster: Ladung konzentriert im Sonnenhöchststand-Fenster (Standard 11:00–15:00), Strom prognosebasiert — kein fester Reduzierstrom mehr
┌─ SOC <= emergency_charge_soc (Notfall)?
│ └─ JA → Sofort mit max_charge_current laden
│
├─ Vollladung faellig (>= full_charge_interval_days)?
│ └─ JA → target_soc = 98% (Zellbalancing), mit max_charge_current laden
│
├─ Nacht (dynamisches Fenster, ca. 20:00–06:00)?
│ └─ JA → Kein Laden (idle, min_charge_current als Reg-2705-Untergrenze)
│
├─ Morgenfenster (vor Optimal-Fenster-Start)?
│ ├─ PV im Optimal-Fenster ausreichend fuer Ziel-SOC UND SOC >= min_required?
│ │ └─ JA → Warten (idle)
│ └─ NEIN → Fruehzeitig laden (Ueberschuss-proportional, max max_charge_current)
│
├─ Im Optimal-Fenster (z.B. 11:00–15:00)?
│ └─ Prognosebasierter Strom (siehe unten): bleibt stundenlang konstant
│
├─ Ausserhalb Optimal-Fenster, PV-Ueberschuss vorhanden, SOC < Ziel?
│ └─ Laden mit max_charge_current (Akku nimmt was er kriegen kann)
│
├─ SOC >= Ziel-SOC?
│ └─ idle (Victron ESS/DVCC regelt physikalisch)
│
└─ Ziel-SOC fast erreicht (innerhalb Hysterese)?
└─ idle oder discharging (Entladesperre bei SOC <= floor_soc)
Das Ziel-SOC wird bei jedem Zyklus neu berechnet — nicht mehr fest konfiguriert:
target_soc = min_soc + (night_consumption_kWh / capacity_kWh) x 100%
Grenzen:
target_soc < min_soc → target_soc = min_soc
target_soc > max_soc → target_soc = max_soc
days_since_full_charge >= 10 → target_soc = 98% (Vollladung erzwingen)
Beispiel:
min_soc = 25%, Nachtverbrauch (VRM-Prognose) = 5.8 kWh, Kapazitaet = 14 kWhtarget_soc = 25 + (5.8 / 14) × 100 = 66%
Nachtverbrauch kommt bevorzugt aus der VRM-Verbrauchsprognose (vrm_consumption_fc);
avg_daily_consumption_kwh aus config.yaml dient nur als Fallback.
Das Optimal-Fenster (Standard 11:00–15:00 Uhr, konfigurierbar via solar_noon_offset_hours)
ist das Herzstuck der Ladestrategie. Seit v3.0.11 wird der Ladestrom nicht mehr aus dem
Momentan-Ueberschuss (Grid-Messung) abgeleitet, sondern einmal pro Stunde aus der
Prognose und dem verbleibenden Bedarf berechnet.
Stundenbeginn — neuer Plan:
missing_wh = (dyn_target - soc) / 100 * capacity_wh
needed_wh_per_h = missing_wh / hours_left # gleichmaessig verteilen
planned_wh = min(forecast_surplus_wh, # nie mehr als PV liefert
needed_wh_per_h + deficit_share_wh)
charge_a = planned_wh / battery_voltage # clamp: min_a..max_a
dyn_target: aktuelles Ziel-SOC (steigt bei wenig Reststunden, faellt bei Ziel fast erreicht)deficit_share_wh: kumulierter Rueckstand aus Vorjahr-Stunden, auf restliche Stunden verteiltcharge_ableibt innerhalb der Stunde konstant — kein Modbus-Write ausser beim Stundenstart
Defizit-Ausgleich zwischen Stunden:
deficit_wh = planned_wh - actual_wh # signed (Entladung zaehlt mit)
carried_wh += deficit_wh
→ naechste Stunde: deficit_share = carried_wh / hours_left
SOC-Guard:
Wenn soc > dyn_target, wird sofort auf min_charge_current gedrosselt und der Plan
zurueckgesetzt. Verhindert Ueberladen bei unerwartet hohem PV-Ertrag.
Vorteile gegenueber v3.0.10.x:
- Kein Rauschen durch Grid-Messung (±2000W Schwankung hatte kuenstliche Glaettung erfordert)
- Konfigurationskeys
optimal_window_write_deadband_a,optimal_window_current_step_a,required_a_smooth_window,optimal_window_min_current_awerden nicht mehr benoetigt (koennen inconfig.yamlstehen bleiben, werden stillschweigend ignoriert)
Log-Beispiel (Normalbetrieb):
Optimal-Fenster H11 neuer Plan: Prognose=5336Wh, Bedarf=924Wh/h (4620Wh/5h), Defizitanteil=+0Wh, Plan=924Wh -> 19.2A
[CHARGING] 19A | Optimal-Fenster H11: 19A (Plan 924Wh, Uebertrag +0Wh, SOC 46.0%->79%)
Optimal-Fenster H11 abgeschlossen: Plan=924Wh, Ist=900Wh, Defizit=+24Wh, Uebertrag=+24Wh
Optimal-Fenster H12 neuer Plan: Prognose=5571Wh, Bedarf=924Wh/h (3696Wh/4h), Defizitanteil=+6Wh, Plan=930Wh -> 19.4A
Das Nachtfenster wird astronomisch berechnet (Sonnenauf-/untergang fuer den konfigurierten Standort) statt als feste Uhrzeiten:
Nacht-Start: floor(sunset_hour + 0.5) # ca. 30 min nach Sonnenuntergang
Nacht-Ende: floor(sunrise_hour - 0.5) # ca. 30 min vor Sonnenaufgang
night_start_hour / night_end_hour in config.yaml dienen als Fallback wenn keine
astronomische Berechnung moeglich ist.
- Rampe: ±
current_ramp_stepA/Zyklus (Default 10A), gilt fuer alle Modi inkl. Optimal-Fenster - Hysterese: kein Modbus-Write wenn gerampetem Strom sich weniger als 1A vom zuletzt geschriebenen Wert unterscheidet
- Minimum:
min_charge_current(Default 3A) — Reg. 2705 wird nie unter diesen Wert gesetzt; Victron ESS fliesst diesen Strom auch im Idle-Zustand - Maximum:
max_charge_current(Default 50A) - Im Optimal-Fenster aendert sich der Sollwert nur stundlich (neuer Plan) oder bei SOC-Guard — zwischen diesen Ereignissen faellt die Hysterese keine weiteren Writes aus
- Datum der letzten Vollladung in
state.jsongespeichert (ueberlebt Neustarts) - Auto-Reset: SOC >= 98% fuer >= 1 Stunde →
days_since_full_charge = 0 - Balancing-Haltezeit: Nach Erreichen von 98% wird der volle Strom fuer
balancing_hold_hours(Default 5h) aufrechterhalten, damit alle Zellen ausbalancieren - Mitternachts-Reset: Balancing-Timer, Glaettungspuffer und Optimal-Fenster-Plan werden taeglich um Mitternacht zurueckgesetzt
Der battery_manager schreibt ausschliesslich Reg. 2705 (DVCC MaxChargeCurrent). Victron ESS/DVCC klemmt den tatsaechlichen Ladestrom physikalisch — der Setpoint ist eine Obergrenze, keine Zielgroesse. Entladung und Grid-Bezug werden von ESS autonom geregelt.
| Victron ESS State | Bedeutung | battery_manager-Verhalten |
|---|---|---|
| 10 (Self-consumption) | SOC >= MinSOC, normaler Betrieb | Steuert Reg. 2705 |
| 11 (SOC < MinSOC) | Entladung gesperrt, Netz speist Last | Reg. 2705 bleibt, SOC-Simulation friert ein |
| 12 (Minimal-Laden) | Minimale Netzladung um MinSOC zu halten | Simulation ignoriert diesen Strom |
Alle Register-Nummern in dieser Dokumentation entsprechen der Victron-Dokumentation (CCGX-Modbus-TCP-register-list) und werden direkt von pymodbus und mbpoll (mit -0) verwendet – kein Offset nötig.
mbpollmuss mit-0aufgerufen werden (0-basierte Adressierung), damit die Register-Nummern mit pymodbus und der Victron-Dokumentation übereinstimmen.
| Messwert | Register | Typ | Skalierung |
|---|---|---|---|
| Batteriespannung | 840 | uint16 | ÷ 10 → V |
| Batteriestrom | 841 | int16 | ÷ 10 → A |
| Batterieleistung | 842 | int16 | direkt W |
| Batterie SOC | 843 | uint16 | direkt % |
| PV-WR L1 | 811 | uint16 | direkt W |
| PV-WR L2 | 812 | uint16 | direkt W |
| PV-WR L3 | 813 | uint16 | direkt W |
| AC-Last L1 | 817 | uint16 | direkt W |
| AC-Last L2 | 818 | uint16 | direkt W |
| AC-Last L3 | 819 | uint16 | direkt W |
| Netz L1 | 820 | int16 | direkt W |
| Netz L2 | 821 | int16 | direkt W |
| Netz L3 | 822 | int16 | direkt W |
| ESS MinSoc | 2901 | uint16 | direkt % |
| DVCC MaxChargeCurrent | 2705 | uint16 | direkt A |
Netz: positiv = Bezug, negativ = Einspeisung
Strom: positiv = laden, negativ = entladen
sudo apt install mbpoll
# SOC (sollte z.B. 89 zeigen)
mbpoll -0 -a 100 -r 843 -c 1 192.168.178.61
# Spannung (÷10 → V)
mbpoll -0 -a 100 -r 840 -c 1 192.168.178.61
# PV-Leistung L1/L2/L3
mbpoll -0 -a 100 -r 811 -c 3 192.168.178.61
# Netz L1/L2/L3 (signed)
mbpoll -0 -a 100 -r 820 -c 3 192.168.178.61
# ESS MinSoc (evcc-Erkennung)
mbpoll -0 -a 100 -r 2901 -c 1 192.168.178.61
# MaxChargeCurrent schreiben (Test 10A)
mbpoll -0 -a 100 -r 2705 -t 4 192.168.178.61 10Register 2705 DVCC MaxChargeCurrent
evcc und battery_manager schreiben auf unterschiedliche Register:
| System | Register | Zweck |
|---|---|---|
| battery_manager | 2705 (MaxChargeCurrent) | Ladestrom-Limit |
| evcc | 2901 (ESS MinSoc) | Entladeschutz beim Schnellladen |
Kein Schreibkonflikt – beide arbeiten parallel.
Normalzustand: Reg 2901 = 10–20% → battery_manager: normaler Betrieb
Schnellladen: Reg 2901 ≈ SOC → battery_manager: Min-SOC angehoben
(z.B. SOC=70% → Reg 2901=70%)
Laden weiterhin erlaubt, Entladen blockiert
Fertig geladen: Reg 2901 = 10–20% → battery_manager: normaler Betrieb
# EvccMonitor liest Reg 2901 alle 30s via Modbus
if reg_2901_wert > 25:
evcc_discharge_locked = True
effective_min_soc = reg_2901_wert # statt battery.min_socGET http://evcc-host:7070/api/state
→ Zeigt Lademodus, Wallbox-Leistung im Dashboard
→ Kein Einfluss auf Ladesteuerung
- Kein API-Key, keine Registrierung
- Stündliche Globalstrahlung für Standort-Koordinaten
- Umrechnung:
PV [kWh] = Strahlung [W/m²] / 1000 × Peak [kWp] × Effizienz × (1 - Bewölkung × 0.3) - Aktualisierung: stündlich (konfigurierbar)
- Kostenlos für Privatnutzer: 10 API-Calls/Tag
- Berücksichtigt Modulausrichtung, Neigung, lokale Abschattung
- Registrierung: https://solcast.com/free-rooftop-solar-forecasting/
forecast:
provider: "solcast"
solcast_api_key: "dein-api-key"
solcast_resource_id: "deine-resource-uuid"pv_remaining_today → wieviel PV kommt heute noch?
night_consumption → wieviel Strom brauchen wir heute Nacht?
projected_evening_soc → SOC-Schätzung um 21:00 Uhr
→ Entscheidet ob Morgen-Verzögerung greift
python3 --version # mind. 3.9
python3 -m venv --help || sudo apt install python3-venvmkdir -p /home/pi/solar_battery
# Dateien kopieren: battery_manager.py, config.yaml,
# requirements.txt, solar-battery.service, install.sh
# config.yaml – Konfiguration (ANPASSEN vor dem Start)
# requirements.txt – Python-Abhängigkeiten
# solar-battery.service – Systemd-Service
# install.sh – Installationsskript# battery_manager.py – Einstiegspunkt / Hauptskript
# controller.py – Ladesteuerungs-Engine
# dashboard.py – Web-Dashboard (Flask)
# forecast.py – PV-Prognose (VRM / Solcast / Open-Meteo)
# modbus_victron.py – Modbus TCP Kommunikation mit Cerbo GX
# evcc.py – evcc REST-API Anbindung
# models.py – Datenklassen (SystemState, HourlyForecast, ...)
# logging_setup.py – Log-Deduplizierung
# version.py – VersionstringAlle Dateien liegen flach in /home/pi/solar_battery/ — keine Paketstruktur, kein Unterverzeichnis.
nano /home/pi/solar_battery/config.yamlMindestens anpassen:
modbus:
host: "192.168.178.61" # IP des Cerbo GX
evcc:
enabled: true
api_url: "http://localhost:7070/api/state" # oder IP wenn remote
# enabled: false wenn kein evcc vorhandenbash /home/pi/solar_battery/install.shDas Skript macht automatisch:
- Virtual Environment anlegen (
venv/) – System-Python bleibt unberührt - Python-Pakete installieren (
pymodbus,flask,requests,pyyaml) - Systemd-Service installieren und für Autostart aktivieren
sudo systemctl start solar-battery
sudo systemctl status solar-battery
tail -f /home/pi/solar_battery/battery_manager.loghttp://<raspi-ip>:5000
# ── Modbus TCP ──────────────────────────────────────────
modbus:
host: "192.168.178.61" # IP Cerbo GX – ANPASSEN
port: 502 # Standardport, nicht ändern
unit_id: 100 # Cerbo GX Unit-ID, fest 100
timeout_seconds: 5
# ── evcc ────────────────────────────────────────────────
evcc:
enabled: true
api_url: "http://localhost:7070/api/state" # ANPASSEN
timeout_seconds: 5
poll_interval_seconds: 30 # Wie oft Reg 2901 + REST lesen
# ── Batterie ────────────────────────────────────────────
battery:
capacity_kwh: 14.0 # Kapazität bei 100% SOC
min_soc: 25 # Untere Grenze [%] (LFP-Schonung)
max_soc: 98 # Obere Grenze [%] (LFP)
# target_soc_normal wird in v3.0 nicht mehr verwendet – Ziel wird dynamisch berechnet
target_soc_normal: 80 # (veraltet, bleibt für Rückwärtskompatibilität)
target_soc_full: 98 # Vollladen-Ziel [%]
full_charge_interval_days: 10 # Spätestens alle N Tage Vollladung (Zellbalancing)
min_charge_current: 0 # 0 = Laden gesperrt
max_charge_current: 50 # Max Ladestrom [A]
trickle_current: 5 # Sanft-Laden [A]
voltage_nominal: 48.0 # Nennspannung [V]
# ── PV-Anlage ───────────────────────────────────────────
pv:
peak_power_kwp: 10.0 # Anlagenleistung [kWp]
efficiency_factor: 0.82 # Systemwirkungsgrad
azimuth_deg: 180 # 180=Süd, 90=Ost, 270=West
tilt_deg: 30 # Neigungswinkel [°]
# ── Standort ────────────────────────────────────────────
location:
latitude: 48.7758 # GPS Breitengrad
longitude: 9.1829 # GPS Längengrad
timezone: "Europe/Berlin"
# ── Victron VRM API (empfohlen für beste Prognosequalität) ──
vrm:
enabled: true
access_token: "dein-token" # Von vrm.victronenergy.com/access-tokens
installation_id: "deine-id" # VRM Portal → Einstellungen → Allgemein
timeout_seconds: 10
# ── Prognose ────────────────────────────────────────────
forecast:
provider: "open_meteo" # open_meteo | solcast
update_interval_minutes: 60
solcast_api_key: "" # nur für provider=solcast
solcast_resource_id: ""
# ── Ladesteuerung (v3.0) ─────────────────────────────────
charging:
control_interval_seconds: 60 # Regelzyklus
soc_hysteresis: 2 # Puffer am Ziel [%]
current_ramp_step: 5 # Strom-Rampe [A/Zyklus]
# Wird nur als Fallback genutzt wenn VRM keine Verbrauchsprognose liefert
avg_daily_consumption_kwh: 8.0 # Durchschnittlicher Tagesverbrauch
default_night_consumption_kwh: 2.5 # Erwarteter Nachtverbrauch (Fallback)
emergency_charge_soc: 20 # Notfall-SOC [%] (unter min_soc möglich)
night_start_hour: 21
night_end_hour: 6
morning_delay_start_hour: 6
morning_delay_end_hour: 10
# ── v3.0.0: Adaptive Ladezeitfenster ─────────────────
solar_noon_offset_hours: 2 # ± Stunden um 13:00 (Sonnenhöchststand)
reduced_charge_current_a: 20 # Reduzierter Strom im Optimal-Fenster [A]
# ── Dashboard ───────────────────────────────────────────
dashboard:
enabled: true
host: "0.0.0.0"
port: 5000
refresh_interval_seconds: 30
state_file: "/home/pi/solar_battery/state.json"
# ── Logging ─────────────────────────────────────────────
logging:
level: "INFO" # DEBUG|INFO|WARNING|ERROR
file: "/home/pi/solar_battery/battery_manager.log"
max_size_mb: 10
backup_count: 3
log_decisions: true # Jede Entscheidung loggen| Parameter | Default | Beschreibung |
|---|---|---|
solar_noon_offset_hours |
2 | ± Stunden um 13:00 für das Hauptladefenster (11:00–15:00) |
reduced_charge_current_a |
20 | Reduzierter Ladestrom im Optimal-Fenster, um PV besser auszunutzen |
Beide Parameter sind optional – alte config.yaml ohne diese Felder funktioniert ohne Anpassung (Defaults greifen automatisch).
Aufruf: http://<raspi-ip>:5000
| Karte | Inhalt |
|---|---|
| Batterie SOC | SOC%, kWh, farbiger Ladebalken |
| Batterie | Spannung [V], Strom [A] mit Richtung ↑↓, Leistung [W] |
| Lademodus | idle/charging/trickle/full_charge, Strom-Sollwert, Tage seit Vollladung |
| PV Leistung | Aktuelle Leistung [W], Energie heute [kWh] |
| Verbrauch | Aktuelle Last [W], Energie heute [kWh] |
| Netz | Aktuelle Leistung [W] (+ Bezug / − Einspeisung) |
| PV Prognose | Gesamtprognose heute [kWh], noch verbleibend |
| Nachtverbrauch | Erwarteter Verbrauch [kWh], aktuelles Ziel-SOC |
Zeigt im Klartext warum gerade geladen/nicht geladen wird, z.B.:
"Morgen-Verzögerung: PV-Prognose heute 28.4 kWh, projizierter Abend-SOC 87% ≥ Ziel 80%. Kein Laden nötig."
Balkendiagramm: PV-Ertrag vs. Verbrauch pro Stunde, aktuelle Stunde hervorgehoben.
Stündliche Vorschau mit projiziertem SOC-Verlauf, Aktion und geplantem Ladestrom.

sudo systemctl start solar-battery # Starten
sudo systemctl stop solar-battery # Stoppen
sudo systemctl restart solar-battery # Neu starten
sudo systemctl status solar-battery # Status
sudo systemctl enable solar-battery # Autostart an
sudo systemctl disable solar-battery # Autostart aus# Live-Log
tail -f /home/pi/solar_battery/battery_manager.log
# Systemd-Journal
journalctl -u solar-battery -f
# Letzte 100 Zeilen
tail -100 /home/pi/solar_battery/battery_manager.log
# Nur Entscheidungen
grep -E "CHARGING|TRICKLE|IDLE|FULL|NOTFALL" battery_manager.log[INFO] Prognose (open_meteo): 28.4 kWh heute
[INFO] Modbus TCP: 192.168.178.61:502
[INFO] [IDLE] 0A | Morgen-Verzögerung: PV reicht...
[INFO] [CHARGING] 23A | PV-Überschuss: 1120W → 23A
[INFO] [TRICKLE] 5A | SOC 45% weit unter Ziel 80%
[INFO] [FULL_CHARGE] 50A | Vollladung: 8 Tage seit letzter
[INFO] Vollladung erreicht (98.1%), Balancing abgeschlossen
# Wann war die letzte Vollladung?
cat /home/pi/solar_battery/state.json# Erreichbarkeit
ping 192.168.178.61
nc -zv 192.168.178.61 502
# Modbus TCP aktivieren
# Cerbo GX: Einstellungen → Dienste → Modbus TCP → Ein# Rohwerte direkt lesen
sudo apt install mbpoll
mbpoll -0 -a 100 -r 843 -c 1 192.168.178.61 # SOC
mbpoll -0 -a 100 -r 840 -c 1 192.168.178.61 # Spannung (/10 → V)
mbpoll -0 -a 100 -r 811 -c 3 192.168.178.61 # PV L1/L2/L3
mbpoll -0 -a 100 -r 817 -c 3 192.168.178.61 # Last L1/L2/L3
mbpoll -0 -a 100 -r 820 -c 3 192.168.178.61 # Netz L1/L2/L3# DVCC-Register manuell schreiben (Test: 10A)
mbpoll -0 -a 100 -r 2705 -t 4 192.168.178.61 10
# Prüfen ob DVCC aktiv
# Cerbo GX: Einstellungen → DVCC → Ein
# Victron VRM: zeigt "Externe Steuerung" wenn DVCC aktiv# Ist ein alter Prozess noch aktiv?
ps aux | grep battery_manager
sudo systemctl restart solar-batterycurl http://localhost:7070/api/state | python3 -m json.tool | grep -E "mode|charging"
# Falls nicht localhost:
curl http://192.168.178.58:7070/api/state# Detaillierten Fehler anzeigen
journalctl -u solar-battery -n 50
tail -50 /home/pi/solar_battery/battery_manager.log
# Manuell testen
source /home/pi/solar_battery/venv/bin/activate
python3 battery_manager.py config.yamlsudo systemctl stop solar-battery
rm -rf /home/pi/solar_battery/venv
bash /home/pi/solar_battery/install.sh
sudo systemctl start solar-batteryRaspi (battery_manager) ──WireGuard──► Fritzbox ──Internet──► Fritzbox ──► Cerbo GX
192.168.168.54:5000 192.168.178.x .61
- config.yaml bleibt wie ist
- WireGuard-Tunnel für Modbus und evcc-API nötig
Raspi (evcc + battery_manager) ──LAN──► Cerbo GX
192.168.178.58 192.168.178.61
Änderungen in config.yaml:
modbus:
host: "192.168.178.61" # unverändert
evcc:
api_url: "http://localhost:7070/api/state" # localhost statt IP- Kein WireGuard nötig
- Beide Dienste laufen parallel, kein Ressourcenkonflikt (Go + Python)
- Raspi 3b mit 1GB RAM reicht für beide
# Prüfen nach Inbetriebnahme beider Dienste
free -h
top -b -n1 | grep -E "evcc|python"Victron nutzt für die Prognose Solcast-Satellitendaten kombiniert mit einem eigenen Machine-Learning-Modell, das auf der historischen Produktions- und Verbrauchshistorie der eigenen Anlage trainiert wurde. Das Ergebnis ist deutlich genauer als generische Wetterdaten.
Quellen:
- Victron Blog: https://www.victronenergy.com/blog/2023/07/05/new-vrm-solar-production-forecast-feature/
- VRM API Docs: https://vrm-api-docs.victronenergy.com/
- Ähnliches Projekt (Node-RED): https://akkudoktor.net/t/eine-art-netzdienliches-laden-mit-victron-node-red-flow/33885
| Bedingung | Details |
|---|---|
| VRM-Registrierung | Anlage muss in VRM registriert sein |
| Mindest-Historie | mind. 30 Tage Ertragsdaten in VRM |
| Standort gesetzt | GPS-Koordinaten in VRM hinterlegt |
| AC-gekoppelter PV-WR | wird vollständig unterstützt |
1. Access Token erstellen
https://vrm.victronenergy.com/access-tokens
→ "Add Token" → Name: battery_manager → kein Ablaufdatum → Create
→ Token sofort kopieren (wird nur einmal angezeigt!)
2. Installation ID ermitteln
VRM Portal → Einstellungen → Allgemein
→ "VRM-Installations-ID" (z.B. 318602)
3. config.yaml
vrm:
enabled: true
access_token: "dein-token" # geheim halten, nicht in Git!
installation_id: "deine-id"
timeout_seconds: 10GET https://vrmapi.victronenergy.com/v2/installations/{id}/stats
?type=forecast
&start={unix_timestamp_jetzt - 60s}
&end={unix_timestamp_sonnenuntergang}
&interval=hours
Header: x-authorization: Token {access_token}
{
"success": true,
"records": {
"solar_yield_forecast": [
[1690675200000, 870.39], // [unix_ms, Wh]
[1690678800000, 2540.12],
...
],
"vrm_consumption_fc": [
[1690675200000, 320.5],
...
]
},
"totals": {
"solar_yield_forecast": 26249.63, // Wh gesamt
"vrm_consumption_fc": 14065.60
}
}Wichtige Details:
- Zeitstempel sind Unix-Millisekunden (÷ 1000 für Python
datetime.fromtimestamp()) - Werte sind Wh pro Stunde (÷ 1000 → kWh)
- Abfrage von
jetzt − 60sbis Sonnenuntergang liefert den Restwert heute - Verbrauchsprognose (
vrm_consumption_fc) basiert auf historischem Verbrauchsmuster - Solar-Prognose für AC-gekoppelten PV-Wechselrichter: Feld
solar_yield_forecast(Summe aller Quellen)
Die Strategie aus der Community (bewährt, Genauigkeit ±1–2 kWh):
# Abfrage: (jetzt - 60s) bis Sonnenuntergang
start = int(time.time()) - 60
end = heute_21_uhr_unix
# Ergebnis = verbleibende PV für heute
# Gesamtprognose = bereits_erzeugt_heute + verbleibendIm battery_manager ist das in VrmForecastManager.fetch() implementiert.
VRM API verfügbar?
JA → VRM-Prognose verwenden (beste Qualität)
NEIN → Solcast (falls konfiguriert)
→ Open-Meteo (immer verfügbar, kein Key)
→ Dummy-Profil (Gauss-Kurve als Notfall)
Dashboard zeigt die aktive Quelle:
- VRM ★ – VRM API aktiv
- Solcast – Solcast API aktiv
- Open-Meteo – generische Wetterprognose
- Dummy
⚠️ – kein Internet, Notfall-Profil
- Prognose ist tagesgenau für heute (rollierend)
- Morgen-Prognose (next-day): in VRM-Portal sichtbar, aber API liefert nur 24–48h
- Bei vollständig geladenem Akku kann VRM die Erzeugung unterschätzen (Feed-in beschränkt den Ertrag)
- Neue Anlagen: bis zu 48h warten bis Prognose verfügbar ist
- Token hat vollen VRM-Zugriff → niemals in Git einchecken
vrm_consumption_fc ist besonders nützlich für die Nacht-Schätzung:
Tagessumme Verbrauch [Wh] ÷ 24h × Nachtstunden (21–6 Uhr = 9h)
→ Prognose nächtlicher Verbrauch
Damit wird avg_daily_consumption_kwh in config.yaml nur noch als Fallback genutzt, wenn VRM nicht verfügbar ist.
| Datei | Zweck |
|---|---|
battery_manager.py |
Hauptskript (~1200 Zeilen) |
config.yaml |
Alle Einstellungen |
requirements.txt |
Python-Abhängigkeiten |
install.sh |
Automatisches Installationsskript |
solar-battery.service |
Systemd-Service-Definition |
state.json |
Persistenter Zustand (auto-generiert) |
battery_manager.log |
Laufendes Log (auto-generiert) |
| Paket | Version | Zweck |
|---|---|---|
| pymodbus | ≥ 3.6 | Modbus TCP Client |
| flask | ≥ 3.0 | Web-Dashboard |
| requests | ≥ 2.31 | HTTP für Prognose-API + evcc |
| pyyaml | ≥ 6.0 | config.yaml parsen |
Offizielle Register-Tabelle: https://www.victronenergy.com/upload/documents/CCGX-Modbus-TCP-register-list-3.71.xlsx
Erstellt: Mai 2026 | Getestet mit: Victron Cerbo GX Venus OS, Raspberry Pi OS Bookworm, pymodbus 3.13