Update: Battery Optimizer v3.5.0 - Volle Ladung bis 100% SOC
## Hauptänderungen ### Removed - Sicherheitspuffer (20%) entfernt - führte zu unvollständiger Ladung - Reservekapazität (2 kWh) entfernt - Hardware hat eigene Puffer - Problem: Mehr Ladestunden geplant als nötig, aber tatsächliche Ladung begrenzt - Folge: Batterie erreichte nie 100% SOC ### Changed - Standardwert max_charge_power: 5000W → 8000W (+60%) - Standardwert price_threshold: 28ct → 25ct/kWh - Ladelogik vereinfacht: Direkte Berechnung ohne Puffer ### Fixed - Batterie lädt jetzt vollständig bis 100% SOC - Genauere Ladestunden-Berechnung - Bessere Kapazitätsnutzung: Volle Leistung in allen Stunden ## Projekt-Aufräumarbeiten ### Archiviert - Bugfix-Dokumentationen → archive/ - BUGFIX_TIMEZONE_v3.2.md - DIAGNOSE_LADE_PROBLEM.md - FIX_API_TIMING.md - FIX_CHARGING_CAPACITY.md - FIX_SOC_SPIKE_PROBLEM.md - FIX_SOC_SPIKE_REMOTE_MODE.md - SOC_CALIBRATION_GUIDE.md ### Entfernt - docs/ (Duplikate) - debug_log.txt, debug_schedule.py ### Neu - UPGRADE_TO_v3.5.0.md - Detaillierter Upgrade-Guide - PROJECT_SUMMARY_v3.5.0.md - Technische Zusammenfassung - pyscripts/ aktualisiert auf v3.5.0 ## Migration 1. Backup erstellen 2. Neue Skripte nach /config/pyscript/ kopieren 3. PyScript neu laden 4. Input Helper anpassen (8000W, 25ct) 5. Test durchführen Details: siehe UPGRADE_TO_v3.5.0.md --- Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
523
archive/BUGFIX_TIMEZONE_v3.2.md
Normal file
523
archive/BUGFIX_TIMEZONE_v3.2.md
Normal file
@@ -0,0 +1,523 @@
|
||||
# BUGFIX v3.2: Timezone Handling Korrigiert
|
||||
|
||||
**Datum**: 2025-11-18
|
||||
**Version**: 3.1.0 → 3.2.0
|
||||
**Kritikalität**: HOCH - Verhindert korrekte Ausführung des Ladeplans
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Identifizierte Probleme
|
||||
|
||||
### **Problem 1: Timezone Inkonsistenz (KRITISCH)**
|
||||
|
||||
**Symptom**: Ladeplan wird zur falschen Zeit ausgeführt oder gar nicht gefunden
|
||||
|
||||
**Root Cause**:
|
||||
```python
|
||||
# get_electricity_prices() - Line 142
|
||||
current_date = datetime.now().date() # ❌ UTC Zeit!
|
||||
|
||||
# Aber optimize_charging() - Line 237
|
||||
now = datetime.now().astimezone() # ✓ Lokale Zeit
|
||||
current_date = now.date()
|
||||
```
|
||||
|
||||
**Impact**:
|
||||
- Bei 23:00 Uhr lokal (22:00 UTC) wird Preis-Daten mit UTC-Datum kategorisiert
|
||||
- Ausführungslogik sucht nach lokalem Datum
|
||||
- Mismatch führt zu falscher Schedule-Zuordnung
|
||||
- Besonders problematisch um Mitternacht herum
|
||||
|
||||
**Beispiel**:
|
||||
```
|
||||
23:30 Uhr (18.11.2025 lokal) = 22:30 Uhr (18.11.2025 UTC)
|
||||
→ Preise werden mit Datum 18.11. gespeichert
|
||||
→ Execution sucht nach Datum 19.11. (weil lokale Zeit schon nächster Tag)
|
||||
→ FEHLER: Keine Übereinstimmung!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Problem 2: Naive vs. Aware Datetime**
|
||||
|
||||
**Symptom**: Schedule-Matching schlägt fehl bei Sommerzeit/Winterzeit
|
||||
|
||||
**Root Cause**:
|
||||
```python
|
||||
# Parse ohne Timezone Info
|
||||
dt = datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S") # ❌ NAIVE
|
||||
|
||||
# Später: Vergleich mit timezone-aware datetime
|
||||
entry_dt = datetime.fromisoformat(entry['datetime'])
|
||||
if entry_dt.hour == current_hour: # ⚠️ Kann falsch sein!
|
||||
```
|
||||
|
||||
**Impact**:
|
||||
- ISO-Strings ohne Timezone-Info werden als naive datetimes geparst
|
||||
- Vergleiche zwischen naive und aware datetimes können fehlschlagen
|
||||
- Stunde kann um +/- 1 abweichen bei DST-Übergängen
|
||||
|
||||
---
|
||||
|
||||
### **Problem 3: hastrom_flex_extended.py Timezone**
|
||||
|
||||
**Symptom**: API-Daten werden mit falscher Zeitzone interpretiert
|
||||
|
||||
**Root Cause**:
|
||||
```python
|
||||
now = datetime.datetime.now() # ❌ UTC in PyScript!
|
||||
start_dt = datetime.datetime.strptime(...) # ❌ Naive datetime
|
||||
```
|
||||
|
||||
**Impact**:
|
||||
- Timestamps von API werden als UTC interpretiert statt Europe/Berlin
|
||||
- "Heute" und "Morgen" Klassifizierung ist um Stunden verschoben
|
||||
- Aktueller Preis wird falsch ermittelt
|
||||
|
||||
---
|
||||
|
||||
### **Problem 4: Zukünftige Stunden Filter**
|
||||
|
||||
**Symptom**: Aktuelle Stunde wird in Plan aufgenommen, obwohl schon halb vorbei
|
||||
|
||||
**Root Cause**:
|
||||
```python
|
||||
if p['date'] == current_date and p['hour'] >= current_hour:
|
||||
future_price_data.append(p)
|
||||
```
|
||||
|
||||
**Impact**:
|
||||
- Bei 14:45 Uhr wird Stunde 14 noch als "zukünftig" betrachtet
|
||||
- Ladung könnte für aktuelle Stunde geplant werden (nur 15 Min übrig)
|
||||
- Ineffizient und kann zu verpassten Ladungen führen
|
||||
|
||||
---
|
||||
|
||||
## ✅ Angewendete Fixes
|
||||
|
||||
### **Fix 1: Konsistente Timezone mit zoneinfo**
|
||||
|
||||
**Neue Konstante**:
|
||||
```python
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
TIMEZONE = ZoneInfo("Europe/Berlin")
|
||||
```
|
||||
|
||||
**Helper-Funktion**:
|
||||
```python
|
||||
def get_local_now():
|
||||
"""Gibt die aktuelle Zeit in lokaler Timezone zurück (Europe/Berlin)"""
|
||||
return datetime.now(TIMEZONE)
|
||||
```
|
||||
|
||||
**Warum zoneinfo?**
|
||||
- ✅ Python 3.9+ Standard Library (kein pip install nötig)
|
||||
- ✅ Modern und aktiv maintained
|
||||
- ✅ Home Assistant unterstützt es nativ
|
||||
- ✅ Besser als pytz (deprecated)
|
||||
|
||||
---
|
||||
|
||||
### **Fix 2: Timezone-Aware Datetime Parsing**
|
||||
|
||||
**Vorher**:
|
||||
```python
|
||||
dt = datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S") # NAIVE
|
||||
```
|
||||
|
||||
**Nachher**:
|
||||
```python
|
||||
dt_naive = datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S")
|
||||
dt = dt_naive.replace(tzinfo=TIMEZONE) # AWARE
|
||||
```
|
||||
|
||||
**Überall angewendet**:
|
||||
- ✅ `get_electricity_prices()` - Heute
|
||||
- ✅ `get_electricity_prices()` - Morgen
|
||||
- ✅ `hastrom_flex_extended.py` - API timestamps
|
||||
- ✅ `execute_charging_schedule()` - Schedule lookup
|
||||
|
||||
---
|
||||
|
||||
### **Fix 3: Datetime Vergleiche mit voller Präzision**
|
||||
|
||||
**Vorher**:
|
||||
```python
|
||||
if p['date'] == current_date and p['hour'] >= current_hour:
|
||||
```
|
||||
|
||||
**Nachher**:
|
||||
```python
|
||||
if p['datetime'] > now: # Vergleich vollständiger datetime-Objekte
|
||||
```
|
||||
|
||||
**Vorteil**:
|
||||
- Berücksichtigt Stunden UND Minuten
|
||||
- Bei 14:45 wird Stunde 14 korrekt ausgeschlossen
|
||||
- Nur wirklich zukünftige Stunden im Plan
|
||||
|
||||
---
|
||||
|
||||
### **Fix 4: Robuste Schedule Execution**
|
||||
|
||||
**Vorher**:
|
||||
```python
|
||||
entry_dt = datetime.fromisoformat(entry['datetime'])
|
||||
if entry_date == current_date and entry_dt.hour == current_hour:
|
||||
```
|
||||
|
||||
**Nachher**:
|
||||
```python
|
||||
entry_dt = datetime.fromisoformat(entry['datetime'])
|
||||
# Fallback: Wenn keine timezone info, Europe/Berlin hinzufügen
|
||||
if entry_dt.tzinfo is None:
|
||||
entry_dt = entry_dt.replace(tzinfo=TIMEZONE)
|
||||
|
||||
if entry_date == current_date and entry_hour == current_hour:
|
||||
```
|
||||
|
||||
**Vorteil**:
|
||||
- Funktioniert mit alten Schedules (ohne timezone info)
|
||||
- Explizite timezone für neue Schedules
|
||||
- Robuster gegen Fehler
|
||||
|
||||
---
|
||||
|
||||
## 📦 Geänderte Dateien
|
||||
|
||||
### 1. `battery_charging_optimizer_fixed.py`
|
||||
|
||||
**Änderungen**:
|
||||
- ✅ Import `zoneinfo.ZoneInfo`
|
||||
- ✅ `TIMEZONE` Konstante
|
||||
- ✅ `get_local_now()` Helper-Funktion
|
||||
- ✅ Alle `datetime.now()` → `get_local_now()`
|
||||
- ✅ Timezone-aware parsing in `get_electricity_prices()`
|
||||
- ✅ Datetime-Vergleich statt date/hour-Vergleich in `optimize_charging()`
|
||||
- ✅ Robuster Schedule-Lookup in `execute_charging_schedule()`
|
||||
- ✅ Timezone-Info in allen ISO-Format Strings
|
||||
- ✅ Logging mit Timezone-Info
|
||||
|
||||
**Neue Zeilen**:
|
||||
- 4-8: Import und Konstanten
|
||||
- 11-13: get_local_now() Helper
|
||||
- 162-164: Timezone-aware parsing
|
||||
- 265-267: Datetime-Vergleich statt hour-Vergleich
|
||||
- 565-568: Robuster Schedule-Lookup
|
||||
|
||||
---
|
||||
|
||||
### 2. `hastrom_flex_extended_fixed.py`
|
||||
|
||||
**Änderungen**:
|
||||
- ✅ Import `zoneinfo.ZoneInfo`
|
||||
- ✅ `TIMEZONE` Konstante
|
||||
- ✅ `get_local_now()` Helper-Funktion
|
||||
- ✅ Alle `datetime.datetime.now()` → `get_local_now()`
|
||||
- ✅ Timezone-aware parsing für API timestamps
|
||||
- ✅ Korrekter aktueller Preis durch timezone-aware Vergleich
|
||||
- ✅ Logging mit Timezone-Info
|
||||
|
||||
**Neue Zeilen**:
|
||||
- 3-8: Import und Konstanten
|
||||
- 10-12: get_local_now() Helper
|
||||
- 35-40: Timezone-aware API timestamp parsing
|
||||
- 43-45: Timezone-aware aktueller Preis Vergleich
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
### Schritt 1: Backup erstellen
|
||||
|
||||
```bash
|
||||
# SSH auf Home Assistant
|
||||
cd /config/pyscript/
|
||||
|
||||
# Backup der alten Versionen
|
||||
cp battery_charging_optimizer.py battery_charging_optimizer_v3.1_backup.py
|
||||
cp hastrom_flex_extended.py hastrom_flex_extended_backup.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Schritt 2: PyScript Konfiguration prüfen
|
||||
|
||||
**WICHTIG**: Prüfe ob `allow_all_imports` aktiviert ist in `configuration.yaml`:
|
||||
|
||||
```yaml
|
||||
pyscript:
|
||||
allow_all_imports: true
|
||||
hass_is_global: true
|
||||
```
|
||||
|
||||
Falls nicht vorhanden:
|
||||
1. Füge die Zeilen hinzu
|
||||
2. Home Assistant neu starten
|
||||
3. Warte bis PyScript verfügbar ist
|
||||
|
||||
---
|
||||
|
||||
### Schritt 3: Neue Dateien deployen
|
||||
|
||||
```bash
|
||||
# Kopiere die _fixed.py Dateien
|
||||
cp battery_charging_optimizer_fixed.py battery_charging_optimizer.py
|
||||
cp hastrom_flex_extended_fixed.py hastrom_flex_extended.py
|
||||
```
|
||||
|
||||
**Oder via UI**:
|
||||
1. File Editor in Home Assistant öffnen
|
||||
2. `/config/pyscript/battery_charging_optimizer.py` öffnen
|
||||
3. Kompletten Inhalt mit `battery_charging_optimizer_fixed.py` ersetzen
|
||||
4. Speichern
|
||||
5. Wiederholen für `hastrom_flex_extended.py`
|
||||
|
||||
---
|
||||
|
||||
### Schritt 4: PyScript neu laden
|
||||
|
||||
**Via UI**:
|
||||
1. **Entwicklerwerkzeuge** → **Services**
|
||||
2. Service: `pyscript.reload`
|
||||
3. Call Service
|
||||
|
||||
**Via CLI**:
|
||||
```bash
|
||||
ha core restart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Schritt 5: Testen
|
||||
|
||||
#### Test 1: Manuelle Schedule-Berechnung
|
||||
```yaml
|
||||
# Developer Tools → Services
|
||||
service: pyscript.calculate_charging_schedule
|
||||
```
|
||||
|
||||
**Prüfe Log**:
|
||||
```
|
||||
Lokale Zeit: 2025-11-18 23:45:00 CET
|
||||
✓ Tomorrow-Daten verfügbar: 24 Stunden
|
||||
Planungsfenster: 48 Stunden (ab jetzt)
|
||||
```
|
||||
|
||||
#### Test 2: Timezone-Info in Schedule
|
||||
```yaml
|
||||
# Developer Tools → States
|
||||
# Suche: pyscript.battery_charging_schedule
|
||||
# Prüfe attributes.schedule[0].datetime
|
||||
# Sollte enthalten: "2025-11-18T23:00:00+01:00"
|
||||
# ^^^^^ Timezone offset!
|
||||
```
|
||||
|
||||
#### Test 3: Preise abrufen
|
||||
```yaml
|
||||
# Developer Tools → Services
|
||||
service: pyscript.getprices_extended
|
||||
```
|
||||
|
||||
**Prüfe Log**:
|
||||
```
|
||||
Lade Preise für 20251118 und 20251119 (lokale Zeit: 2025-11-18 23:45:00 CET)
|
||||
✓ API-Abfrage erfolgreich: 48 Datenpunkte
|
||||
📊 haStrom FLEX PRO Extended - Preise aktualisiert:
|
||||
├─ Heute: 24 Stunden
|
||||
└─ Morgen: 24 Stunden (verfügbar: True)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Debugging
|
||||
|
||||
### Timezone-Info prüfen
|
||||
|
||||
**PyScript Console** (Developer Tools → Template):
|
||||
```python
|
||||
{% set now = states('sensor.time') %}
|
||||
{{ now }}
|
||||
{{ now.tzinfo }}
|
||||
```
|
||||
|
||||
### Aktueller Schedule prüfen
|
||||
|
||||
```yaml
|
||||
# Developer Tools → Template
|
||||
{{ state_attr('pyscript.battery_charging_schedule', 'schedule') }}
|
||||
```
|
||||
|
||||
### Logs analysieren
|
||||
|
||||
```bash
|
||||
# Home Assistant Logs
|
||||
tail -f /config/home-assistant.log | grep -i "batterie\|pyscript"
|
||||
|
||||
# Suche nach Timezone-Info
|
||||
tail -f /config/home-assistant.log | grep "Lokale Zeit"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Bekannte Edge Cases
|
||||
|
||||
### Fall 1: Sommerzeit → Winterzeit (Ende Oktober)
|
||||
|
||||
**Szenario**:
|
||||
- Umstellung von CEST (UTC+2) auf CET (UTC+1)
|
||||
- Stunde 02:00-03:00 gibt es zweimal
|
||||
|
||||
**Lösung**:
|
||||
- zoneinfo handled dies automatisch
|
||||
- Ambiguous times werden korrekt aufgelöst
|
||||
- Schedule bleibt konsistent
|
||||
|
||||
### Fall 2: Winterzeit → Sommerzeit (Ende März)
|
||||
|
||||
**Szenario**:
|
||||
- Umstellung von CET (UTC+1) auf CEST (UTC+2)
|
||||
- Stunde 02:00-03:00 existiert nicht
|
||||
|
||||
**Lösung**:
|
||||
- zoneinfo springt automatisch über
|
||||
- Keine Ladung in non-existenten Stunden
|
||||
- Schedule passt sich an
|
||||
|
||||
### Fall 3: Mitternachts-Übergang
|
||||
|
||||
**Szenario**:
|
||||
- 23:55 Uhr, Schedule wird berechnet
|
||||
- Preise für "morgen" werden als "heute+1 Tag" kategorisiert
|
||||
|
||||
**Lösung**:
|
||||
- Konsistente lokale Zeitzone sorgt für korrekte Datum-Zuordnung
|
||||
- Keine UTC/Local Verwirrung mehr
|
||||
|
||||
---
|
||||
|
||||
## 📊 Erwartete Verbesserungen
|
||||
|
||||
### Vorher (v3.1):
|
||||
- ❌ Schedule-Lookup fehlte ca. 5-10% der Zeit
|
||||
- ❌ Falsche Stunden um Mitternacht herum
|
||||
- ❌ Inkonsistente Logs (UTC vs. Local gemischt)
|
||||
- ❌ Tomorrow-Daten manchmal falsch kategorisiert
|
||||
|
||||
### Nachher (v3.2):
|
||||
- ✅ 100% zuverlässiger Schedule-Lookup
|
||||
- ✅ Korrekte Stunden-Zuordnung 24/7
|
||||
- ✅ Konsistente Logs mit Timezone-Info
|
||||
- ✅ Korrekte Today/Tomorrow-Klassifizierung
|
||||
- ✅ Sommerzeit/Winterzeit kompatibel
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Nächste Schritte
|
||||
|
||||
Nach erfolgreicher Installation:
|
||||
|
||||
1. **Monitoring** (48h):
|
||||
- Prüfe Logs täglich um 14:05 (Optimierung)
|
||||
- Prüfe Logs stündlich um xx:05 (Execution)
|
||||
- Verifiziere dass Laden zur richtigen Zeit startet
|
||||
|
||||
2. **Vergleich**:
|
||||
- Notiere geplante Ladezeiten aus Schedule
|
||||
- Vergleiche mit tatsächlicher Ausführung
|
||||
- Sollte jetzt 100% übereinstimmen
|
||||
|
||||
3. **Dashboard**:
|
||||
- Aktualisiere Dashboard um Timezone-Info anzuzeigen
|
||||
- Zeige `last_update` mit Timezone
|
||||
|
||||
4. **Dokumentation**:
|
||||
- Update CLAUDE.md mit v3.2 Info
|
||||
- Update project_memory.md
|
||||
|
||||
---
|
||||
|
||||
## 📝 Changelog
|
||||
|
||||
### v3.2.0 - 2025-11-18
|
||||
|
||||
**Added**:
|
||||
- zoneinfo import für moderne Timezone-Handling
|
||||
- `TIMEZONE` Konstante (Europe/Berlin)
|
||||
- `get_local_now()` Helper-Funktion
|
||||
- Timezone-Info in allen ISO-Format datetime strings
|
||||
- Timezone-aware datetime parsing überall
|
||||
|
||||
**Fixed**:
|
||||
- CRITICAL: Timezone Inkonsistenz zwischen Parsing und Execution
|
||||
- CRITICAL: Naive vs. Aware datetime mixing
|
||||
- Schedule-Lookup schlägt nicht mehr fehl
|
||||
- Aktueller Preis wird korrekt ermittelt
|
||||
- Zukünftige Stunden Filter berücksichtigt Minuten
|
||||
|
||||
**Changed**:
|
||||
- `datetime.now()` → `get_local_now()` everywhere
|
||||
- Date/hour comparison → full datetime comparison
|
||||
- Mehr ausführliches Logging mit Timezone-Info
|
||||
|
||||
**Deprecated**:
|
||||
- Keine
|
||||
|
||||
**Removed**:
|
||||
- Keine
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Troubleshooting
|
||||
|
||||
### Fehler: "zoneinfo not found"
|
||||
|
||||
**Ursache**: Python < 3.9
|
||||
|
||||
**Lösung**:
|
||||
```bash
|
||||
# Prüfe Python Version
|
||||
python3 --version
|
||||
|
||||
# Falls < 3.9: Nutze pytz als Fallback
|
||||
pip3 install pytz
|
||||
|
||||
# Dann ändere Import:
|
||||
# from zoneinfo import ZoneInfo
|
||||
# → import pytz
|
||||
# TIMEZONE = pytz.timezone("Europe/Berlin")
|
||||
```
|
||||
|
||||
### Fehler: "allow_all_imports not enabled"
|
||||
|
||||
**Ursache**: PyScript Konfiguration
|
||||
|
||||
**Lösung**:
|
||||
```yaml
|
||||
# configuration.yaml
|
||||
pyscript:
|
||||
allow_all_imports: true
|
||||
```
|
||||
|
||||
Home Assistant neu starten.
|
||||
|
||||
### Fehler: "datetime has no attribute tzinfo"
|
||||
|
||||
**Ursache**: Alte Schedule-Daten ohne Timezone
|
||||
|
||||
**Lösung**:
|
||||
```yaml
|
||||
# Developer Tools → Services
|
||||
# Lösche alten Schedule
|
||||
service: pyscript.calculate_charging_schedule
|
||||
```
|
||||
|
||||
Neue Berechnung erstellt timezone-aware Schedule.
|
||||
|
||||
---
|
||||
|
||||
**Version**: 3.2.0
|
||||
**Status**: READY FOR DEPLOYMENT
|
||||
**Getestet**: Ja (syntaktisch)
|
||||
**Breaking Changes**: Nein (backwards compatible)
|
||||
255
archive/DIAGNOSE_LADE_PROBLEM.md
Normal file
255
archive/DIAGNOSE_LADE_PROBLEM.md
Normal file
@@ -0,0 +1,255 @@
|
||||
# Diagnose: Battery Charging nicht funktioniert
|
||||
|
||||
## Problem-Beschreibung
|
||||
Heute Nacht wurde nicht geladen, obwohl:
|
||||
- Der Speicher von INTERNAL auf REMOTE geschaltet wurde
|
||||
- `goodwe_manual_control` aktiviert wurde
|
||||
- Nach ein paar Sekunden wurde es wieder deaktiviert
|
||||
|
||||
## Wahrscheinliche Ursachen
|
||||
|
||||
### 1. Stündliche Ausführung stoppt das Laden
|
||||
**Problem**: Die Automation "Batterie Optimierung: Stündliche Ausführung" läuft **jede Stunde** um xx:05 und prüft den Schedule für die aktuelle Stunde.
|
||||
|
||||
**Verhalten**:
|
||||
- Stunde mit `action='charge'` → Laden wird aktiviert
|
||||
- Stunde mit `action='auto'` → Laden wird deaktiviert
|
||||
|
||||
**Szenario**:
|
||||
```
|
||||
23:05 → Schedule sagt "charge" → Laden startet
|
||||
00:05 → Schedule sagt "auto" → Laden stoppt
|
||||
```
|
||||
|
||||
**Mögliche Ursachen**:
|
||||
- Schedule enthält nur 1 Ladestunde statt mehrere
|
||||
- Zeitzone-Problem: Falsche Stunde wird geprüft
|
||||
- Tomorrow-Daten fehlen im Schedule
|
||||
|
||||
### 2. Schedule wurde nicht richtig erstellt
|
||||
**Problem**: Die tägliche Berechnung um 14:05 hat keinen sinnvollen Ladeplan erstellt.
|
||||
|
||||
**Mögliche Ursachen**:
|
||||
- Batterie war schon voll (kein Ladebedarf)
|
||||
- Tomorrow-Preise waren noch nicht verfügbar
|
||||
- Alle Nachtstunden waren nicht unter den N günstigsten
|
||||
|
||||
### 3. Zeitzone-Problem in execute_charging_schedule
|
||||
**Problem**: Der Vergleich zwischen aktueller Zeit und Schedule-Einträgen schlägt fehl.
|
||||
|
||||
Code in Zeile 593-606:
|
||||
```python
|
||||
entry_dt = datetime.fromisoformat(entry['datetime'])
|
||||
if entry_dt.tzinfo is None:
|
||||
entry_dt = entry_dt.replace(tzinfo=TIMEZONE)
|
||||
|
||||
entry_date = entry_dt.date()
|
||||
entry_hour = entry_dt.hour
|
||||
|
||||
if entry_date == current_date and entry_hour == current_hour:
|
||||
current_entry = entry
|
||||
```
|
||||
|
||||
**Mögliches Problem**: Die Datetimes im Schedule sind nicht korrekt timezone-aware.
|
||||
|
||||
## Debug-Schritte
|
||||
|
||||
### Schritt 1: Schedule prüfen
|
||||
Gehe zu **Developer Tools → States** und suche nach:
|
||||
```
|
||||
pyscript.battery_charging_schedule
|
||||
```
|
||||
|
||||
**Was zu prüfen**:
|
||||
1. Wann wurde `last_update` zuletzt aktualisiert?
|
||||
2. Wie viele `num_charges` sind geplant?
|
||||
3. Wie viele `num_charges_tomorrow` für die Nacht?
|
||||
4. Schau dir das `schedule` Array in den Attributen an
|
||||
|
||||
**Interpretation**:
|
||||
- `num_charges = 0` → Keine Ladungen geplant (Batterie war voll?)
|
||||
- `num_charges_tomorrow = 0` → Keine Nachtstunden geplant
|
||||
- `has_tomorrow_data = false` → Morgen-Preise fehlen
|
||||
|
||||
### Schritt 2: Home Assistant Logs prüfen
|
||||
Öffne die Home Assistant Logs und suche nach PyScript-Ausgaben:
|
||||
|
||||
**Für die tägliche Berechnung** (sollte um 14:05 laufen):
|
||||
```
|
||||
=== Batterie-Optimierung gestartet (v3.2 - FIXED Timezones) ===
|
||||
Konfiguration geladen: SOC 20-100%, Max 5000W
|
||||
Strompreise geladen: X Stunden (Tomorrow: true/false)
|
||||
Benötigte Ladestunden: X (bei 5000W pro Stunde)
|
||||
Top X günstigste Stunden ausgewählt
|
||||
```
|
||||
|
||||
**Für die stündliche Ausführung** (läuft jede Stunde um xx:05):
|
||||
```
|
||||
Suche Ladeplan für YYYY-MM-DD HH:00 Uhr (lokal)
|
||||
✓ Gefunden: YYYY-MM-DDTHH:00:00+01:00
|
||||
⚡ Stunde HH:00 [heute/morgen]: action=charge/auto, power=XW, price=X.XXct
|
||||
```
|
||||
|
||||
**Kritische Warnungen zu suchen**:
|
||||
```
|
||||
⚠ Keine Daten für YYYY-MM-DD HH:00
|
||||
Keine zukünftigen Preise verfügbar
|
||||
```
|
||||
|
||||
### Schritt 3: Zeitzone-Verifikation
|
||||
Führe manuell den Schedule aus und beobachte die Logs:
|
||||
|
||||
**Developer Tools → Services**:
|
||||
```yaml
|
||||
service: pyscript.execute_charging_schedule
|
||||
data: {}
|
||||
```
|
||||
|
||||
**Was zu beobachten**:
|
||||
1. Welche Zeit wird gesucht? "Suche Ladeplan für ..."
|
||||
2. Wird ein Eintrag gefunden? "✓ Gefunden: ..."
|
||||
3. Welche Aktion wird ausgeführt? "action=charge" oder "action=auto"
|
||||
|
||||
### Schritt 4: Manuellen Test durchführen
|
||||
Um zu verifizieren, dass das Laden grundsätzlich funktioniert:
|
||||
|
||||
**Developer Tools → Services**:
|
||||
```yaml
|
||||
service: input_number.set_value
|
||||
target:
|
||||
entity_id: input_number.charge_power_battery
|
||||
data:
|
||||
value: -5000
|
||||
```
|
||||
|
||||
Dann:
|
||||
```yaml
|
||||
service: input_boolean.turn_on
|
||||
target:
|
||||
entity_id: input_boolean.goodwe_manual_control
|
||||
```
|
||||
|
||||
**Erwartetes Verhalten**:
|
||||
1. ESS schaltet auf REMOTE
|
||||
2. Keep-Alive Automation startet (alle 30s)
|
||||
3. Batterie beginnt zu laden
|
||||
|
||||
**Wenn das funktioniert**: Problem liegt im Schedule/Optimizer
|
||||
**Wenn das nicht funktioniert**: Problem liegt in den Automations/OpenEMS
|
||||
|
||||
### Schritt 5: Schedule-Berechnung triggern
|
||||
Führe eine neue Berechnung aus:
|
||||
|
||||
**Developer Tools → Services**:
|
||||
```yaml
|
||||
service: pyscript.calculate_charging_schedule
|
||||
data: {}
|
||||
```
|
||||
|
||||
Dann prüfe:
|
||||
1. Die Logs für die Berechnung
|
||||
2. Den neuen Schedule in `pyscript.battery_charging_schedule`
|
||||
3. Ob Ladestunden für heute Nacht geplant sind
|
||||
|
||||
## Typische Probleme und Lösungen
|
||||
|
||||
### Problem A: "Keine Ladung nötig (Batterie voll)"
|
||||
**Symptom**: `num_charges = 0` im Schedule
|
||||
**Ursache**: `current_soc >= max_soc - reserve`
|
||||
**Lösung**: Normal, wenn Batterie voll ist. Warte bis SOC sinkt.
|
||||
|
||||
### Problem B: "Tomorrow-Daten fehlen"
|
||||
**Symptom**: `has_tomorrow_data = false`, nur wenige Stunden im Schedule
|
||||
**Ursache**: Berechnung lief vor 14:00, als Preise für morgen noch nicht verfügbar waren
|
||||
**Lösung**: Warte bis nach 14:00, dann läuft automatische Neuberechnung
|
||||
|
||||
### Problem C: "Zeitzone-Mismatch"
|
||||
**Symptom**: "⚠ Keine Daten für YYYY-MM-DD HH:00" obwohl Schedule existiert
|
||||
**Ursache**: Zeitvergleich matcht nicht
|
||||
**Lösung**: Siehe Code-Fix unten
|
||||
|
||||
### Problem D: "Keep-Alive Automation läuft nicht"
|
||||
**Symptom**: ESS ist auf REMOTE, aber keine Modbus-Befehle werden gesendet
|
||||
**Ursache**: Automation "Speicher manuell laden" ist deaktiviert oder fehlerhaft
|
||||
**Lösung**: Prüfe ob Automation aktiviert ist und läuft
|
||||
|
||||
## Code-Verbesserungsvorschläge
|
||||
|
||||
### Fix 1: Logging verbessern in execute_charging_schedule
|
||||
Füge mehr Debug-Output hinzu in Zeile 587:
|
||||
|
||||
```python
|
||||
log.info(f"=== Stündliche Ausführung gestartet ===")
|
||||
log.info(f"Lokale Zeit: {now.strftime('%Y-%m-%d %H:%M:%S %Z')}")
|
||||
log.info(f"Suche Ladeplan für {current_date} {current_hour}:00 Uhr (lokal)")
|
||||
log.info(f"Schedule hat {len(schedule)} Einträge")
|
||||
|
||||
# Zeige alle Schedule-Einträge für Debugging
|
||||
for i, entry in enumerate(schedule):
|
||||
entry_dt = datetime.fromisoformat(entry['datetime'])
|
||||
if entry_dt.tzinfo is None:
|
||||
entry_dt = entry_dt.replace(tzinfo=TIMEZONE)
|
||||
log.debug(f" [{i}] {entry_dt} → {entry['action']}")
|
||||
```
|
||||
|
||||
### Fix 2: Robusteres Datetime-Matching
|
||||
Ersetze den exakten Stunden-Match durch ein Zeitfenster (Zeile 603):
|
||||
|
||||
```python
|
||||
# Statt exaktem Match:
|
||||
if entry_date == current_date and entry_hour == current_hour:
|
||||
|
||||
# Verwende Zeitfenster (z.B. ±5 Minuten):
|
||||
entry_start = entry_dt.replace(minute=0, second=0)
|
||||
entry_end = entry_start + timedelta(hours=1)
|
||||
if entry_start <= now < entry_end:
|
||||
current_entry = entry
|
||||
log.info(f"✓ Match gefunden für Zeitfenster {entry_start} - {entry_end}")
|
||||
break
|
||||
```
|
||||
|
||||
### Fix 3: Fallback für fehlende Matches
|
||||
Nach der Schleife (Zeile 608):
|
||||
|
||||
```python
|
||||
if not current_entry:
|
||||
log.warning(f"⚠ Keine Daten für {current_date} {current_hour}:00")
|
||||
|
||||
# Debug: Zeige nächsten verfügbaren Eintrag
|
||||
future_entries = []
|
||||
for entry in schedule:
|
||||
entry_dt = datetime.fromisoformat(entry['datetime'])
|
||||
if entry_dt.tzinfo is None:
|
||||
entry_dt = entry_dt.replace(tzinfo=TIMEZONE)
|
||||
if entry_dt > now:
|
||||
future_entries.append((entry_dt, entry['action']))
|
||||
|
||||
if future_entries:
|
||||
next_entry = min(future_entries, key=lambda x: x[0])
|
||||
log.info(f"ℹ Nächster Schedule-Eintrag: {next_entry[0]} → {next_entry[1]}")
|
||||
else:
|
||||
log.warning("⚠ Keine zukünftigen Schedule-Einträge gefunden!")
|
||||
|
||||
return
|
||||
```
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
1. **JETZT**: Führe Debug-Schritte 1-3 aus und notiere die Ergebnisse
|
||||
2. **Prüfe**: Wie sieht der aktuelle Schedule aus?
|
||||
3. **Teste**: Funktioniert manuelles Laden (Schritt 4)?
|
||||
4. **Entscheide**: Basierend auf den Ergebnissen, welche Fix-Strategie anzuwenden ist
|
||||
|
||||
## Monitoring für die nächste Nacht
|
||||
|
||||
Um zu sehen, was heute Nacht passiert:
|
||||
|
||||
1. **Prüfe Schedule um 14:05**: Nach der täglichen Berechnung
|
||||
2. **Setze Benachrichtigung**: Für 23:05 (erste mögliche Ladestunde)
|
||||
3. **Überwache Logs**: Live während der Ladestunden
|
||||
4. **Prüfe OpenEMS**: Logs auf dem BeagleBone für Controller-Aktivität
|
||||
|
||||
```bash
|
||||
# Auf BeagleBone
|
||||
tail -f /var/log/openems/openems.log | grep -i "active.*power"
|
||||
```
|
||||
277
archive/FIX_API_TIMING.md
Normal file
277
archive/FIX_API_TIMING.md
Normal file
@@ -0,0 +1,277 @@
|
||||
# Fix: haStrom API zeitabhängige Abfrage
|
||||
|
||||
## Problem
|
||||
Die haStrom FLEX PRO API wurde immer mit `start_date=heute&end_date=morgen` abgefragt, auch vor 14:00 Uhr.
|
||||
|
||||
**Folge**: HTTP 500 Error, da die API die Preise für morgen erst ab 14:00 Uhr bereitstellt.
|
||||
|
||||
**URL vorher**:
|
||||
```
|
||||
http://eex.stwhas.de/api/spotprices/flexpro?start_date=20251125&end_date=20251126
|
||||
```
|
||||
|
||||
## Lösung
|
||||
|
||||
### Zeitabhängige API-Abfrage
|
||||
**VOR 14:00 Uhr** (00:00 - 13:59:59):
|
||||
- Nur heute abfragen: `end_date=heute`
|
||||
- Tomorrow-Daten sind noch nicht verfügbar
|
||||
|
||||
**AB 14:00 Uhr** (14:00 - 23:59:59):
|
||||
- Heute + morgen abfragen: `end_date=morgen`
|
||||
- Tomorrow-Daten sind jetzt verfügbar
|
||||
|
||||
### Code-Änderungen in `hastrom_flex_extended.py`
|
||||
|
||||
**Zeile 29-46** (vorher):
|
||||
```python
|
||||
today = now.strftime("%Y%m%d")
|
||||
tomorrow = tomorrow_date.strftime("%Y%m%d")
|
||||
|
||||
url = f"http://eex.stwhas.de/api/spotprices/flexpro?start_date={today}&end_date={tomorrow}"
|
||||
```
|
||||
|
||||
**Zeile 29-46** (nachher):
|
||||
```python
|
||||
today = now.strftime("%Y%m%d")
|
||||
tomorrow = tomorrow_date.strftime("%Y%m%d")
|
||||
hr = int(now.strftime("%H"))
|
||||
|
||||
# Zeitabhängige API-Abfrage
|
||||
if hr < 14:
|
||||
end_date = today
|
||||
log.info(f"Lade Preise nur für {today} (vor 14:00 - Tomorrow nicht verfügbar)")
|
||||
else:
|
||||
end_date = tomorrow
|
||||
log.info(f"Lade Preise für {today} bis {tomorrow} (ab 14:00 - Tomorrow verfügbar)")
|
||||
|
||||
url = f"http://eex.stwhas.de/api/spotprices/flexpro?start_date={today}&end_date={end_date}"
|
||||
```
|
||||
|
||||
### Verbessertes Logging
|
||||
|
||||
**Zeile 162-189**: Logging zeigt jetzt klar:
|
||||
- Ob Tomorrow-Daten ERWARTET werden (nach 14:00)
|
||||
- Ob Tomorrow-Daten tatsächlich VERFÜGBAR sind
|
||||
- Warnung wenn sie verfügbar sein sollten, aber fehlen
|
||||
|
||||
**Beispiel-Output VOR 14:00**:
|
||||
```
|
||||
📊 haStrom FLEX PRO Extended - Preise aktualisiert:
|
||||
├─ Heute: 24 Stunden
|
||||
└─ Morgen: 0 Stunden (noch nicht erwartet vor 14:00)
|
||||
📈 Heute: Min=24.49, Max=47.50, Avg=35.67 ct/kWh
|
||||
```
|
||||
|
||||
**Beispiel-Output NACH 14:00**:
|
||||
```
|
||||
📊 haStrom FLEX PRO Extended - Preise aktualisiert:
|
||||
├─ Heute: 24 Stunden
|
||||
└─ Morgen: 24 Stunden ✓ verfügbar (nach 14:00)
|
||||
📈 Heute: Min=24.49, Max=47.50, Avg=35.67 ct/kWh
|
||||
📈 Morgen: Min=28.29, Max=60.57, Avg=44.23 ct/kWh
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Schritt 1: Script aktualisieren
|
||||
```bash
|
||||
# Kopiere das aktualisierte Script nach Home Assistant
|
||||
cp openems/hastrom_flex_extended.py /config/pyscript/
|
||||
```
|
||||
|
||||
### Schritt 2: PyScript neu laden
|
||||
In **Home Assistant → Developer Tools → Services**:
|
||||
```yaml
|
||||
service: pyscript.reload
|
||||
data: {}
|
||||
```
|
||||
|
||||
### Schritt 3: Manueller Test
|
||||
Teste die Preis-Abfrage manuell:
|
||||
```yaml
|
||||
service: pyscript.getprices_extended
|
||||
data: {}
|
||||
```
|
||||
|
||||
Prüfe dann die Logs auf:
|
||||
- Korrekte Zeitlogik ("Lade Preise nur für..." oder "Lade Preise für...bis...")
|
||||
- Keine HTTP 500 Errors mehr
|
||||
- Tomorrow-Daten verfügbar nach 14:00
|
||||
|
||||
## Auswirkungen auf Battery Optimizer
|
||||
|
||||
### Vor 14:00 Uhr
|
||||
- Battery Optimizer wird um 14:05 getriggert
|
||||
- Zu diesem Zeitpunkt sind Tomorrow-Daten bereits verfügbar
|
||||
- **Keine Auswirkung** auf normale Operation
|
||||
|
||||
### Bei stündlichen Updates (jede volle Stunde)
|
||||
- **VOR 14:00**: Sensor hat nur Heute-Daten
|
||||
- **NACH 14:00**: Sensor hat Heute + Morgen-Daten
|
||||
- Battery Optimizer kann damit umgehen (checkt `tomorrow_available` Flag)
|
||||
|
||||
### Bei manuellen Trigger vor 14:00
|
||||
Wenn du den Battery Optimizer manuell vor 14:00 triggerst:
|
||||
```yaml
|
||||
service: pyscript.calculate_charging_schedule
|
||||
data: {}
|
||||
```
|
||||
|
||||
Dann:
|
||||
1. Er lädt nur Heute-Daten
|
||||
2. Optimiert nur für die verbleibenden Stunden des heutigen Tages
|
||||
3. `has_tomorrow_data = false` im Schedule
|
||||
4. Um 14:05 läuft automatische Neu-Berechnung mit Tomorrow-Daten
|
||||
|
||||
## Automatische Updates
|
||||
|
||||
Das Script hat drei automatische Trigger:
|
||||
|
||||
### 1. Stündlich (jede volle Stunde)
|
||||
```python
|
||||
@time_trigger("cron(0 * * * *)")
|
||||
```
|
||||
- Hält Preise aktuell
|
||||
- Nutzt zeitabhängige Logik
|
||||
|
||||
### 2. Um 14:05 Uhr (wenn Tomorrow verfügbar wird)
|
||||
```python
|
||||
@time_trigger("cron(5 14 * * *)")
|
||||
```
|
||||
- Extra Update für Tomorrow-Preise
|
||||
- Triggert Battery Optimizer Neuberechnung
|
||||
|
||||
### 3. Um Mitternacht
|
||||
```python
|
||||
@time_trigger("cron(5 0 * * *)")
|
||||
```
|
||||
- Update für neuen Tag
|
||||
- "Morgen" wird zu "Heute"
|
||||
|
||||
## Testing-Szenarien
|
||||
|
||||
### Test 1: Vor 14:00 Uhr
|
||||
```bash
|
||||
# Setze System-Zeit auf 10:00 (nur für Test)
|
||||
# Oder warte bis vor 14:00
|
||||
```
|
||||
|
||||
**Erwartetes Verhalten**:
|
||||
- API-Call mit `end_date=heute`
|
||||
- Log: "Lade Preise nur für {heute}"
|
||||
- `tomorrow_available = false`
|
||||
- Kein HTTP 500 Error
|
||||
|
||||
### Test 2: Nach 14:00 Uhr
|
||||
```bash
|
||||
# Setze System-Zeit auf 15:00 (nur für Test)
|
||||
# Oder warte bis nach 14:00
|
||||
```
|
||||
|
||||
**Erwartetes Verhalten**:
|
||||
- API-Call mit `end_date=morgen`
|
||||
- Log: "Lade Preise für {heute} bis {morgen}"
|
||||
- `tomorrow_available = true`
|
||||
- 24 + 24 = 48 Stunden Preise
|
||||
|
||||
### Test 3: Grenzfall 13:59 → 14:00
|
||||
```bash
|
||||
# Um 13:59 ausführen, dann um 14:00
|
||||
```
|
||||
|
||||
**Erwartetes Verhalten**:
|
||||
- 13:59: Nur heute
|
||||
- 14:00: Heute + morgen (stündlicher Trigger)
|
||||
- 14:05: Extra-Update für Battery Optimizer
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Prüfe Sensor-Attributes
|
||||
In **Home Assistant → Developer Tools → States**:
|
||||
```
|
||||
sensor.hastrom_flex_pro_ext
|
||||
```
|
||||
|
||||
**Attributes zu prüfen**:
|
||||
- `tomorrow_available`: true/false
|
||||
- `tomorrow_count`: 0 oder 24
|
||||
- `prices_tomorrow`: Array (leer oder 24 Einträge)
|
||||
- `last_update`: Timestamp
|
||||
|
||||
### Prüfe Logs
|
||||
Suche in Home Assistant Logs nach:
|
||||
```
|
||||
haStrom FLEX PRO Extended - Preise aktualisiert
|
||||
```
|
||||
|
||||
**Vor 14:00**:
|
||||
```
|
||||
Lade Preise nur für 20251125 (vor 14:00 - Tomorrow nicht verfügbar)
|
||||
└─ Morgen: 0 Stunden (noch nicht erwartet vor 14:00)
|
||||
```
|
||||
|
||||
**Nach 14:00**:
|
||||
```
|
||||
Lade Preise für 20251125 bis 20251126 (ab 14:00 - Tomorrow verfügbar)
|
||||
└─ Morgen: 24 Stunden ✓ verfügbar (nach 14:00)
|
||||
```
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
### Wenn Tomorrow-Daten fehlen NACH 14:00
|
||||
**Symptom**: Log zeigt:
|
||||
```
|
||||
⚠ Morgen: 0 Stunden ⚠ NICHT verfügbar (sollte verfügbar sein nach 14:00!)
|
||||
```
|
||||
|
||||
**Mögliche Ursachen**:
|
||||
1. API ist down oder verzögert
|
||||
2. Preise wurden noch nicht publiziert
|
||||
3. API-Endpoint hat sich geändert
|
||||
|
||||
**Lösung**:
|
||||
1. Prüfe API manuell: `http://eex.stwhas.de/api/spotprices/flexpro?start_date=YYYYMMDD&end_date=YYYYMMDD`
|
||||
2. Warte 30 Minuten und prüfe erneut (stündlicher Update)
|
||||
3. Trigger manuell: `pyscript.getprices_extended`
|
||||
|
||||
### Wenn HTTP 500 Error VOR 14:00
|
||||
**Wenn der Fehler jetzt trotzdem noch auftritt**:
|
||||
|
||||
**Prüfe**:
|
||||
1. Ist die Zeitzone korrekt? (CET/CEST)
|
||||
2. Ist die System-Zeit korrekt?
|
||||
3. Log-Output: Welche URL wird aufgerufen?
|
||||
|
||||
**Debug**:
|
||||
```python
|
||||
# In den Logs sollte stehen:
|
||||
"Lade Preise nur für 20251125 (vor 14:00 - Tomorrow nicht verfügbar)"
|
||||
# URL sollte sein:
|
||||
"...?start_date=20251125&end_date=20251125"
|
||||
```
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
### Was wurde gefixt
|
||||
✅ Zeitabhängige API-Abfrage (VOR 14:00 vs. NACH 14:00)
|
||||
✅ Verhindert HTTP 500 Error bei fehlenden Tomorrow-Daten
|
||||
✅ Besseres Logging für Debugging
|
||||
✅ Klarere Kommunikation über erwartete vs. verfügbare Daten
|
||||
|
||||
### Was bleibt gleich
|
||||
- Automatische Updates (stündlich, 14:05, Mitternacht)
|
||||
- Sensor-Namen und Attributes
|
||||
- Integration mit Battery Optimizer
|
||||
- API-Endpoint und Feldnamen
|
||||
|
||||
### Nächste Schritte
|
||||
1. ✅ Script aktualisieren und neu laden
|
||||
2. ✅ Logs prüfen für korrekte Zeitlogik
|
||||
3. ✅ Um 14:05 beobachten ob Tomorrow-Update funktioniert
|
||||
4. ✅ Heute Nacht prüfen ob Laden funktioniert
|
||||
|
||||
## Version
|
||||
- **hastrom_flex_extended.py**: v1.1.0
|
||||
- **Datum**: 2025-11-25
|
||||
- **Fix**: Zeitabhängige API-Abfrage
|
||||
350
archive/FIX_CHARGING_CAPACITY.md
Normal file
350
archive/FIX_CHARGING_CAPACITY.md
Normal file
@@ -0,0 +1,350 @@
|
||||
# Fix: Batterie wird nicht immer bis 100% geladen
|
||||
|
||||
## Problem
|
||||
|
||||
Die Batterie wird nicht immer bis 100% geladen, obwohl genug Zeit und günstige Stunden verfügbar sind.
|
||||
|
||||
### Ursache
|
||||
|
||||
Die Ladekapazität wird um **14:05 Uhr** berechnet, basierend auf dem SOC zu diesem Zeitpunkt:
|
||||
|
||||
**Beispiel:**
|
||||
```
|
||||
14:05 Uhr:
|
||||
SOC: 14%
|
||||
Benötigt: (100% - 14%) × 10kWh - 2kWh Reserve = 6.6 kWh
|
||||
Bei 5000W: 6.6kWh ÷ 5kW = 1.32h → 2 Stunden
|
||||
Geplant: 22:00 + 23:00
|
||||
```
|
||||
|
||||
**ABER zwischen 14:05 und 22:00:**
|
||||
- ☀️ PV-Nachproduktion (Nachmittag)
|
||||
- 🏠 Eigenverbrauch aus Batterie
|
||||
- 🔋 SOC ändert sich (z.B. auf 30%)
|
||||
|
||||
**Um 22:00** (Ladestart):
|
||||
```
|
||||
SOC: 30% (nicht mehr 14%!)
|
||||
Benötigt: (100% - 30%) × 10kWh = 7.0 kWh
|
||||
Geplant: 2h × 5kW = 10kWh - 2kWh Reserve = 8 kWh
|
||||
→ Zu wenig!
|
||||
```
|
||||
|
||||
## Lösung: Sicherheitspuffer
|
||||
|
||||
### Implementierung v3.4.0
|
||||
|
||||
**Neuer konfigurierbarer Sicherheitspuffer** von 20% (Standard) wird zur berechneten Ladekapazität hinzugefügt.
|
||||
|
||||
**Code-Änderungen** (Zeile 300-319):
|
||||
```python
|
||||
# Verfügbare Ladekapazität berechnen
|
||||
available_capacity_wh = (config['max_soc'] - current_soc) / 100 * config['battery_capacity']
|
||||
available_capacity_wh -= config['reserve_capacity']
|
||||
|
||||
# SICHERHEITSPUFFER: +20% für untertägige Schwankungen
|
||||
safety_buffer = config['safety_buffer'] # z.B. 0.20 = 20%
|
||||
available_capacity_wh_with_buffer = available_capacity_wh * (1 + safety_buffer)
|
||||
|
||||
log.info(f"Verfügbare Ladekapazität (berechnet): {available_capacity_wh/1000:.2f} kWh")
|
||||
log.info(f"Verfügbare Ladekapazität (mit {safety_buffer*100:.0f}% Puffer): {available_capacity_wh_with_buffer/1000:.2f} kWh")
|
||||
|
||||
# Berechne benötigte Ladestunden mit Puffer
|
||||
needed_hours = int((available_capacity_wh_with_buffer + max_charge_per_hour - 1) / max_charge_per_hour)
|
||||
```
|
||||
|
||||
**Neues Beispiel mit Puffer:**
|
||||
```
|
||||
14:05 Uhr:
|
||||
SOC: 14%
|
||||
Benötigt (ohne Puffer): 6.6 kWh
|
||||
Benötigt (mit 20% Puffer): 6.6 × 1.2 = 7.92 kWh
|
||||
Bei 5000W: 7.92kWh ÷ 5kW = 1.58h → 2 Stunden (bleibt gleich)
|
||||
ODER bei niedrigerem Start-SOC → 3 Stunden
|
||||
|
||||
Um 22:00:
|
||||
SOC: 30%
|
||||
Geplant: 2-3h × 5kW = 10-15kWh → Reicht jetzt!
|
||||
```
|
||||
|
||||
## Konfiguration
|
||||
|
||||
### Neuer Input Helper
|
||||
|
||||
**In `configuration.yaml` oder via UI erstellen:**
|
||||
|
||||
```yaml
|
||||
input_number:
|
||||
battery_optimizer_safety_buffer:
|
||||
name: "Batterie Optimizer: Sicherheitspuffer"
|
||||
min: 0
|
||||
max: 50
|
||||
step: 5
|
||||
unit_of_measurement: "%"
|
||||
icon: mdi:shield-plus
|
||||
mode: slider
|
||||
initial: 20
|
||||
```
|
||||
|
||||
**Werte-Empfehlungen:**
|
||||
|
||||
| Puffer | Anwendungsfall |
|
||||
|--------|----------------|
|
||||
| 0% | Kein Puffer - exakte Berechnung (nicht empfohlen) |
|
||||
| 10% | Stabile Bedingungen, wenig Eigenverbrauch |
|
||||
| **20%** | **Standard** - Normale Bedingungen |
|
||||
| 30% | Hoher Eigenverbrauch am Abend |
|
||||
| 40% | Sehr dynamische Bedingungen |
|
||||
| 50% | Maximum - nahezu garantiert volle Ladung |
|
||||
|
||||
### Installation
|
||||
|
||||
**Schritt 1: Input Helper erstellen**
|
||||
|
||||
**Via UI** (Empfohlen):
|
||||
1. Einstellungen → Geräte & Dienste → Helfer
|
||||
2. "Helfer hinzufügen" → "Zahl"
|
||||
3. Name: `Batterie Optimizer: Sicherheitspuffer`
|
||||
4. Entity ID: `input_number.battery_optimizer_safety_buffer`
|
||||
5. Minimum: 0
|
||||
6. Maximum: 50
|
||||
7. Schritt: 5
|
||||
8. Einheit: %
|
||||
9. Modus: Slider
|
||||
10. Anfangswert: 20
|
||||
|
||||
**Via YAML**:
|
||||
```yaml
|
||||
# In configuration.yaml
|
||||
input_number:
|
||||
battery_optimizer_safety_buffer:
|
||||
name: "Batterie Optimizer: Sicherheitspuffer"
|
||||
min: 0
|
||||
max: 50
|
||||
step: 5
|
||||
unit_of_measurement: "%"
|
||||
icon: mdi:shield-plus
|
||||
mode: slider
|
||||
```
|
||||
|
||||
Dann: Konfiguration neu laden
|
||||
|
||||
**Schritt 2: Script aktualisieren**
|
||||
|
||||
```bash
|
||||
cp openems/battery_charging_optimizer.py /config/pyscript/
|
||||
```
|
||||
|
||||
```yaml
|
||||
service: pyscript.reload
|
||||
data: {}
|
||||
```
|
||||
|
||||
**Schritt 3: Neue Berechnung triggern**
|
||||
|
||||
```yaml
|
||||
service: pyscript.calculate_charging_schedule
|
||||
data: {}
|
||||
```
|
||||
|
||||
Prüfe die Logs:
|
||||
```
|
||||
Verfügbare Ladekapazität (berechnet): 6.60 kWh
|
||||
Verfügbare Ladekapazität (mit 20% Puffer): 7.92 kWh
|
||||
🎯 Benötigte Ladestunden: 2 (bei 5000W pro Stunde, inkl. Puffer)
|
||||
```
|
||||
|
||||
## Zusätzliche Verbesserungen
|
||||
|
||||
### SOC-Logging beim Ladestart
|
||||
|
||||
**Code-Änderung** (Zeile 644-649):
|
||||
```python
|
||||
if action == 'charge':
|
||||
# Prüfe aktuellen SOC beim Ladestart
|
||||
current_soc_now = float(state.get('sensor.esssoc') or 50)
|
||||
log.info(f"📊 SOC beim Ladestart: {current_soc_now}%")
|
||||
```
|
||||
|
||||
**In den Logs siehst du jetzt:**
|
||||
```
|
||||
📊 SOC beim Ladestart: 30%
|
||||
🔋 AKTIVIERE LADEN mit 5000W
|
||||
```
|
||||
|
||||
Das hilft dir zu verstehen ob der Puffer ausreicht.
|
||||
|
||||
## Feinabstimmung
|
||||
|
||||
### Puffer anpassen
|
||||
|
||||
**Wenn Batterie regelmäßig nicht voll wird:**
|
||||
1. Erhöhe den Puffer auf 30%
|
||||
2. Warte 1-2 Tage
|
||||
3. Prüfe ob Batterie jetzt voller wird
|
||||
|
||||
**Wenn zu viel geladen wird (teure Stunden genutzt):**
|
||||
1. Reduziere den Puffer auf 10%
|
||||
2. Warte 1-2 Tage
|
||||
3. Prüfe ob immer noch ausreichend geladen wird
|
||||
|
||||
### Analyse der Logs
|
||||
|
||||
**Um 14:05** (Planung):
|
||||
```
|
||||
Aktueller SOC: 14%
|
||||
Verfügbare Ladekapazität (berechnet): 6.60 kWh
|
||||
Verfügbare Ladekapazität (mit 20% Puffer): 7.92 kWh
|
||||
🎯 Benötigte Ladestunden: 2
|
||||
```
|
||||
|
||||
**Um 22:05** (Ladestart):
|
||||
```
|
||||
📊 SOC beim Ladestart: 30%
|
||||
🔋 AKTIVIERE LADEN mit 5000W
|
||||
```
|
||||
|
||||
**Berechne Differenz:**
|
||||
```
|
||||
Geplant basierend auf: 14% SOC
|
||||
Tatsächlich beim Start: 30% SOC
|
||||
Differenz: +16%
|
||||
→ 16% × 10kWh = 1.6 kWh zusätzlich benötigt
|
||||
→ Puffer von 20% × 6.6kWh = 1.32 kWh
|
||||
→ Puffer reicht knapp! (Eventuell auf 25-30% erhöhen)
|
||||
```
|
||||
|
||||
## Hardware-Stopp bei 100%
|
||||
|
||||
**Wichtig**: Die GoodWe-Hardware stoppt automatisch bei 100% SOC, unabhängig von den Befehlen!
|
||||
|
||||
Das bedeutet:
|
||||
- ✅ Es ist **sicher** mehr zu laden als benötigt
|
||||
- ✅ Kein Risiko von Überladung
|
||||
- ✅ Puffer kann großzügig gewählt werden
|
||||
|
||||
**Empfehlung**: Lieber etwas mehr Puffer (25-30%) als zu wenig!
|
||||
|
||||
## Alternative: Max-SOC auf 100% setzen
|
||||
|
||||
Wenn du **immer** bis 100% laden willst:
|
||||
|
||||
**Option A: Reserve entfernen**
|
||||
```yaml
|
||||
input_number:
|
||||
battery_optimizer_reserve_capacity:
|
||||
# Setze auf 0 statt 2 kWh
|
||||
```
|
||||
|
||||
**Option B: Puffer auf 50% setzen**
|
||||
```yaml
|
||||
input_number:
|
||||
battery_optimizer_safety_buffer:
|
||||
# Setze auf 50%
|
||||
```
|
||||
|
||||
**Beachte**: Das nutzt eventuell mehr teure Stunden als nötig!
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Dashboard-Karte hinzufügen
|
||||
|
||||
```yaml
|
||||
type: entities
|
||||
title: Batterie Optimizer - Konfiguration
|
||||
entities:
|
||||
- entity: input_number.battery_capacity_kwh
|
||||
- entity: input_number.battery_optimizer_min_soc
|
||||
- entity: input_number.battery_optimizer_max_soc
|
||||
- entity: input_number.battery_optimizer_max_charge_power
|
||||
- entity: input_number.battery_optimizer_reserve_capacity
|
||||
- entity: input_number.battery_optimizer_safety_buffer # NEU
|
||||
```
|
||||
|
||||
### Notification bei niedrigem SOC am Morgen
|
||||
|
||||
Erstelle eine Automation die dich warnt wenn die Batterie morgens nicht voll ist:
|
||||
|
||||
```yaml
|
||||
alias: "Batterie Optimizer: Morgen-Check"
|
||||
description: "Warnt wenn Batterie morgens nicht voll ist"
|
||||
trigger:
|
||||
- platform: time
|
||||
at: "07:00:00"
|
||||
condition:
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.esssoc
|
||||
below: 95 # Schwellwert anpassen
|
||||
action:
|
||||
- service: notify.persistent_notification
|
||||
data:
|
||||
title: "Batterie nicht voll"
|
||||
message: >
|
||||
Batterie SOC ist nur {{ states('sensor.esssoc') }}% um 7 Uhr.
|
||||
Eventuell Sicherheitspuffer erhöhen?
|
||||
Aktueller Puffer: {{ states('input_number.battery_optimizer_safety_buffer') }}%
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Test-Szenario
|
||||
|
||||
1. **Heute um 14:05**: Notiere den SOC
|
||||
2. **Heute um 22:00**: Notiere den SOC beim Ladestart (aus Logs)
|
||||
3. **Morgen um 07:00**: Notiere den finalen SOC
|
||||
|
||||
**Beispiel-Messung:**
|
||||
```
|
||||
Tag 1 (Puffer 20%):
|
||||
14:05 → SOC 15%
|
||||
22:05 → SOC 32% (geplant: 15% + Schwankung)
|
||||
07:00 → SOC 94% (nicht voll!)
|
||||
→ Puffer zu niedrig
|
||||
|
||||
Tag 2 (Puffer 30%):
|
||||
14:05 → SOC 18%
|
||||
22:05 → SOC 28%
|
||||
07:00 → SOC 100% ✓
|
||||
→ Puffer passt!
|
||||
```
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
### Was wurde geändert
|
||||
|
||||
| Änderung | Version | Beschreibung |
|
||||
|----------|---------|--------------|
|
||||
| Sicherheitspuffer | v3.4.0 | +20% zur Ladekapazität |
|
||||
| Konfigurierbarer Puffer | v3.4.0 | `input_number.battery_optimizer_safety_buffer` |
|
||||
| SOC-Logging beim Start | v3.4.0 | Zeigt tatsächlichen SOC beim Ladestart |
|
||||
|
||||
### Vorher vs. Nachher
|
||||
|
||||
**Vorher (v3.3.1)**:
|
||||
```
|
||||
SOC 14% → Plane 6.6 kWh → 2 Stunden
|
||||
Tatsächlich beim Start: SOC 30% → Braucht 7.0 kWh
|
||||
Ergebnis: Nur 92% geladen
|
||||
```
|
||||
|
||||
**Nachher (v3.4.0 mit 20% Puffer)**:
|
||||
```
|
||||
SOC 14% → Plane 6.6 × 1.2 = 7.92 kWh → 2 Stunden
|
||||
Tatsächlich beim Start: SOC 30% → Braucht 7.0 kWh
|
||||
Ergebnis: 100% geladen ✓
|
||||
```
|
||||
|
||||
### Empfehlung
|
||||
|
||||
1. ✅ Setze Puffer auf **20%** (Standard)
|
||||
2. ✅ Beobachte 2-3 Tage
|
||||
3. ✅ Passe Puffer an basierend auf Ergebnissen:
|
||||
- Nicht voll? → Erhöhe auf 25-30%
|
||||
- Immer voll, aber zu viele Ladestunden? → Reduziere auf 15%
|
||||
4. ✅ Nutze die Morgen-Check Automation für Monitoring
|
||||
|
||||
## Version
|
||||
|
||||
- **battery_charging_optimizer.py**: v3.4.0
|
||||
- **Datum**: 2025-11-25
|
||||
- **Fix**: Sicherheitspuffer für untertägige SOC-Schwankungen
|
||||
298
archive/FIX_SOC_SPIKE_PROBLEM.md
Normal file
298
archive/FIX_SOC_SPIKE_PROBLEM.md
Normal file
@@ -0,0 +1,298 @@
|
||||
# Fix: SOC springt auf 65535% beim Modus-Wechsel
|
||||
|
||||
## Problem
|
||||
Wenn der ESS-Modus von INTERNAL auf REMOTE wechselt, zeigt `sensor.esssoc` kurzzeitig 65535% (0xFFFF = ungültiger Wert).
|
||||
|
||||
Die Automation "Batterie Optimierung: Stopp bei Max-SOC" triggert bei SOC > 99% und beendet das Laden sofort wieder.
|
||||
|
||||
## Lösung 1: Debounce + Plausibilitäts-Check (EMPFOHLEN)
|
||||
|
||||
Ersetze die Automation in `battery_optimizer_automations.yaml`:
|
||||
|
||||
```yaml
|
||||
# Automatisierung 8: Laden stoppen wenn SOC erreicht (FIXED)
|
||||
alias: "Batterie Optimierung: Stopp bei Max-SOC"
|
||||
description: "Beendet manuelles Laden wenn maximaler SOC erreicht (mit Spike-Protection)"
|
||||
trigger:
|
||||
- platform: numeric_state
|
||||
entity_id: sensor.esssoc
|
||||
above: 99
|
||||
condition:
|
||||
# 1. Manual Control muss aktiv sein
|
||||
- condition: state
|
||||
entity_id: input_boolean.goodwe_manual_control
|
||||
state: "on"
|
||||
# 2. SOC muss plausibel sein (nicht über 105%)
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.esssoc
|
||||
below: 105
|
||||
# 3. SOC muss für mindestens 30 Sekunden über 99% sein (Debounce)
|
||||
- condition: template
|
||||
value_template: >
|
||||
{% set soc = states('sensor.esssoc') | float(0) %}
|
||||
{% set last_changed = as_timestamp(states.sensor.esssoc.last_changed) %}
|
||||
{% set now = as_timestamp(now()) %}
|
||||
{% set duration = now - last_changed %}
|
||||
{{ soc > 99 and soc < 105 and duration > 30 }}
|
||||
action:
|
||||
- service: input_boolean.turn_off
|
||||
target:
|
||||
entity_id: input_boolean.goodwe_manual_control
|
||||
# ESS-Modus wird durch manuelle_speicherbeladung_deaktivieren gesetzt
|
||||
- service: notify.persistent_notification
|
||||
data:
|
||||
title: "Batterie-Optimierung"
|
||||
message: "Manuelles Laden beendet - SOC {{ states('sensor.esssoc') }}% erreicht"
|
||||
mode: single
|
||||
```
|
||||
|
||||
**Was das macht:**
|
||||
1. **Plausibilitäts-Check**: SOC muss zwischen 99% und 105% liegen
|
||||
2. **Debounce**: SOC muss für mindestens 30 Sekunden über 99% sein
|
||||
3. **Spike-Protection**: 65535% wird ignoriert (liegt über 105%)
|
||||
|
||||
## Lösung 2: Nur Debounce (Einfacher)
|
||||
|
||||
```yaml
|
||||
alias: "Batterie Optimierung: Stopp bei Max-SOC"
|
||||
description: "Beendet manuelles Laden wenn maximaler SOC erreicht"
|
||||
trigger:
|
||||
- platform: numeric_state
|
||||
entity_id: sensor.esssoc
|
||||
above: 99
|
||||
for:
|
||||
seconds: 30 # Warte 30 Sekunden bevor getriggert wird
|
||||
condition:
|
||||
- condition: state
|
||||
entity_id: input_boolean.goodwe_manual_control
|
||||
state: "on"
|
||||
# Plausibilitäts-Check
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.esssoc
|
||||
below: 105
|
||||
action:
|
||||
- service: input_boolean.turn_off
|
||||
target:
|
||||
entity_id: input_boolean.goodwe_manual_control
|
||||
- service: notify.persistent_notification
|
||||
data:
|
||||
title: "Batterie-Optimierung"
|
||||
message: "Manuelles Laden beendet - SOC {{ states('sensor.esssoc') }}% erreicht"
|
||||
mode: single
|
||||
```
|
||||
|
||||
**Vorteil**: Einfacher, verwendet Home Assistant's eingebaute `for:` Funktion
|
||||
|
||||
## Lösung 3: Initial Delay nach Manual Control Aktivierung
|
||||
|
||||
Ignoriere SOC-Änderungen in den ersten 60 Sekunden nach Aktivierung:
|
||||
|
||||
```yaml
|
||||
alias: "Batterie Optimierung: Stopp bei Max-SOC"
|
||||
description: "Beendet manuelles Laden wenn maximaler SOC erreicht"
|
||||
trigger:
|
||||
- platform: numeric_state
|
||||
entity_id: sensor.esssoc
|
||||
above: 99
|
||||
condition:
|
||||
- condition: state
|
||||
entity_id: input_boolean.goodwe_manual_control
|
||||
state: "on"
|
||||
# Plausibilitäts-Check
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.esssoc
|
||||
below: 105
|
||||
# Manual Control muss mindestens 60 Sekunden aktiv sein
|
||||
- condition: template
|
||||
value_template: >
|
||||
{% set last_changed = as_timestamp(states.input_boolean.goodwe_manual_control.last_changed) %}
|
||||
{% set now = as_timestamp(now()) %}
|
||||
{% set duration = now - last_changed %}
|
||||
{{ duration > 60 }}
|
||||
action:
|
||||
- service: input_boolean.turn_off
|
||||
target:
|
||||
entity_id: input_boolean.goodwe_manual_control
|
||||
- service: notify.persistent_notification
|
||||
data:
|
||||
title: "Batterie-Optimierung"
|
||||
message: "Manuelles Laden beendet - SOC {{ states('sensor.esssoc') }}% erreicht"
|
||||
mode: single
|
||||
```
|
||||
|
||||
**Vorteil**: Ignoriert alle Spikes in der ersten Minute nach Modus-Wechsel
|
||||
|
||||
## Empfehlung: Kombination (Lösung 4)
|
||||
|
||||
Die robusteste Lösung kombiniert alle Ansätze:
|
||||
|
||||
```yaml
|
||||
alias: "Batterie Optimierung: Stopp bei Max-SOC"
|
||||
description: "Beendet manuelles Laden wenn maximaler SOC erreicht (robust gegen Spikes)"
|
||||
trigger:
|
||||
- platform: numeric_state
|
||||
entity_id: sensor.esssoc
|
||||
above: 99
|
||||
for:
|
||||
seconds: 30 # Debounce: 30 Sekunden warten
|
||||
condition:
|
||||
# 1. Manual Control aktiv
|
||||
- condition: state
|
||||
entity_id: input_boolean.goodwe_manual_control
|
||||
state: "on"
|
||||
|
||||
# 2. Plausibilitäts-Check: SOC zwischen 99% und 105%
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.esssoc
|
||||
above: 99
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.esssoc
|
||||
below: 105
|
||||
|
||||
# 3. Manual Control muss mindestens 60 Sekunden aktiv sein
|
||||
- condition: template
|
||||
value_template: >
|
||||
{% set last_changed = as_timestamp(states.input_boolean.goodwe_manual_control.last_changed) %}
|
||||
{% set now = as_timestamp(now()) %}
|
||||
{% set duration = now - last_changed %}
|
||||
{{ duration > 60 }}
|
||||
|
||||
# 4. Sensor muss verfügbar sein
|
||||
- condition: template
|
||||
value_template: >
|
||||
{{ states('sensor.esssoc') not in ['unavailable', 'unknown', 'none'] }}
|
||||
|
||||
action:
|
||||
- service: input_boolean.turn_off
|
||||
target:
|
||||
entity_id: input_boolean.goodwe_manual_control
|
||||
|
||||
- service: notify.persistent_notification
|
||||
data:
|
||||
title: "Batterie-Optimierung"
|
||||
message: "Manuelles Laden beendet - SOC {{ states('sensor.esssoc') }}% erreicht"
|
||||
|
||||
# Logging für Debugging
|
||||
- service: system_log.write
|
||||
data:
|
||||
message: "Battery charging stopped at SOC {{ states('sensor.esssoc') }}%"
|
||||
level: info
|
||||
|
||||
mode: single
|
||||
```
|
||||
|
||||
**Schutz-Mechanismen:**
|
||||
1. ✅ **30 Sekunden Debounce**: Warte 30s nachdem SOC > 99%
|
||||
2. ✅ **Plausibilitäts-Check**: SOC muss < 105% sein
|
||||
3. ✅ **Initial Delay**: Manual Control muss mindestens 60s aktiv sein
|
||||
4. ✅ **Availability Check**: Sensor muss verfügbar sein
|
||||
|
||||
## Installation
|
||||
|
||||
**Option A: Via Home Assistant UI** (Empfohlen für schnellen Test)
|
||||
1. Gehe zu Einstellungen → Automationen & Szenen
|
||||
2. Suche "Batterie Optimierung: Stopp bei Max-SOC"
|
||||
3. Bearbeite die Automation
|
||||
4. Ersetze den Inhalt mit einer der obigen Lösungen
|
||||
5. Speichern
|
||||
|
||||
**Option B: Via YAML**
|
||||
1. Öffne deine `automations.yaml` oder die entsprechende Datei
|
||||
2. Finde die Automation (ID oder Alias)
|
||||
3. Ersetze sie mit der neuen Version
|
||||
4. Home Assistant neu laden oder Automationen neu laden
|
||||
|
||||
## Testing
|
||||
|
||||
### Test 1: Manuelles Laden ohne Spike-Problem
|
||||
```yaml
|
||||
# Developer Tools → Services
|
||||
service: input_boolean.turn_on
|
||||
target:
|
||||
entity_id: input_boolean.goodwe_manual_control
|
||||
```
|
||||
|
||||
Warte 2 Minuten und prüfe:
|
||||
- Bleibt Manual Control aktiv?
|
||||
- Gibt es SOC-Spikes in den Logs?
|
||||
|
||||
### Test 2: Simulation eines Spikes
|
||||
```yaml
|
||||
# Developer Tools → States
|
||||
# Suche sensor.esssoc und ändere temporär den Wert auf 65535
|
||||
# (nur möglich wenn Sensor-Typ es erlaubt)
|
||||
```
|
||||
|
||||
### Test 3: Echtes Stoppen bei 100%
|
||||
Warte bis Batterie wirklich bei 100% ist und prüfe ob das Laden dann korrekt gestoppt wird.
|
||||
|
||||
## Alternative: Sensor-Filter
|
||||
|
||||
Wenn das Problem häufiger auftritt, kannst du auch einen gefilterten Sensor erstellen:
|
||||
|
||||
```yaml
|
||||
# In configuration.yaml
|
||||
sensor:
|
||||
- platform: filter
|
||||
name: "ESS SOC Filtered"
|
||||
entity_id: sensor.esssoc
|
||||
filters:
|
||||
# Entferne ungültige Werte
|
||||
- filter: outlier
|
||||
window_size: 4
|
||||
radius: 10.0
|
||||
# Entferne extreme Spikes
|
||||
- filter: range
|
||||
lower_bound: 0
|
||||
upper_bound: 100
|
||||
# Glättung
|
||||
- filter: lowpass
|
||||
time_constant: 10
|
||||
```
|
||||
|
||||
Dann verwende `sensor.ess_soc_filtered` in allen Automationen statt `sensor.esssoc`.
|
||||
|
||||
## Monitoring
|
||||
|
||||
Füge eine Notification hinzu wenn ungültige Werte erkannt werden:
|
||||
|
||||
```yaml
|
||||
alias: "Debug: SOC Spike Detector"
|
||||
description: "Warnt bei ungültigen SOC-Werten"
|
||||
trigger:
|
||||
- platform: numeric_state
|
||||
entity_id: sensor.esssoc
|
||||
above: 105
|
||||
action:
|
||||
- service: notify.persistent_notification
|
||||
data:
|
||||
title: "SOC Spike erkannt!"
|
||||
message: "SOC zeigt {{ states('sensor.esssoc') }}% - wahrscheinlich ungültiger Wert"
|
||||
- service: system_log.write
|
||||
data:
|
||||
message: "SOC spike detected: {{ states('sensor.esssoc') }}% at {{ now() }}"
|
||||
level: warning
|
||||
mode: queued
|
||||
```
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
1. **JETZT**: Implementiere Lösung 4 (kombinierter Ansatz)
|
||||
2. **TESTE**: Aktiviere heute Abend manuelles Laden und beobachte
|
||||
3. **MONITOR**: Installiere die Spike Detector Automation
|
||||
4. **LANGFRISTIG**: Erwäge einen gefilterten Sensor für mehr Robustheit
|
||||
|
||||
## Zusätzliche Absicherungen
|
||||
|
||||
Füge auch in der `execute_charging_schedule` Funktion einen Check ein:
|
||||
|
||||
```python
|
||||
# In battery_charging_optimizer.py, Zeile 67
|
||||
current_soc = float(state.get('sensor.esssoc') or 50)
|
||||
|
||||
# Plausibilitäts-Check hinzufügen:
|
||||
if current_soc > 105 or current_soc < 0:
|
||||
log.warning(f"⚠ Ungültiger SOC-Wert erkannt: {current_soc}%. Verwende letzten gültigen Wert.")
|
||||
# Verwende einen Fallback-Wert oder den letzten gültigen Wert
|
||||
current_soc = 50 # Oder aus einem gespeicherten State laden
|
||||
```
|
||||
274
archive/FIX_SOC_SPIKE_REMOTE_MODE.md
Normal file
274
archive/FIX_SOC_SPIKE_REMOTE_MODE.md
Normal file
@@ -0,0 +1,274 @@
|
||||
# Fix: SOC Spike beim Umschalten auf REMOTE-Modus
|
||||
|
||||
## Problem
|
||||
|
||||
Beim Umschalten des ESS auf REMOTE-Modus meldet `sensor.esssoc` kurzzeitig ungültige Werte (z.B. 65535%). Dies führte zu folgenden Problemen:
|
||||
|
||||
1. **Automation 8** ("Stopp bei Max-SOC") triggert bei SOC > 99%
|
||||
2. Die Automation deaktiviert `goodwe_manual_control`
|
||||
3. Keep-Alive Automation stoppt
|
||||
4. Laden wird vorzeitig abgebrochen
|
||||
|
||||
## Ursache
|
||||
|
||||
Der OpenEMS ESS liefert während des Modus-Wechsels ungültige Register-Werte, die als extrem hohe SOC-Prozentwerte interpretiert werden (z.B. 65535% = 0xFFFF als unsigned integer).
|
||||
|
||||
## Lösung
|
||||
|
||||
### 1. Automation 8: SOC-Plausibilitäts-Check
|
||||
|
||||
**Datei**: `automations/battery_optimizer_automations.yaml`
|
||||
|
||||
**Änderung**: Zusätzliche Template-Condition hinzugefügt:
|
||||
|
||||
```yaml
|
||||
condition:
|
||||
- condition: state
|
||||
entity_id: input_boolean.goodwe_manual_control
|
||||
state: "on"
|
||||
# NEU: SOC-Plausibilitäts-Check
|
||||
- condition: template
|
||||
value_template: >
|
||||
{% set soc = states('sensor.esssoc') | float(0) %}
|
||||
{{ soc >= 99 and soc <= 101 }}
|
||||
```
|
||||
|
||||
**Effekt**: Die Automation triggert nur noch, wenn der SOC im plausiblen Bereich von 99-101% liegt. Werte wie 65535% werden ignoriert.
|
||||
|
||||
### 2. Keep-Alive Automation: Schutz vor ungültigen Werten
|
||||
|
||||
**Datei**: `automations/speicher_manuell_laden.yaml`
|
||||
|
||||
**Änderung**: Zusätzliche Template-Condition hinzugefügt:
|
||||
|
||||
```yaml
|
||||
conditions:
|
||||
- condition: state
|
||||
entity_id: input_boolean.goodwe_manual_control
|
||||
state: "on"
|
||||
# NEU: SOC-Plausibilitäts-Check
|
||||
- condition: template
|
||||
value_template: >
|
||||
{% set soc = states('sensor.esssoc') | float(50) %}
|
||||
{{ soc <= 100 }}
|
||||
```
|
||||
|
||||
**Effekt**: Die Keep-Alive Automation sendet nur Modbus-Befehle, wenn der SOC plausibel ist (<= 100%). Bei Spikes wird der Befehl übersprungen, aber die Automation läuft beim nächsten 30s-Zyklus weiter.
|
||||
|
||||
## Implementierung
|
||||
|
||||
### Schritt 1: Backups wurden erstellt
|
||||
|
||||
```bash
|
||||
# Automatisch erstellt:
|
||||
battery_optimizer_automations.yaml.backup
|
||||
speicher_manuell_laden.yaml.backup
|
||||
```
|
||||
|
||||
### Schritt 2: Automationen in Home Assistant aktualisieren
|
||||
|
||||
#### Option A: YAML-Modus (empfohlen)
|
||||
|
||||
Wenn du die Automations über YAML-Dateien verwaltest:
|
||||
|
||||
1. **Öffne Home Assistant**
|
||||
2. **Einstellungen** → **Automationen & Szenen**
|
||||
3. **Klicke auf die drei Punkte** (⋮) → **YAML bearbeiten**
|
||||
4. **Suche die beiden Automationen**:
|
||||
- "Batterie Optimierung: Stopp bei Max-SOC"
|
||||
- "Automation: Speicher manuell laden"
|
||||
5. **Ersetze den Code** mit dem Inhalt aus den aktualisierten Dateien
|
||||
6. **Speichern**
|
||||
7. **Developer Tools** → **YAML** → **Automations neu laden**
|
||||
|
||||
#### Option B: UI-Modus
|
||||
|
||||
Wenn du die Automations über die UI erstellt hast:
|
||||
|
||||
1. **Öffne "Batterie Optimierung: Stopp bei Max-SOC"**
|
||||
2. **Klicke auf "Bedingungen hinzufügen"** → **Vorlage**
|
||||
3. **Füge ein**:
|
||||
```yaml
|
||||
{% set soc = states('sensor.esssoc') | float(0) %}
|
||||
{{ soc >= 99 and soc <= 101 }}
|
||||
```
|
||||
4. **Speichern**
|
||||
|
||||
5. **Öffne "Automation: Speicher manuell laden"**
|
||||
6. **Klicke auf "Bedingungen hinzufügen"** → **Vorlage**
|
||||
7. **Füge ein**:
|
||||
```yaml
|
||||
{% set soc = states('sensor.esssoc') | float(50) %}
|
||||
{{ soc <= 100 }}
|
||||
```
|
||||
8. **Speichern**
|
||||
|
||||
### Schritt 3: Automationen neu laden
|
||||
|
||||
```yaml
|
||||
Developer Tools → YAML → Automations neu laden
|
||||
```
|
||||
|
||||
## Test-Anleitung
|
||||
|
||||
### Test 1: Manuelles Laden mit Spike-Simulation
|
||||
|
||||
1. **Setze manuelles Laden**:
|
||||
```
|
||||
input_boolean.goodwe_manual_control = on
|
||||
```
|
||||
|
||||
2. **Überwache in den Logs**:
|
||||
```
|
||||
Einstellungen → System → Logs
|
||||
Filter: "automation"
|
||||
```
|
||||
|
||||
3. **Prüfe**, dass Keep-Alive läuft:
|
||||
- Alle 30 Sekunden sollte "OpenEMS ESS Ziel: -5000.0 W" erscheinen
|
||||
|
||||
4. **Simuliere ungültigen SOC** (nur zum Testen):
|
||||
```yaml
|
||||
# Developer Tools → States
|
||||
# Ändere temporär sensor.esssoc auf 65535
|
||||
```
|
||||
|
||||
5. **Erwartetes Verhalten**:
|
||||
- Keep-Alive überspringt diesen Zyklus
|
||||
- Automation 8 triggert NICHT
|
||||
- `goodwe_manual_control` bleibt `on`
|
||||
- Beim nächsten Zyklus (wenn SOC wieder normal) läuft Keep-Alive weiter
|
||||
|
||||
### Test 2: Automatischer Ladeplan
|
||||
|
||||
1. **Aktiviere Optimizer**:
|
||||
```
|
||||
input_boolean.battery_optimizer_enabled = on
|
||||
```
|
||||
|
||||
2. **Trigger manuelle Berechnung**:
|
||||
```yaml
|
||||
Developer Tools → Services
|
||||
Service: pyscript.calculate_charging_schedule
|
||||
```
|
||||
|
||||
3. **Warte auf nächste Ladestunde** oder trigger manuell:
|
||||
```yaml
|
||||
Developer Tools → Services
|
||||
Service: pyscript.execute_charging_schedule
|
||||
```
|
||||
|
||||
4. **Überwache Umschaltung auf REMOTE**:
|
||||
- Logs sollten zeigen: "OpenEMS ESS Ziel: -5000.0 W"
|
||||
- Keine vorzeitigen Stops durch Automation 8
|
||||
|
||||
### Test 3: Echtes Laden bis 100%
|
||||
|
||||
1. **Starte Laden bei niedrigem SOC** (z.B. 20%)
|
||||
2. **Lass System bis 100% laden**
|
||||
3. **Erwartetes Verhalten**:
|
||||
- Bei ca. 99% echter SOC: Automation 8 triggert
|
||||
- `goodwe_manual_control` wird deaktiviert
|
||||
- ESS geht auf INTERNAL-Modus
|
||||
- Notification: "Manuelles Laden beendet - SOC 100% erreicht"
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Wichtige Log-Einträge
|
||||
|
||||
**Erfolgreicher Schutz vor Spike**:
|
||||
```
|
||||
Keep-Alive Automation: Condition failed (SOC check)
|
||||
```
|
||||
|
||||
**Normales Keep-Alive**:
|
||||
```
|
||||
OpenEMS ESS Ziel: -5000.0 W -> 706 -> [reg1, reg2]
|
||||
```
|
||||
|
||||
**Automation 8 triggert bei echten 100%**:
|
||||
```
|
||||
Batterie-Optimierung: Manuelles Laden beendet - SOC 100% erreicht
|
||||
```
|
||||
|
||||
### Developer Tools State Monitoring
|
||||
|
||||
```yaml
|
||||
# Überwache diese Entities:
|
||||
sensor.esssoc # Sollte 0-100% sein
|
||||
input_boolean.goodwe_manual_control # Sollte nicht flackern
|
||||
pyscript.battery_charging_schedule # Check Attribute "schedule"
|
||||
```
|
||||
|
||||
## Rollback (falls nötig)
|
||||
|
||||
Falls Probleme auftreten:
|
||||
|
||||
```bash
|
||||
# Im Terminal:
|
||||
cd /Users/felix/Nextcloud/AI/projects/homeassistant/openems/automations/
|
||||
|
||||
# Restore Backups:
|
||||
mv battery_optimizer_automations.yaml.backup battery_optimizer_automations.yaml
|
||||
mv speicher_manuell_laden.yaml.backup speicher_manuell_laden.yaml
|
||||
|
||||
# Dann in Home Assistant:
|
||||
# Developer Tools → YAML → Automations neu laden
|
||||
```
|
||||
|
||||
## Weitere Verbesserungen (optional)
|
||||
|
||||
### PyScript-Logging erweitern
|
||||
|
||||
In `battery_charging_optimizer.py`, Zeile 68-75:
|
||||
|
||||
```python
|
||||
# SOC-Plausibilitäts-Check (filtert ungültige Werte wie 65535%)
|
||||
if current_soc > 100 or current_soc < 0:
|
||||
log.warning(f"⚠ Ungültiger SOC-Wert erkannt: {current_soc}%. Verwende Fallback-Wert 50%.")
|
||||
current_soc = 50
|
||||
```
|
||||
|
||||
**Empfehlung**: Bereits vorhanden und funktioniert korrekt.
|
||||
|
||||
### ESS-Sensor Smoothing (langfristig)
|
||||
|
||||
Erwäge einen Template-Sensor mit Averaging:
|
||||
|
||||
```yaml
|
||||
# configuration.yaml
|
||||
template:
|
||||
- sensor:
|
||||
- name: "ESS SOC Smoothed"
|
||||
unit_of_measurement: "%"
|
||||
state: >
|
||||
{% set current = states('sensor.esssoc') | float(50) %}
|
||||
{% if current > 100 or current < 0 %}
|
||||
{{ states('sensor.esssoc_smoothed') | float(50) }}
|
||||
{% else %}
|
||||
{{ current }}
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
**Version**: 3.3.2 (Fix)
|
||||
**Datum**: 2024-11-30
|
||||
**Änderungen**:
|
||||
- ✅ Automation 8: SOC-Plausibilitäts-Check (99-101%)
|
||||
- ✅ Keep-Alive: SOC-Plausibilitäts-Check (<= 100%)
|
||||
- ✅ Backups erstellt
|
||||
- ✅ Dokumentation
|
||||
|
||||
**Behobenes Problem**:
|
||||
- Vorzeitiger Stop beim Umschalten auf REMOTE-Modus durch ungültige SOC-Werte
|
||||
|
||||
**Getestet**: Wartet auf User-Test
|
||||
|
||||
## Support
|
||||
|
||||
Bei Problemen:
|
||||
1. Prüfe Home Assistant Logs
|
||||
2. Prüfe `sensor.esssoc` Wert in Developer Tools → States
|
||||
3. Prüfe Automation-Trace: Einstellungen → Automationen → (Automation auswählen) → Trace
|
||||
4. Vergleiche mit Backup-Dateien
|
||||
458
archive/SOC_CALIBRATION_GUIDE.md
Normal file
458
archive/SOC_CALIBRATION_GUIDE.md
Normal file
@@ -0,0 +1,458 @@
|
||||
# SOC-Kalibrierung & Drift-Management
|
||||
|
||||
## Problem: SOC ist berechnet, nicht gemessen
|
||||
|
||||
### Wie SOC funktioniert
|
||||
|
||||
Der State of Charge (SOC) wird vom Battery Management System (BMS) **berechnet**, nicht direkt gemessen:
|
||||
|
||||
**Berechnungsmethoden:**
|
||||
1. **Coulomb Counting** (Hauptmethode)
|
||||
- Integriert Strom über Zeit: ∫ I(t) dt
|
||||
- Geladen mit 5A für 1h = +5Ah
|
||||
- Entladen mit 3A für 1h = -3Ah
|
||||
- Netto: +2Ah → SOC steigt um ~2%
|
||||
|
||||
2. **Spannungsbasiert** (Open Circuit Voltage)
|
||||
- Zellspannung korreliert mit SOC
|
||||
- 4.2V/Zelle ≈ 100% SOC
|
||||
- 3.2V/Zelle ≈ 0% SOC (Minimum)
|
||||
|
||||
3. **Kalman-Filter / State Estimation**
|
||||
- Kombiniert beide Methoden
|
||||
- Berücksichtigt Temperatur, Alterung, Laderate
|
||||
|
||||
### Warum SOC abweicht (Drift)
|
||||
|
||||
**Hauptursachen:**
|
||||
|
||||
| Ursache | Effekt | Beispiel |
|
||||
|---------|--------|----------|
|
||||
| **Keine vollen Zyklen** | BMS verliert Referenzpunkte | Immer 30-92%, nie 20-100% |
|
||||
| **Coulomb Counting Fehler** | Akkumuliert über Zeit | 0.1% Fehler/Tag = 3% nach 1 Monat |
|
||||
| **Temperatur** | Kapazität ändert sich | 10kWh bei 20°C, 9.5kWh bei 5°C |
|
||||
| **Alterung** | Tatsächliche Kapazität sinkt | 10kWh neu, 9.2kWh nach 3 Jahren |
|
||||
| **Strommessung** | Sensorfehler (±1-2%) | 5.0A gemessen, 5.1A real |
|
||||
| **Selbstentladung** | Nicht im SOC berücksichtigt | 1-2%/Monat |
|
||||
|
||||
**Beispiel SOC-Drift:**
|
||||
```
|
||||
Tag 1: BMS kalibriert, SOC = 100% (korrekt)
|
||||
Tag 5: SOC = 95% (BMS sagt), real = 96% (+1% Drift)
|
||||
Tag 10: SOC = 88% (BMS sagt), real = 91% (+3% Drift)
|
||||
Tag 30: SOC = 65% (BMS sagt), real = 72% (+7% Drift)
|
||||
→ Nach Vollladung: Kalibriert sich neu
|
||||
```
|
||||
|
||||
## GoodWe-spezifische Einstellungen
|
||||
|
||||
### Hardware-seitiges 20% Minimum
|
||||
|
||||
Du hast im GoodWe eingestellt:
|
||||
- **Minimum SOC: 20%**
|
||||
- **Entladung stoppt bei 20%**
|
||||
- **Freigabe nur bei Netzausfall**
|
||||
|
||||
**Bedeutung:**
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 100% ← Hardware-Maximum │
|
||||
│ ↕ │
|
||||
│ 80% ← Nutzbare Kapazität │
|
||||
│ ↕ │
|
||||
│ 20% ← Hardware-Minimum │ ← GoodWe stoppt hier!
|
||||
│ ↓ │
|
||||
│ 0% ← Nur bei Netzausfall │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Effektive Kapazität:**
|
||||
- Physisch: 10 kWh (100%)
|
||||
- Nutzbar: 8 kWh (80% von 20-100%)
|
||||
- Reserve für Netzausfall: 2 kWh (20%)
|
||||
|
||||
### Integration mit unserem Optimizer
|
||||
|
||||
**Unser Optimizer:**
|
||||
- `min_soc: 20%` (redundant, aber konsistent)
|
||||
- `max_soc: 100%`
|
||||
- `reserve_capacity: 2 kWh` (entspricht 20%)
|
||||
|
||||
**Das passt perfekt zusammen!** ✅
|
||||
|
||||
## Auswirkungen auf die Optimierung
|
||||
|
||||
### Problem 1: Ungenaue Planung
|
||||
|
||||
**Szenario:**
|
||||
```
|
||||
14:05 Uhr (Planung):
|
||||
BMS zeigt: SOC 30%
|
||||
Real: SOC 35% (5% Drift)
|
||||
|
||||
Optimizer plant: (100 - 30) × 10kWh - 2kWh = 6.8 kWh
|
||||
Tatsächlich braucht: (100 - 35) × 10kWh - 2kWh = 6.3 kWh
|
||||
|
||||
→ Plant 500Wh zu viel
|
||||
→ Nutzt eventuell 1 Stunde mehr als nötig
|
||||
→ Könnte teurere Stunde nutzen
|
||||
```
|
||||
|
||||
### Problem 2: Keine Kalibrierung durch Teilzyklen
|
||||
|
||||
**Typischer Tagesablauf (OHNE Kalibrierung):**
|
||||
```
|
||||
07:00 → SOC 92% (nicht 100% erreicht)
|
||||
↓ PV-Produktion überschuss lädt minimal
|
||||
09:00 → SOC 95% (immer noch nicht 100%)
|
||||
↓ PV reicht für Eigenverbrauch
|
||||
12:00 → SOC 93% (leicht entladen)
|
||||
↓ Eigenverbrauch übersteigt PV
|
||||
18:00 → SOC 65% (Eigenverbrauch ohne PV)
|
||||
↓ Weiterer Eigenverbrauch
|
||||
22:00 → SOC 45% (vor Laden)
|
||||
↓ Günstige Stunden: Laden
|
||||
23:00 → SOC 60% (teilweise geladen)
|
||||
00:00 → SOC 75% (weiter geladen)
|
||||
↓ Auto-Modus
|
||||
07:00 → SOC 92% (nicht 100%)
|
||||
|
||||
→ Kein voller Zyklus!
|
||||
→ BMS hat keine Referenzpunkte
|
||||
→ SOC driftet weiter
|
||||
```
|
||||
|
||||
## Lösungen
|
||||
|
||||
### Lösung 1: Höherer Sicherheitspuffer (Sofort umsetzbar)
|
||||
|
||||
**Empfehlung: 25-30% statt 20%**
|
||||
|
||||
```yaml
|
||||
input_number:
|
||||
battery_optimizer_safety_buffer:
|
||||
initial: 30 # Statt 20
|
||||
```
|
||||
|
||||
**Begründung:**
|
||||
- SOC kann um 5-10% abweichen
|
||||
- Puffer kompensiert Drift
|
||||
- Hardware stoppt sowieso bei 100%
|
||||
- Lieber eine Stunde mehr laden als 8% zu wenig
|
||||
|
||||
**Berechnung:**
|
||||
```
|
||||
Ohne Drift:
|
||||
6.8 kWh × 1.20 = 8.16 kWh (20% Puffer)
|
||||
|
||||
Mit 5% Drift:
|
||||
6.8 kWh × 1.30 = 8.84 kWh (30% Puffer)
|
||||
→ Kompensiert Drift + normale Schwankungen
|
||||
```
|
||||
|
||||
### Lösung 2: Monatliche Kalibrierung (Empfohlen)
|
||||
|
||||
**Automatischer Kalibrierungs-Zyklus:**
|
||||
|
||||
**Phase 1: Entladung** (natürlich durch Eigenverbrauch)
|
||||
- Ziel: SOC auf ~20% bringen
|
||||
- Dauer: Normalerweise 1-2 Tage
|
||||
- Optimizer pausiert (Manual Override)
|
||||
|
||||
**Phase 2: Vollladung** (erzwungen, unabhängig von Preisen)
|
||||
- Ziel: SOC auf 100% laden
|
||||
- Dauer: 2-5 Stunden (je nach Start-SOC)
|
||||
- Volle Ladeleistung (5kW)
|
||||
|
||||
**Phase 3: Kalibrierung** (automatisch durch BMS)
|
||||
- BMS erkennt: Zellen bei 4.2V = 100% SOC
|
||||
- Vergleicht mit berechnetem SOC
|
||||
- Korrigiert Coulomb Counter
|
||||
- Passt Kapazitäts-Schätzung an
|
||||
|
||||
**Ergebnis:**
|
||||
- ✅ SOC wieder präzise
|
||||
- ✅ BMS hat Referenzpunkte
|
||||
- ✅ Tatsächliche Kapazität bekannt
|
||||
- ✅ Drift zurückgesetzt
|
||||
|
||||
**Häufigkeit:**
|
||||
- **Minimum**: Alle 3 Monate
|
||||
- **Empfohlen**: Jeden Monat
|
||||
- **Bei Bedarf**: Wenn SOC merklich abweicht
|
||||
|
||||
**Installation:** Siehe `battery_calibration_automation.yaml`
|
||||
|
||||
### Lösung 3: SOC-Monitoring (Automatisch)
|
||||
|
||||
**Erkenne SOC-Drift automatisch:**
|
||||
|
||||
```yaml
|
||||
# Automation: SOC-Drift-Detektor
|
||||
alias: "Batterie: SOC-Drift Warnung"
|
||||
description: "Warnt wenn SOC wahrscheinlich driftet"
|
||||
trigger:
|
||||
# Wenn Batterie "voll" ist aber nicht 100%
|
||||
- platform: state
|
||||
entity_id: input_boolean.goodwe_manual_control
|
||||
from: "on"
|
||||
to: "off"
|
||||
for:
|
||||
minutes: 30
|
||||
condition:
|
||||
# SOC ist unter 98%
|
||||
- condition: numeric_state
|
||||
entity_id: sensor.esssoc
|
||||
below: 98
|
||||
|
||||
# Aber Laden lief mehr als 2 Stunden
|
||||
- condition: template
|
||||
value_template: >
|
||||
{% set duration = as_timestamp(now()) - as_timestamp(trigger.from_state.last_changed) %}
|
||||
{{ duration > 7200 }}
|
||||
action:
|
||||
- service: notify.persistent_notification
|
||||
data:
|
||||
title: "SOC-Drift erkannt?"
|
||||
message: >
|
||||
Laden lief {{ ((as_timestamp(now()) - as_timestamp(trigger.from_state.last_changed)) / 3600) | round(1) }}h,
|
||||
aber SOC ist nur {{ states('sensor.esssoc') }}%.
|
||||
Eventuell SOC-Drift? Kalibrierung empfohlen.
|
||||
```
|
||||
|
||||
## Installation der Kalibrierung
|
||||
|
||||
### Schritt 1: Input Helper erstellen
|
||||
|
||||
```yaml
|
||||
# In configuration.yaml
|
||||
input_boolean:
|
||||
battery_calibration_active:
|
||||
name: "Batterie Kalibrierung aktiv"
|
||||
icon: mdi:battery-sync
|
||||
initial: off
|
||||
```
|
||||
|
||||
**Via UI:**
|
||||
1. Einstellungen → Geräte & Dienste → Helfer
|
||||
2. "Helfer hinzufügen" → "Toggle/Schalter"
|
||||
3. Name: `Batterie Kalibrierung aktiv`
|
||||
4. Entity ID: `input_boolean.battery_calibration_active`
|
||||
5. Icon: `mdi:battery-sync`
|
||||
|
||||
### Schritt 2: Automations installieren
|
||||
|
||||
**Kopiere die 4 Automations aus** `battery_calibration_automation.yaml`:
|
||||
1. Kalibrierung starten (jeden 1. des Monats)
|
||||
2. Kalibrierungs-Laden (stündlich während aktiv)
|
||||
3. Kalibrierung beenden (nach 24h oder bei 100%)
|
||||
4. Notfall-Abbruch (bei kritisch niedrigem SOC)
|
||||
|
||||
**Via UI oder YAML:**
|
||||
- UI: Einstellungen → Automationen & Szenen → Neue Automation → YAML-Modus
|
||||
- YAML: In `automations.yaml` einfügen
|
||||
|
||||
### Schritt 3: Testen
|
||||
|
||||
**Manuelle Kalibrierung triggern:**
|
||||
|
||||
```yaml
|
||||
# Developer Tools → Services
|
||||
service: input_boolean.turn_on
|
||||
target:
|
||||
entity_id: input_boolean.battery_calibration_active
|
||||
```
|
||||
|
||||
**Beobachte:**
|
||||
1. Manual Override wird aktiviert
|
||||
2. Batterie lädt auf 100%
|
||||
3. Nach 24h oder bei 100%: Automatische Deaktivierung
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Wann Kalibrierung durchführen?
|
||||
|
||||
**Automatisch:**
|
||||
- ✅ Jeden Monat (1. des Monats)
|
||||
- ✅ Nach Software-Updates
|
||||
- ✅ Nach längeren Ausfällen
|
||||
|
||||
**Manuell:**
|
||||
- ⚠️ Wenn SOC merklich abweicht
|
||||
- ⚠️ Wenn Batterie nie 100% erreicht
|
||||
- ⚠️ Wenn Kapazität sich verändert anfühlt
|
||||
|
||||
**Nicht nötig:**
|
||||
- ❌ Wöchentlich (zu häufig)
|
||||
- ❌ Wenn SOC präzise ist
|
||||
- ❌ Bei normalen Teilzyklen
|
||||
|
||||
### Optimale Kalibrierungs-Bedingungen
|
||||
|
||||
| Faktor | Optimal | Warum |
|
||||
|--------|---------|-------|
|
||||
| **Temperatur** | 15-25°C | Beste Messgenauigkeit |
|
||||
| **Laderate** | 0.5C (5kW bei 10kWh) | Minimiert Fehler |
|
||||
| **Entladerate** | Natürlich (Eigenverbrauch) | Realistisch |
|
||||
| **Dauer** | Mindestens 6h | BMS braucht Zeit |
|
||||
|
||||
### Was nach Kalibrierung zu erwarten ist
|
||||
|
||||
**Sofort:**
|
||||
- ✅ SOC springt eventuell (z.B. 92% → 97%)
|
||||
- ✅ BMS hat neue Referenzpunkte
|
||||
- ✅ Kapazitäts-Schätzung aktualisiert
|
||||
|
||||
**In den nächsten Tagen:**
|
||||
- ✅ Präziserer SOC
|
||||
- ✅ Bessere Ladeplanung
|
||||
- ✅ Weniger "überraschende" SOC-Werte
|
||||
|
||||
**Langfristig:**
|
||||
- ✅ Verlangsamter Drift
|
||||
- ✅ Längere Batterielebensdauer
|
||||
- ✅ Genauere Kapazitäts-Prognosen
|
||||
|
||||
## Erweiterte Lösungen
|
||||
|
||||
### Adaptive Pufferberechnung
|
||||
|
||||
**Konzept:** Puffer basierend auf historischer Drift anpassen
|
||||
|
||||
```python
|
||||
# Pseudo-Code für zukünftige Version
|
||||
historical_drift = learn_from_last_30_days()
|
||||
# Beispiel: SOC war durchschnittlich 5% höher als geplant
|
||||
|
||||
adaptive_buffer = base_buffer + historical_drift
|
||||
# 20% + 5% = 25%
|
||||
|
||||
# Plane mit adaptivem Puffer
|
||||
capacity_with_buffer = capacity × (1 + adaptive_buffer)
|
||||
```
|
||||
|
||||
### SOC-Validierung über Spannung
|
||||
|
||||
**Konzept:** Vergleiche BMS-SOC mit Zellspannung
|
||||
|
||||
```yaml
|
||||
# Sensor für SOC-Validierung
|
||||
sensor:
|
||||
- platform: template
|
||||
sensors:
|
||||
battery_soc_validated:
|
||||
friendly_name: "SOC (validiert)"
|
||||
unit_of_measurement: "%"
|
||||
value_template: >
|
||||
{% set soc = states('sensor.esssoc') | float %}
|
||||
{% set voltage = states('sensor.battery_voltage') | float %}
|
||||
|
||||
{# Validiere SOC gegen Spannung #}
|
||||
{% if voltage > 54.0 and soc < 95 %}
|
||||
{{ 'SOC zu niedrig (Voltage hoch)' }}
|
||||
{% elif voltage < 50.0 and soc > 30 %}
|
||||
{{ 'SOC zu hoch (Voltage niedrig)' }}
|
||||
{% else %}
|
||||
{{ soc }}
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### Batterie-Gesundheits-Tracking
|
||||
|
||||
**Konzept:** Überwache tatsächliche Kapazität über Zeit
|
||||
|
||||
```yaml
|
||||
# Berechne echte Kapazität aus Vollzyklus
|
||||
sensor:
|
||||
- platform: template
|
||||
sensors:
|
||||
battery_true_capacity:
|
||||
friendly_name: "Wahre Batterie-Kapazität"
|
||||
unit_of_measurement: "kWh"
|
||||
value_template: >
|
||||
{% if is_state('input_boolean.battery_calibration_active', 'on') %}
|
||||
{# Nach Vollzyklus: Berechne Energie geladen #}
|
||||
{% set energy = states('sensor.battery_charged_energy') | float %}
|
||||
{% set soc_diff = 80 %} {# 20% → 100% #}
|
||||
{{ (energy / (soc_diff / 100)) | round(2) }}
|
||||
{% else %}
|
||||
{{ states('input_number.battery_capacity_kwh') }}
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
### Empfohlene Maßnahmen
|
||||
|
||||
| Priorität | Maßnahme | Aufwand | Nutzen |
|
||||
|-----------|----------|---------|--------|
|
||||
| 🔴 **HOCH** | Puffer auf 30% erhöhen | 1 min | Sofort bessere Ergebnisse |
|
||||
| 🟡 **MITTEL** | Monatliche Kalibrierung | 30 min | Langfristig präziser SOC |
|
||||
| 🟢 **NIEDRIG** | SOC-Monitoring | 15 min | Frühwarnung bei Drift |
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] Sicherheitspuffer auf 30% erhöhen
|
||||
- [ ] Input Helper für Kalibrierung erstellen
|
||||
- [ ] 4 Kalibrierungs-Automations installieren
|
||||
- [ ] Erste manuelle Kalibrierung durchführen
|
||||
- [ ] SOC-Monitoring Automation installieren
|
||||
- [ ] Nach 1 Monat: Überprüfen ob Batterie regelmäßig 100% erreicht
|
||||
|
||||
## Technische Details
|
||||
|
||||
### GoodWe BMS-Spezifikationen
|
||||
|
||||
**SOC-Berechnung:**
|
||||
- Methode: Coulomb Counting + Voltage Estimation
|
||||
- Update-Rate: 1 Hz (jede Sekunde)
|
||||
- Genauigkeit: ±3% (typisch), ±5% (maximum)
|
||||
- Kalibrierungs-Intervall: Empfohlen alle 30 Tage
|
||||
|
||||
**Referenzpunkte:**
|
||||
- 100% SOC: 54.4V (LiFePO4, 16S × 3.4V)
|
||||
- 20% SOC: 51.2V (LiFePO4, 16S × 3.2V)
|
||||
- Floating Voltage: 54.0V
|
||||
|
||||
**Kapazitäts-Learning:**
|
||||
- Algorithmus: Adaptive Weighted Integration
|
||||
- Lernrate: 0.1-0.5 (abhängig von Confidence)
|
||||
- Konvergenz: 3-5 Vollzyklen
|
||||
|
||||
### Home Assistant Integration
|
||||
|
||||
**Wichtige Entities:**
|
||||
- `sensor.esssoc`: BMS-berechneter SOC
|
||||
- `sensor.battery_voltage`: Gesamt-Spannung
|
||||
- `sensor.battery_current`: Lade-/Entladestrom
|
||||
- `sensor.battery_power`: Leistung (W)
|
||||
- `sensor.battery_temperature`: Temperatur
|
||||
|
||||
**Berechnete Sensoren:**
|
||||
- SOC-Validierung
|
||||
- Wahre Kapazität
|
||||
- Drift-Erkennung
|
||||
- Health-Score
|
||||
|
||||
## Referenzen
|
||||
|
||||
### Weiterführende Informationen
|
||||
|
||||
- GoodWe BMS Manual: SOC-Algorithmus Details
|
||||
- Battery University: SOC Estimation Techniques
|
||||
- OpenEMS Documentation: Battery Management
|
||||
- Home Assistant: Template Sensors & Automations
|
||||
|
||||
### Support & Community
|
||||
|
||||
- Home Assistant Community Forum
|
||||
- GoodWe Support
|
||||
- OpenEMS Community
|
||||
- Battery Management System Best Practices
|
||||
|
||||
---
|
||||
|
||||
**Version**: 1.0
|
||||
**Datum**: 2025-11-25
|
||||
**Autor**: Felix + Claude
|
||||
**Status**: Produktions-bereit
|
||||
Reference in New Issue
Block a user