HOTFIX v3.5.1: Preisschwelle wird jetzt korrekt angewendet (KRITISCH)
## Problem
Die price_threshold wurde geladen aber NIE verwendet!
- System lud auch bei Preisen ÜBER der Schwelle
- Beispiel: Schwelle 25ct, aber Ladung bei 25.93ct
- User-Erwartung komplett ignoriert
## Root Cause
```python
# Zeile 110: Geladen ✓
'price_threshold': float(state.get(...) or 25)
# Zeile 317-340: Aber nie verwendet! ✗
for p in future_price_data:
charging_candidates.append({...}) # Keine threshold-Prüfung!
```
## Fix (v3.5.1)
### 1. Filter VOR Ranking
- Filtere alle Stunden in affordable_hours (≤ threshold)
- Ignoriere teure Stunden komplett
- Wenn keine bezahlbaren Stunden: Keine Ladung (Auto-Modus)
### 2. Besseres Logging
```
💶 Preisschwelle: 25.0 ct/kWh
- Stunden unter Schwelle: 18
- Stunden über Schwelle: 12 (werden ignoriert)
```
### 3. Warnung bei Teilladung
Wenn nicht genug günstige Stunden für volle Ladung
## Verhalten
**VORHER (v3.5.0):**
- Alle Preise 25-30ct, Schwelle 25ct
- → Lädt bei 25.93ct ✗
**NACHHER (v3.5.1):**
- Alle Preise 25-30ct, Schwelle 25ct
- → Keine Ladung, Auto-Modus ✓
## Impact
Severity: 🔴 CRITICAL
- Ungewollte Ladevorgänge bei zu teuren Preisen
- Kosteneinsparungen nicht realisiert
- SOFORT updaten empfohlen!
## Dateien
- battery_charging_optimizer.py: Filter + Logging
- CHANGELOG.md: v3.5.1 Eintrag
- HOTFIX_PRICE_THRESHOLD_v3.5.1.md: Detaillierte Analyse
Danke an Felix für sofortiges Bug-Melden! 🙏
---
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
17
CHANGELOG.md
17
CHANGELOG.md
@@ -5,6 +5,23 @@ Alle wichtigen Änderungen an diesem Projekt werden in dieser Datei dokumentiert
|
|||||||
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.0.0/),
|
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.0.0/),
|
||||||
und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
||||||
|
|
||||||
|
## [3.5.1] - 2025-12-28
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **KRITISCHER BUG**: Preisschwelle wurde nicht angewendet
|
||||||
|
- `price_threshold` wurde geladen aber nie verwendet
|
||||||
|
- System lud auch bei Preisen über der Schwelle (z.B. 25.93ct bei Schwelle 25ct)
|
||||||
|
- Jetzt: Nur Stunden ≤ Preisschwelle werden für Ranking berücksichtigt
|
||||||
|
- Wenn alle Preise über Schwelle: Keine Ladung, bleibe im Auto-Modus
|
||||||
|
- **Besseres Logging**: Zeigt gefilterte Stunden an
|
||||||
|
- "X Stunden unter Schwelle, Y Stunden über Schwelle (werden ignoriert)"
|
||||||
|
- Schedule-Reason zeigt "Zu teuer: X.XXct (Schwelle: Yct)"
|
||||||
|
- **Warnung bei Teilladung**: Log-Warnung wenn nicht genug günstige Stunden verfügbar
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Ranking-Logik: Filtert zuerst nach Preisschwelle, dann Ranking der verbleibenden Stunden
|
||||||
|
- Schedule-Reasons verbessert für besseres Debugging
|
||||||
|
|
||||||
## [3.5.0] - 2025-12-28
|
## [3.5.0] - 2025-12-28
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|||||||
205
HOTFIX_PRICE_THRESHOLD_v3.5.1.md
Normal file
205
HOTFIX_PRICE_THRESHOLD_v3.5.1.md
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
# Hotfix: Preisschwelle wurde nicht angewendet (v3.5.1)
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Die Preisschwelle (`price_threshold`) wurde zwar aus den Input Helpern geladen, aber **nie im Code verwendet**. Das führte dazu, dass:
|
||||||
|
|
||||||
|
- System lud auch bei Preisen ÜBER der Schwelle
|
||||||
|
- Beispiel: Schwelle 25ct, aber Ladung bei 25.93ct geplant
|
||||||
|
- User-Erwartung: Keine Ladung wenn alle Preise über Schwelle
|
||||||
|
|
||||||
|
## Analyse
|
||||||
|
|
||||||
|
```python
|
||||||
|
# battery_charging_optimizer.py:110
|
||||||
|
'price_threshold': float(state.get('input_number.battery_optimizer_price_threshold') or 25),
|
||||||
|
# ✅ Wurde geladen
|
||||||
|
|
||||||
|
# battery_charging_optimizer.py:317-340 (ALT)
|
||||||
|
for p in future_price_data:
|
||||||
|
charging_candidates.append({...}) # ❌ Keine Prüfung gegen threshold!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resultat**: Alle zukünftigen Stunden wurden in die Ranking-Liste aufgenommen, unabhängig vom Preis.
|
||||||
|
|
||||||
|
## Lösung (v3.5.1)
|
||||||
|
|
||||||
|
### 1. Filter VOR dem Ranking
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Zeile 315-334: Neuer Filter
|
||||||
|
price_threshold = config['price_threshold']
|
||||||
|
affordable_hours = []
|
||||||
|
expensive_hours = []
|
||||||
|
|
||||||
|
for p in future_price_data:
|
||||||
|
if p['price'] <= price_threshold:
|
||||||
|
affordable_hours.append(p)
|
||||||
|
else:
|
||||||
|
expensive_hours.append(p)
|
||||||
|
|
||||||
|
# Wenn keine bezahlbaren Stunden verfügbar
|
||||||
|
if not affordable_hours:
|
||||||
|
log.warning(f"⚠️ Keine Stunden unter Preisschwelle {price_threshold} ct/kWh")
|
||||||
|
return create_auto_only_schedule(future_price_data) # Keine Ladung!
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Ranking nur mit bezahlbaren Stunden
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Zeile 341: Nur affordable_hours verwenden
|
||||||
|
for p in affordable_hours: # Statt future_price_data
|
||||||
|
charging_candidates.append({...})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Besseres Logging
|
||||||
|
|
||||||
|
```python
|
||||||
|
log.info(f"💶 Preisschwelle: {price_threshold} ct/kWh")
|
||||||
|
log.info(f" - Stunden unter Schwelle: {len(affordable_hours)}")
|
||||||
|
log.info(f" - Stunden über Schwelle: {len(expensive_hours)} (werden ignoriert)")
|
||||||
|
|
||||||
|
# Im Schedule:
|
||||||
|
reason = f"Zu teuer: {p['price']:.2f}ct (Schwelle: {price_threshold}ct)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Warnung bei Teilladung
|
||||||
|
|
||||||
|
```python
|
||||||
|
if actual_hours_needed < needed_hours:
|
||||||
|
log.warning(f"⚠️ Nur {actual_hours_needed} von {needed_hours} benötigten Stunden unter Preisschwelle")
|
||||||
|
log.warning(f" Batterie wird nur teilweise geladen")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verhalten vorher vs. nachher
|
||||||
|
|
||||||
|
### Szenario: Schwelle 25ct, alle Preise 25-30ct
|
||||||
|
|
||||||
|
**VORHER (v3.5.0)**:
|
||||||
|
```
|
||||||
|
Preis-Array: [25.93, 26.45, 27.12, 28.50, ...]
|
||||||
|
→ Ranking: Sortiere alle nach Preis
|
||||||
|
→ Wähle günstigste: 25.93ct (Rang 1)
|
||||||
|
→ ✗ LÄDT bei 25.93ct (über Schwelle 25ct!)
|
||||||
|
```
|
||||||
|
|
||||||
|
**NACHHER (v3.5.1)**:
|
||||||
|
```
|
||||||
|
Preis-Array: [25.93, 26.45, 27.12, 28.50, ...]
|
||||||
|
→ Filter: Alle über 25ct → affordable_hours = []
|
||||||
|
→ ⚠️ Keine Stunden unter Preisschwelle 25ct
|
||||||
|
→ ✓ KEINE LADUNG (Auto-Modus)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Szenario: Schwelle 25ct, Preise 20-30ct
|
||||||
|
|
||||||
|
**VORHER (v3.5.0)**:
|
||||||
|
```
|
||||||
|
Preis-Array: [20.50, 24.80, 25.93, 26.45, ...]
|
||||||
|
→ Ranking: [20.50, 24.80, 25.93, 26.45, ...]
|
||||||
|
→ Wähle Top 3: [20.50, 24.80, 25.93]
|
||||||
|
→ ✗ LÄDT auch bei 25.93ct (über Schwelle!)
|
||||||
|
```
|
||||||
|
|
||||||
|
**NACHHER (v3.5.1)**:
|
||||||
|
```
|
||||||
|
Preis-Array: [20.50, 24.80, 25.93, 26.45, ...]
|
||||||
|
→ Filter: affordable_hours = [20.50, 24.80]
|
||||||
|
→ Ranking: [20.50, 24.80]
|
||||||
|
→ Wähle Top 2: [20.50, 24.80]
|
||||||
|
→ ✓ NUR unter Schwelle, 25.93ct ignoriert
|
||||||
|
→ Warnung: "Nur 2 von 3 benötigten Stunden"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Log-Ausgaben neu (v3.5.1)
|
||||||
|
|
||||||
|
```
|
||||||
|
=== Batterie-Optimierung gestartet (v3.5.1 - Preisschwelle aktiv) ===
|
||||||
|
Preise: Min=20.50, Max=30.45, Avg=25.67 ct/kWh
|
||||||
|
Verfügbare Ladekapazität: 5.00 kWh (bis 100% SOC)
|
||||||
|
🎯 Benötigte Ladestunden: 1 (bei 8000W pro Stunde)
|
||||||
|
💶 Preisschwelle: 25.0 ct/kWh
|
||||||
|
- Stunden unter Schwelle: 18
|
||||||
|
- Stunden über Schwelle: 12 (werden ignoriert)
|
||||||
|
✓ Top 1 günstigste Stunden ausgewählt:
|
||||||
|
- Preise: 20.50 - 20.50 ct/kWh
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder wenn KEINE günstigen Stunden:
|
||||||
|
|
||||||
|
```
|
||||||
|
💶 Preisschwelle: 25.0 ct/kWh
|
||||||
|
- Stunden unter Schwelle: 0
|
||||||
|
- Stunden über Schwelle: 30 (werden ignoriert)
|
||||||
|
⚠️ Keine Stunden unter Preisschwelle 25.0 ct/kWh gefunden!
|
||||||
|
Günstigster Preis: 25.93 ct/kWh
|
||||||
|
→ Keine Ladung, bleibe im Auto-Modus
|
||||||
|
```
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
**Severity**: 🔴 **CRITICAL**
|
||||||
|
- Führte zu ungewollten Ladevorgängen bei zu teuren Preisen
|
||||||
|
- Kosteneinsparungen wurden nicht realisiert
|
||||||
|
- User-Erwartung komplett ignoriert
|
||||||
|
|
||||||
|
**Betroffene Versionen**:
|
||||||
|
- v3.5.0 (heute released)
|
||||||
|
- Wahrscheinlich auch v3.0.0 - v3.4.0 (zu prüfen)
|
||||||
|
|
||||||
|
**Fix-Priorität**: SOFORT
|
||||||
|
- Hotfix v3.5.1 released
|
||||||
|
- User sollten SOFORT updaten
|
||||||
|
|
||||||
|
## Migration 3.5.0 → 3.5.1
|
||||||
|
|
||||||
|
1. Update `battery_charging_optimizer.py` (v3.5.1)
|
||||||
|
2. PyScript neu laden
|
||||||
|
3. Neuberechnung: `pyscript.calculate_charging_schedule`
|
||||||
|
4. Log prüfen: Sollte "Preisschwelle: X ct/kWh" zeigen
|
||||||
|
|
||||||
|
**Keine Breaking Changes** - Nur Bugfix.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Test-Szenarien:
|
||||||
|
|
||||||
|
1. **Alle Preise über Schwelle**:
|
||||||
|
- Setze `price_threshold` auf 20ct
|
||||||
|
- Wenn alle Preise > 20ct → Keine Ladung
|
||||||
|
- Log: "Keine Stunden unter Preisschwelle"
|
||||||
|
|
||||||
|
2. **Mix aus günstigen/teuren Stunden**:
|
||||||
|
- Setze `price_threshold` auf 25ct
|
||||||
|
- Nur Stunden ≤ 25ct sollten im Plan sein
|
||||||
|
- Log: "X Stunden unter Schwelle, Y über Schwelle"
|
||||||
|
|
||||||
|
3. **Alle Preise unter Schwelle**:
|
||||||
|
- Setze `price_threshold` auf 50ct
|
||||||
|
- Normales Verhalten wie bisher
|
||||||
|
- Log: "30 Stunden unter Schwelle, 0 über Schwelle"
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
1. **Configuration muss verwendet werden**:
|
||||||
|
- Laden von Config ≠ Verwenden von Config
|
||||||
|
- Code-Review: Prüfe ob alle Config-Werte auch benutzt werden
|
||||||
|
|
||||||
|
2. **User-Feedback ernst nehmen**:
|
||||||
|
- User hat Bug sofort entdeckt beim ersten Test
|
||||||
|
- Ohne User-Test wäre Bug unentdeckt geblieben
|
||||||
|
|
||||||
|
3. **Logging ist essentiell**:
|
||||||
|
- Mit neuem Logging ist sofort ersichtlich was passiert
|
||||||
|
- "X Stunden unter Schwelle" macht Verhalten transparent
|
||||||
|
|
||||||
|
## Danke
|
||||||
|
|
||||||
|
Großes Dankeschön an Felix für das sofortige Melden des Bugs! 🙏
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Version**: 3.5.1
|
||||||
|
**Datum**: 2025-12-28
|
||||||
|
**Severity**: Critical
|
||||||
|
**Status**: ✅ Fixed
|
||||||
@@ -3,7 +3,10 @@ Battery Charging Optimizer für OpenEMS + GoodWe
|
|||||||
Nutzt das bestehende manuelle Steuerungssystem
|
Nutzt das bestehende manuelle Steuerungssystem
|
||||||
|
|
||||||
Speicherort: /config/pyscript/battery_charging_optimizer.py
|
Speicherort: /config/pyscript/battery_charging_optimizer.py
|
||||||
Version: 3.5.0 - REMOVED: Sicherheitspuffer und Reservekapazität (Hardware hat eigene Puffer)
|
Version: 3.5.1 - FIXED: Preisschwelle wird jetzt korrekt angewendet (kritischer Bug)
|
||||||
|
Stunden über Preisschwelle werden komplett ignoriert
|
||||||
|
Keine Ladung wenn alle Preise über Schwelle liegen
|
||||||
|
v3.5.0 - REMOVED: Sicherheitspuffer und Reservekapazität (Hardware hat eigene Puffer)
|
||||||
Batterie lädt jetzt bis 100% SOC
|
Batterie lädt jetzt bis 100% SOC
|
||||||
CHANGED: Standardwerte - Preisschwelle 25ct, Ladeleistung 8000W
|
CHANGED: Standardwerte - Preisschwelle 25ct, Ladeleistung 8000W
|
||||||
FIXED: SOC-Plausibilitäts-Check (filtert 65535% Spikes beim Modus-Wechsel)
|
FIXED: SOC-Plausibilitäts-Check (filtert 65535% Spikes beim Modus-Wechsel)
|
||||||
@@ -32,7 +35,7 @@ def calculate_charging_schedule():
|
|||||||
Nutzt Ranking-Methode: Wählt die N günstigsten Stunden aus
|
Nutzt Ranking-Methode: Wählt die N günstigsten Stunden aus
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log.info("=== Batterie-Optimierung gestartet (v3.5.0 - Volle Ladung bis 100%) ===")
|
log.info("=== Batterie-Optimierung gestartet (v3.5.1 - Preisschwelle aktiv) ===")
|
||||||
|
|
||||||
# Prüfe ob Optimierung aktiviert ist
|
# Prüfe ob Optimierung aktiviert ist
|
||||||
if state.get('input_boolean.battery_optimizer_enabled') != 'on':
|
if state.get('input_boolean.battery_optimizer_enabled') != 'on':
|
||||||
@@ -310,11 +313,35 @@ def optimize_charging(price_data, pv_forecast, current_soc, config):
|
|||||||
log.info(f"🎯 Benötigte Ladestunden: {needed_hours} (bei {max_charge_per_hour}W pro Stunde)")
|
log.info(f"🎯 Benötigte Ladestunden: {needed_hours} (bei {max_charge_per_hour}W pro Stunde)")
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# RANKING: Erstelle Kandidaten-Liste
|
# FILTER: Nur Stunden unter Preisschwelle
|
||||||
|
# ==========================================
|
||||||
|
price_threshold = config['price_threshold']
|
||||||
|
affordable_hours = []
|
||||||
|
expensive_hours = []
|
||||||
|
|
||||||
|
for p in future_price_data:
|
||||||
|
if p['price'] <= price_threshold:
|
||||||
|
affordable_hours.append(p)
|
||||||
|
else:
|
||||||
|
expensive_hours.append(p)
|
||||||
|
|
||||||
|
log.info(f"💶 Preisschwelle: {price_threshold} ct/kWh")
|
||||||
|
log.info(f" - Stunden unter Schwelle: {len(affordable_hours)}")
|
||||||
|
log.info(f" - Stunden über Schwelle: {len(expensive_hours)} (werden ignoriert)")
|
||||||
|
|
||||||
|
# Wenn keine bezahlbaren Stunden verfügbar, nicht laden
|
||||||
|
if not affordable_hours:
|
||||||
|
log.warning(f"⚠️ Keine Stunden unter Preisschwelle {price_threshold} ct/kWh gefunden!")
|
||||||
|
log.warning(f" Günstigster Preis: {min_price:.2f} ct/kWh")
|
||||||
|
log.warning(f" → Keine Ladung, bleibe im Auto-Modus")
|
||||||
|
return create_auto_only_schedule(future_price_data)
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# RANKING: Erstelle Kandidaten-Liste (nur bezahlbare Stunden)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
charging_candidates = []
|
charging_candidates = []
|
||||||
|
|
||||||
for p in future_price_data:
|
for p in affordable_hours:
|
||||||
# PV-Prognose für diese Stunde
|
# PV-Prognose für diese Stunde
|
||||||
pv_wh = pv_forecast['hourly'].get(p['hour'], 0)
|
pv_wh = pv_forecast['hourly'].get(p['hour'], 0)
|
||||||
|
|
||||||
@@ -336,8 +363,13 @@ def optimize_charging(price_data, pv_forecast, current_soc, config):
|
|||||||
# Sortiere nach Score (beste zuerst)
|
# Sortiere nach Score (beste zuerst)
|
||||||
charging_candidates.sort(key=lambda x: x['score'])
|
charging_candidates.sort(key=lambda x: x['score'])
|
||||||
|
|
||||||
# Wähle die N besten Stunden
|
# Wähle die N besten Stunden (begrenzt auf verfügbare bezahlbare Stunden)
|
||||||
selected_hours = charging_candidates[:needed_hours]
|
actual_hours_needed = min(needed_hours, len(charging_candidates))
|
||||||
|
selected_hours = charging_candidates[:actual_hours_needed]
|
||||||
|
|
||||||
|
if actual_hours_needed < needed_hours:
|
||||||
|
log.warning(f"⚠️ Nur {actual_hours_needed} von {needed_hours} benötigten Stunden unter Preisschwelle")
|
||||||
|
log.warning(f" Batterie wird nur teilweise geladen")
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# Logging: Zeige Auswahl
|
# Logging: Zeige Auswahl
|
||||||
@@ -414,8 +446,11 @@ def optimize_charging(price_data, pv_forecast, current_soc, config):
|
|||||||
reason = "Automatik"
|
reason = "Automatik"
|
||||||
|
|
||||||
# Debugging: Warum nicht geladen?
|
# Debugging: Warum nicht geladen?
|
||||||
if p['datetime'] not in selected_datetimes:
|
if p['price'] > price_threshold:
|
||||||
# Finde Position im Ranking
|
# Über Preisschwelle
|
||||||
|
reason = f"Zu teuer: {p['price']:.2f}ct (Schwelle: {price_threshold}ct)"
|
||||||
|
elif p['datetime'] not in selected_datetimes:
|
||||||
|
# Finde Position im Ranking (nur in bezahlbaren Stunden)
|
||||||
rank = 1
|
rank = 1
|
||||||
for candidate in charging_candidates:
|
for candidate in charging_candidates:
|
||||||
if candidate['datetime'] == p['datetime']:
|
if candidate['datetime'] == p['datetime']:
|
||||||
@@ -423,7 +458,7 @@ def optimize_charging(price_data, pv_forecast, current_soc, config):
|
|||||||
rank += 1
|
rank += 1
|
||||||
|
|
||||||
if rank <= len(charging_candidates):
|
if rank <= len(charging_candidates):
|
||||||
reason = f"Rang {rank} (nicht unter Top {needed_hours})"
|
reason = f"Rang {rank} (nicht unter Top {actual_hours_needed})"
|
||||||
|
|
||||||
schedule.append({
|
schedule.append({
|
||||||
'datetime': p['datetime'].isoformat(),
|
'datetime': p['datetime'].isoformat(),
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ Battery Charging Optimizer für OpenEMS + GoodWe
|
|||||||
Nutzt das bestehende manuelle Steuerungssystem
|
Nutzt das bestehende manuelle Steuerungssystem
|
||||||
|
|
||||||
Speicherort: /config/pyscript/battery_charging_optimizer.py
|
Speicherort: /config/pyscript/battery_charging_optimizer.py
|
||||||
Version: 3.5.0 - REMOVED: Sicherheitspuffer und Reservekapazität (Hardware hat eigene Puffer)
|
Version: 3.5.1 - FIXED: Preisschwelle wird jetzt korrekt angewendet (kritischer Bug)
|
||||||
|
Stunden über Preisschwelle werden komplett ignoriert
|
||||||
|
Keine Ladung wenn alle Preise über Schwelle liegen
|
||||||
|
v3.5.0 - REMOVED: Sicherheitspuffer und Reservekapazität (Hardware hat eigene Puffer)
|
||||||
Batterie lädt jetzt bis 100% SOC
|
Batterie lädt jetzt bis 100% SOC
|
||||||
CHANGED: Standardwerte - Preisschwelle 25ct, Ladeleistung 8000W
|
CHANGED: Standardwerte - Preisschwelle 25ct, Ladeleistung 8000W
|
||||||
FIXED: SOC-Plausibilitäts-Check (filtert 65535% Spikes beim Modus-Wechsel)
|
FIXED: SOC-Plausibilitäts-Check (filtert 65535% Spikes beim Modus-Wechsel)
|
||||||
@@ -32,7 +35,7 @@ def calculate_charging_schedule():
|
|||||||
Nutzt Ranking-Methode: Wählt die N günstigsten Stunden aus
|
Nutzt Ranking-Methode: Wählt die N günstigsten Stunden aus
|
||||||
"""
|
"""
|
||||||
|
|
||||||
log.info("=== Batterie-Optimierung gestartet (v3.5.0 - Volle Ladung bis 100%) ===")
|
log.info("=== Batterie-Optimierung gestartet (v3.5.1 - Preisschwelle aktiv) ===")
|
||||||
|
|
||||||
# Prüfe ob Optimierung aktiviert ist
|
# Prüfe ob Optimierung aktiviert ist
|
||||||
if state.get('input_boolean.battery_optimizer_enabled') != 'on':
|
if state.get('input_boolean.battery_optimizer_enabled') != 'on':
|
||||||
@@ -310,11 +313,35 @@ def optimize_charging(price_data, pv_forecast, current_soc, config):
|
|||||||
log.info(f"🎯 Benötigte Ladestunden: {needed_hours} (bei {max_charge_per_hour}W pro Stunde)")
|
log.info(f"🎯 Benötigte Ladestunden: {needed_hours} (bei {max_charge_per_hour}W pro Stunde)")
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# RANKING: Erstelle Kandidaten-Liste
|
# FILTER: Nur Stunden unter Preisschwelle
|
||||||
|
# ==========================================
|
||||||
|
price_threshold = config['price_threshold']
|
||||||
|
affordable_hours = []
|
||||||
|
expensive_hours = []
|
||||||
|
|
||||||
|
for p in future_price_data:
|
||||||
|
if p['price'] <= price_threshold:
|
||||||
|
affordable_hours.append(p)
|
||||||
|
else:
|
||||||
|
expensive_hours.append(p)
|
||||||
|
|
||||||
|
log.info(f"💶 Preisschwelle: {price_threshold} ct/kWh")
|
||||||
|
log.info(f" - Stunden unter Schwelle: {len(affordable_hours)}")
|
||||||
|
log.info(f" - Stunden über Schwelle: {len(expensive_hours)} (werden ignoriert)")
|
||||||
|
|
||||||
|
# Wenn keine bezahlbaren Stunden verfügbar, nicht laden
|
||||||
|
if not affordable_hours:
|
||||||
|
log.warning(f"⚠️ Keine Stunden unter Preisschwelle {price_threshold} ct/kWh gefunden!")
|
||||||
|
log.warning(f" Günstigster Preis: {min_price:.2f} ct/kWh")
|
||||||
|
log.warning(f" → Keine Ladung, bleibe im Auto-Modus")
|
||||||
|
return create_auto_only_schedule(future_price_data)
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# RANKING: Erstelle Kandidaten-Liste (nur bezahlbare Stunden)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
charging_candidates = []
|
charging_candidates = []
|
||||||
|
|
||||||
for p in future_price_data:
|
for p in affordable_hours:
|
||||||
# PV-Prognose für diese Stunde
|
# PV-Prognose für diese Stunde
|
||||||
pv_wh = pv_forecast['hourly'].get(p['hour'], 0)
|
pv_wh = pv_forecast['hourly'].get(p['hour'], 0)
|
||||||
|
|
||||||
@@ -336,8 +363,13 @@ def optimize_charging(price_data, pv_forecast, current_soc, config):
|
|||||||
# Sortiere nach Score (beste zuerst)
|
# Sortiere nach Score (beste zuerst)
|
||||||
charging_candidates.sort(key=lambda x: x['score'])
|
charging_candidates.sort(key=lambda x: x['score'])
|
||||||
|
|
||||||
# Wähle die N besten Stunden
|
# Wähle die N besten Stunden (begrenzt auf verfügbare bezahlbare Stunden)
|
||||||
selected_hours = charging_candidates[:needed_hours]
|
actual_hours_needed = min(needed_hours, len(charging_candidates))
|
||||||
|
selected_hours = charging_candidates[:actual_hours_needed]
|
||||||
|
|
||||||
|
if actual_hours_needed < needed_hours:
|
||||||
|
log.warning(f"⚠️ Nur {actual_hours_needed} von {needed_hours} benötigten Stunden unter Preisschwelle")
|
||||||
|
log.warning(f" Batterie wird nur teilweise geladen")
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# Logging: Zeige Auswahl
|
# Logging: Zeige Auswahl
|
||||||
@@ -414,8 +446,11 @@ def optimize_charging(price_data, pv_forecast, current_soc, config):
|
|||||||
reason = "Automatik"
|
reason = "Automatik"
|
||||||
|
|
||||||
# Debugging: Warum nicht geladen?
|
# Debugging: Warum nicht geladen?
|
||||||
if p['datetime'] not in selected_datetimes:
|
if p['price'] > price_threshold:
|
||||||
# Finde Position im Ranking
|
# Über Preisschwelle
|
||||||
|
reason = f"Zu teuer: {p['price']:.2f}ct (Schwelle: {price_threshold}ct)"
|
||||||
|
elif p['datetime'] not in selected_datetimes:
|
||||||
|
# Finde Position im Ranking (nur in bezahlbaren Stunden)
|
||||||
rank = 1
|
rank = 1
|
||||||
for candidate in charging_candidates:
|
for candidate in charging_candidates:
|
||||||
if candidate['datetime'] == p['datetime']:
|
if candidate['datetime'] == p['datetime']:
|
||||||
@@ -423,7 +458,7 @@ def optimize_charging(price_data, pv_forecast, current_soc, config):
|
|||||||
rank += 1
|
rank += 1
|
||||||
|
|
||||||
if rank <= len(charging_candidates):
|
if rank <= len(charging_candidates):
|
||||||
reason = f"Rang {rank} (nicht unter Top {needed_hours})"
|
reason = f"Rang {rank} (nicht unter Top {actual_hours_needed})"
|
||||||
|
|
||||||
schedule.append({
|
schedule.append({
|
||||||
'datetime': p['datetime'].isoformat(),
|
'datetime': p['datetime'].isoformat(),
|
||||||
|
|||||||
Reference in New Issue
Block a user