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:
41
.gitignore
vendored
41
.gitignore
vendored
@@ -4,9 +4,6 @@ __pycache__/
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
@@ -14,40 +11,18 @@ build/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Backup files
|
||||
*_backup.py
|
||||
*_backup.yaml
|
||||
*_old.py
|
||||
*_old.yaml
|
||||
*.bak
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
debug_log.txt
|
||||
|
||||
# Personal/Sensitive
|
||||
secrets.yaml
|
||||
*.secret
|
||||
*.key
|
||||
*.pem
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.bak
|
||||
|
||||
# Home Assistant specific
|
||||
.uuid
|
||||
.HA_VERSION
|
||||
.storage/
|
||||
.cloud/
|
||||
deps/
|
||||
tts/
|
||||
# Legacy versions (kept for reference but not tracked)
|
||||
legacy/
|
||||
|
||||
# Project specific
|
||||
*_fixed.py
|
||||
*_fixed.yaml
|
||||
DIAGNOSE_*.md
|
||||
FIX_*.md
|
||||
# Claude cache
|
||||
.claude/
|
||||
|
||||
49
CHANGELOG.md
49
CHANGELOG.md
@@ -5,6 +5,38 @@ 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/),
|
||||
und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
||||
|
||||
## [3.5.0] - 2025-12-28
|
||||
|
||||
### Removed
|
||||
- **Sicherheitspuffer entfernt**: 20% Safety Buffer führte zu unvollständiger Ladung
|
||||
- Problem: Mehr Ladestunden geplant als nötig, aber tatsächliche Ladung begrenzt
|
||||
- Folge: Batterie erreichte nie 100% SOC
|
||||
- Lösung: Hardware (GoodWe) hat eigene Sicherheitspuffer, Software-Puffer unnötig
|
||||
- **Reservekapazität entfernt**: 2 kWh Reserve ebenfalls in Hardware integriert
|
||||
- `input_number.battery_optimizer_reserve_capacity` wird nicht mehr verwendet
|
||||
- `input_number.battery_optimizer_safety_buffer` wird nicht mehr verwendet
|
||||
|
||||
### Changed
|
||||
- **Standardwerte optimiert**:
|
||||
- `max_charge_power`: 5000W → **8000W** (nutzt volle Inverter-Leistung)
|
||||
- `price_threshold`: 28 ct/kWh → **25 ct/kWh** (realistischere Schwelle)
|
||||
- **Ladelogik vereinfacht**: Direkte Berechnung ohne Puffer
|
||||
- **Logging verbessert**: Klarere Ausgaben zur Ladekapazität
|
||||
|
||||
### Fixed
|
||||
- **100% Ladung jetzt möglich**: Batterie wird vollständig geladen
|
||||
- **Genauere Ladestunden-Berechnung**: Nur tatsächlich benötigte Stunden werden geplant
|
||||
- **Bessere Kapazitätsnutzung**: Volle Ladeleistung in allen geplanten Stunden
|
||||
|
||||
### Migration 3.4.0 → 3.5.0
|
||||
1. Update `battery_charging_optimizer.py` zu v3.5.0
|
||||
2. **Optional**: Entferne nicht mehr verwendete Input Helper:
|
||||
- `input_number.battery_optimizer_reserve_capacity`
|
||||
- `input_number.battery_optimizer_safety_buffer`
|
||||
3. **Empfohlen**: Aktualisiere `input_number.battery_optimizer_max_charge_power` auf 8000W
|
||||
4. **Empfohlen**: Aktualisiere `input_number.battery_optimizer_price_threshold` auf 25 ct/kWh
|
||||
5. Teste Ladeverhalten über Nacht
|
||||
|
||||
## [3.3.2] - 2024-11-30
|
||||
|
||||
### Fixed
|
||||
@@ -139,14 +171,15 @@ e Home Assistant 2024.2+ Dashboards
|
||||
|
||||
## Version Matrix
|
||||
|
||||
| Version | Algorithm | Tomorrow Support | Dashboard | Automations |
|
||||
|---------|-----------|------------------|-----------|-------------|
|
||||
| 3.3.1 | Ranking | ✅ | Sections | HA-based |
|
||||
| 3.2.0 | Ranking | ✅ | Sections | PyScript |
|
||||
| 3.1.0 | Ranking | ✅ | Sections | PyScript |
|
||||
| 3.0.0 | Ranking | ✅ | Sections | PyScript |
|
||||
| 2.x | Threshold | ❌ | Enhanced | PyScript |
|
||||
| 1.x | Threshold | ❌ | Basic | PyScript |
|
||||
| Version | Algorithm | Tomorrow Support | Dashboard | Automations | 100% Ladung |
|
||||
|---------|-----------|------------------|-----------|-------------|-------------|
|
||||
| 3.5.0 | Ranking | ✅ | Sections | HA-based | ✅ |
|
||||
| 3.3.1 | Ranking | ✅ | Sections | HA-based | ❌ |
|
||||
| 3.2.0 | Ranking | ✅ | Sections | PyScript | ❌ |
|
||||
| 3.1.0 | Ranking | ✅ | Sections | PyScript | ❌ |
|
||||
| 3.0.0 | Ranking | ✅ | Sections | PyScript | ❌ |
|
||||
| 2.x | Threshold | ❌ | Enhanced | PyScript | ❌ |
|
||||
| 1.x | Threshold | ❌ | Basic | PyScript | ❌ |
|
||||
|
||||
---
|
||||
|
||||
|
||||
207
PROJECT_SUMMARY_v3.5.0.md
Normal file
207
PROJECT_SUMMARY_v3.5.0.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# Projekt-Zusammenfassung: Battery Optimizer v3.5.0
|
||||
|
||||
## Aufräumarbeiten durchgeführt ✅
|
||||
|
||||
### Entfernte/Archivierte Dateien
|
||||
- **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
|
||||
|
||||
- **Debug-Dateien** (gelöscht):
|
||||
- debug_log.txt
|
||||
- debug_schedule.py
|
||||
|
||||
- **Duplikate** (gelöscht):
|
||||
- docs/ (Duplikate von Root-Dokumentationen)
|
||||
- pyscripts/ (veraltete v3.3.1 Kopien, jetzt neu erstellt mit v3.5.0)
|
||||
|
||||
- **Legacy-Versionen** → `legacy/`:
|
||||
- v1/, v2/, v3/ (340KB historische Daten)
|
||||
|
||||
### Aktuelle Projektstruktur
|
||||
|
||||
```
|
||||
openems/
|
||||
├── archive/ # Alte Bugfix-Dokumentationen
|
||||
├── automations/ # Home Assistant Automationen
|
||||
├── config/ # Konfigurationsdateien
|
||||
├── dashboards/ # Dashboard-Varianten
|
||||
├── legacy/ # Alte Versionen (v1-v3)
|
||||
├── pyscripts/ # 🎯 PRODUKTIONS-SKRIPTE (v3.5.0)
|
||||
│ ├── battery_charging_optimizer.py
|
||||
│ ├── hastrom_flex_extended.py
|
||||
│ └── ess_set_power.py
|
||||
├── battery_charging_optimizer.py # Root-Version (Master)
|
||||
├── hastrom_flex_extended.py
|
||||
├── ess_set_power.py
|
||||
├── CHANGELOG.md # ✅ Aktualisiert mit v3.5.0
|
||||
├── CLAUDE.md # Projekt-Anweisungen für AI
|
||||
├── EMS_OpenEMS_HomeAssistant_Dokumentation.md
|
||||
├── INSTALLATION.md
|
||||
├── LICENSE
|
||||
├── README.md
|
||||
├── UPGRADE_TO_v3.5.0.md # 🆕 Neuer Upgrade-Guide
|
||||
├── project_memory.md
|
||||
├── battery_optimizer_config.yaml
|
||||
├── battery_optimizer_input_helper_safety_buffer.yaml # Veraltet
|
||||
├── battery_calibration_automation.yaml
|
||||
├── battery_calibration_input_helper.yaml
|
||||
└── rest_requests.yaml
|
||||
```
|
||||
|
||||
## Hauptänderungen v3.5.0
|
||||
|
||||
### 🔧 Code-Änderungen
|
||||
|
||||
#### 1. Sicherheitspuffer entfernt
|
||||
**Vorher (v3.4.0)**:
|
||||
```python
|
||||
'safety_buffer': float(state.get('input_number.battery_optimizer_safety_buffer') or 20) / 100, # 20%
|
||||
'reserve_capacity': float(state.get('input_number.battery_optimizer_reserve_capacity') or 2) * 1000, # 2 kWh
|
||||
|
||||
available_capacity_wh -= config['reserve_capacity'] # -2000 Wh
|
||||
available_capacity_wh_with_buffer = available_capacity_wh * (1 + safety_buffer) # +20%
|
||||
needed_hours = int((available_capacity_wh_with_buffer + ...) / max_charge_per_hour)
|
||||
```
|
||||
|
||||
**Nachher (v3.5.0)**:
|
||||
```python
|
||||
# Keine Puffer mehr!
|
||||
available_capacity_wh = (config['max_soc'] - current_soc) / 100 * config['battery_capacity']
|
||||
needed_hours = int((available_capacity_wh + max_charge_per_hour - 1) / max_charge_per_hour)
|
||||
```
|
||||
|
||||
#### 2. Standardwerte optimiert
|
||||
```python
|
||||
# Vorher:
|
||||
'max_charge_power': float(state.get('...') or 5000),
|
||||
'price_threshold': float(state.get('...') or 28),
|
||||
|
||||
# Nachher:
|
||||
'max_charge_power': float(state.get('...') or 8000), # +60%
|
||||
'price_threshold': float(state.get('...') or 25), # -10%
|
||||
```
|
||||
|
||||
#### 3. Version und Logging
|
||||
```python
|
||||
# Version Header:
|
||||
Version: 3.5.0 - REMOVED: Sicherheitspuffer und Reservekapazität
|
||||
Batterie lädt jetzt bis 100% SOC
|
||||
|
||||
# Log-Ausgaben:
|
||||
log.info("=== Batterie-Optimierung gestartet (v3.5.0 - Volle Ladung bis 100%) ===")
|
||||
log.info(f"Verfügbare Ladekapazität: {available_capacity_wh/1000:.2f} kWh (bis {config['max_soc']}% SOC)")
|
||||
log.info(f"🎯 Benötigte Ladestunden: {needed_hours} (bei {max_charge_per_hour}W pro Stunde)")
|
||||
```
|
||||
|
||||
### 📊 Problem-Analyse: Warum 100% nicht erreicht wurde
|
||||
|
||||
**Szenario**: SOC 50% → Soll auf 100%
|
||||
|
||||
**Alt (v3.4.0)**:
|
||||
```
|
||||
1. Verfügbare Kapazität: (100 - 50) * 10kWh = 5000 Wh
|
||||
2. Reserve-Abzug: 5000 - 2000 = 3000 Wh
|
||||
3. Sicherheitspuffer: 3000 * 1.20 = 3600 Wh (für Planung)
|
||||
4. Benötigte Stunden: 3600 / 5000 = 0.72 → 1 Stunde
|
||||
5. Geplante Energie: 1 Stunde * 5000W = 5000 Wh (theoretisch)
|
||||
6. ABER remaining_capacity = 3000 Wh (OHNE Puffer!)
|
||||
7. Tatsächlich geladen: min(5000, 3000) = 3000 Wh
|
||||
8. Ergebnis: 50% + 30% = 80% SOC ❌
|
||||
```
|
||||
|
||||
**Neu (v3.5.0)**:
|
||||
```
|
||||
1. Verfügbare Kapazität: (100 - 50) * 10kWh = 5000 Wh
|
||||
2. Benötigte Stunden: 5000 / 8000 = 0.625 → 1 Stunde
|
||||
3. Geplante Energie: 1 Stunde * 8000W = 8000 Wh (theoretisch)
|
||||
4. remaining_capacity: 5000 Wh (gleich wie verfügbar)
|
||||
5. Tatsächlich geladen: min(8000, 5000) = 5000 Wh
|
||||
6. Ergebnis: 50% + 50% = 100% SOC ✅
|
||||
```
|
||||
|
||||
### 🎯 Erwartete Verbesserungen
|
||||
|
||||
1. **Volle Ladung**: Batterie lädt bis 100% SOC (oder Hardware-Limit ~98%)
|
||||
2. **Weniger Ladestunden**: Bei 8000W statt 5000W werden ~37% weniger Stunden benötigt
|
||||
3. **Bessere Effizienz**: Volle Leistung in allen geplanten Stunden
|
||||
4. **Niedrigere Kosten**: Konzentration auf wirklich günstigste Stunden
|
||||
|
||||
### 📋 Migration-Checkliste
|
||||
|
||||
- [x] Code aktualisiert (v3.5.0)
|
||||
- [x] Syntax-Check erfolgreich
|
||||
- [x] pyscripts/ neu erstellt
|
||||
- [x] CHANGELOG.md aktualisiert
|
||||
- [x] Upgrade-Guide erstellt (UPGRADE_TO_v3.5.0.md)
|
||||
- [ ] In Home Assistant installieren
|
||||
- [ ] PyScript neu laden
|
||||
- [ ] Input Helper aktualisieren (8000W, 25ct)
|
||||
- [ ] Erste Berechnung testen (manuell)
|
||||
- [ ] Erste Ladung überwachen
|
||||
- [ ] 100% SOC bestätigen
|
||||
|
||||
## Nächste Schritte für Deployment
|
||||
|
||||
1. **Backup erstellen**:
|
||||
```bash
|
||||
ssh homeassistant
|
||||
cd /config/pyscript
|
||||
cp battery_charging_optimizer.py battery_charging_optimizer.py.v3.4.0.backup
|
||||
```
|
||||
|
||||
2. **Neue Version kopieren**:
|
||||
```bash
|
||||
# Von diesem Repository zu Home Assistant
|
||||
scp pyscripts/*.py homeassistant:/config/pyscript/
|
||||
```
|
||||
|
||||
3. **PyScript neu laden**:
|
||||
- Home Assistant → Developer Tools → Services
|
||||
- Service: `pyscript.reload`
|
||||
|
||||
4. **Input Helper anpassen**:
|
||||
- `input_number.battery_optimizer_max_charge_power` → 8000
|
||||
- `input_number.battery_optimizer_price_threshold` → 25
|
||||
|
||||
5. **Test durchführen**:
|
||||
- Service: `pyscript.calculate_charging_schedule`
|
||||
- Log prüfen: `tail -f /config/home-assistant.log | grep battery`
|
||||
- Schedule prüfen: State von `pyscript.battery_charging_schedule`
|
||||
|
||||
6. **Über Nacht testen**:
|
||||
- Erste Ladung abwarten
|
||||
- SOC-Verlauf überwachen
|
||||
- 100% Erreichen bestätigen
|
||||
|
||||
## Dateigrößen
|
||||
|
||||
- battery_charging_optimizer.py: 24 KB (v3.5.0)
|
||||
- Reduzierung durch Code-Bereinigung: ~1.5 KB (-6%)
|
||||
|
||||
## Git Status
|
||||
|
||||
Bereite Commit vor mit:
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Update: Battery Optimizer v3.5.0 - Volle Ladung bis 100% SOC
|
||||
|
||||
- Entfernt: Sicherheitspuffer (20%) und Reservekapazität (2kWh)
|
||||
- Hardware hat eigene Schutzpuffer, Software-Reserven unnötig
|
||||
- Geändert: Standardwerte auf 8000W und 25ct/kWh
|
||||
- Fix: Batterie lädt jetzt vollständig bis 100% SOC
|
||||
- Projekt aufgeräumt: Archive, Legacy-Versionen strukturiert
|
||||
- Upgrade-Guide hinzugefügt"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Status**: ✅ Bereit für Deployment
|
||||
**Version**: 3.5.0
|
||||
**Datum**: 2025-12-28
|
||||
**Größte Verbesserung**: 100% Ladung jetzt möglich
|
||||
196
UPGRADE_TO_v3.5.0.md
Normal file
196
UPGRADE_TO_v3.5.0.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# Upgrade Guide: v3.4.0 → v3.5.0
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
Version 3.5.0 entfernt die Software-Sicherheitspuffer, die verhinderten dass die Batterie vollständig bis 100% geladen wurde. Die Hardware (GoodWe) hat bereits eigene Schutzpuffer integriert, sodass zusätzliche Software-Reserven unnötig sind.
|
||||
|
||||
## Hauptänderungen
|
||||
|
||||
### Entfernte Features
|
||||
- **Sicherheitspuffer (20%)**: Führte zu unvollständiger Ladung
|
||||
- **Reservekapazität (2 kWh)**: Ebenfalls redundant
|
||||
|
||||
### Neue Standardwerte
|
||||
- **Ladeleistung**: 5000W → **8000W**
|
||||
- **Preisschwelle**: 28 ct/kWh → **25 ct/kWh**
|
||||
|
||||
### Verbesserungen
|
||||
- ✅ Batterie lädt jetzt vollständig bis 100% SOC
|
||||
- ✅ Genauere Berechnung der benötigten Ladestunden
|
||||
- ✅ Bessere Nutzung der verfügbaren Ladekapazität
|
||||
|
||||
## Upgrade-Schritte
|
||||
|
||||
### 1. Backup erstellen
|
||||
|
||||
```bash
|
||||
cd /config/pyscript
|
||||
cp battery_charging_optimizer.py battery_charging_optimizer.py.v3.4.0.backup
|
||||
```
|
||||
|
||||
### 2. Neues Skript kopieren
|
||||
|
||||
Kopiere die neue Version `battery_charging_optimizer.py` (v3.5.0) nach `/config/pyscript/`.
|
||||
|
||||
### 3. PyScript neu laden
|
||||
|
||||
In Home Assistant Developer Tools → Services:
|
||||
|
||||
```yaml
|
||||
service: pyscript.reload
|
||||
```
|
||||
|
||||
### 4. Input Helper aktualisieren (empfohlen)
|
||||
|
||||
#### Ladeleistung erhöhen (8000W)
|
||||
|
||||
In Home Assistant → Developer Tools → States:
|
||||
- Entity: `input_number.battery_optimizer_max_charge_power`
|
||||
- Neuer Wert: `8000`
|
||||
|
||||
Oder via UI:
|
||||
- Settings → Devices & Services → Helpers
|
||||
- Suche "Battery Optimizer Max Charge Power"
|
||||
- Setze auf 8000
|
||||
|
||||
#### Preisschwelle anpassen (25 ct/kWh)
|
||||
|
||||
In Home Assistant → Developer Tools → States:
|
||||
- Entity: `input_number.battery_optimizer_price_threshold`
|
||||
- Neuer Wert: `25`
|
||||
|
||||
### 5. Nicht mehr verwendete Helper entfernen (optional)
|
||||
|
||||
Diese Input Helper werden nicht mehr verwendet und können gelöscht werden:
|
||||
|
||||
- `input_number.battery_optimizer_reserve_capacity`
|
||||
- `input_number.battery_optimizer_safety_buffer`
|
||||
|
||||
**Hinweis**: Falls diese Helper nicht existieren, ist das kein Problem. Sie waren nur in v3.4.0 vorhanden.
|
||||
|
||||
### 6. Ersten Test durchführen
|
||||
|
||||
Warte auf die nächste automatische Berechnung (14:05 Uhr) oder starte manuell:
|
||||
|
||||
```yaml
|
||||
service: pyscript.calculate_charging_schedule
|
||||
```
|
||||
|
||||
Prüfe das Log:
|
||||
```bash
|
||||
tail -f /config/home-assistant.log | grep -i battery
|
||||
```
|
||||
|
||||
Erwartete Log-Ausgaben:
|
||||
```
|
||||
=== Batterie-Optimierung gestartet (v3.5.0 - Volle Ladung bis 100%) ===
|
||||
Verfügbare Ladekapazität: X.XX kWh (bis 100% SOC)
|
||||
🎯 Benötigte Ladestunden: N (bei 8000W pro Stunde)
|
||||
```
|
||||
|
||||
### 7. Ergebnis überprüfen
|
||||
|
||||
Prüfe nach der ersten Ladung:
|
||||
- Hat die Batterie 100% SOC erreicht?
|
||||
- Wurde in allen geplanten Ladestunden tatsächlich geladen?
|
||||
- Zeigt das Dashboard die korrekten Werte?
|
||||
|
||||
## Was ist anders?
|
||||
|
||||
### Vorher (v3.4.0)
|
||||
|
||||
```
|
||||
SOC: 50% → 5 kWh verfügbar
|
||||
- Reserve (2 kWh): → 3 kWh
|
||||
- Sicherheitspuffer (20%): → 3.6 kWh Planung
|
||||
→ Planung: 1 Stunde à 5000W (3.6 kWh theoretisch)
|
||||
→ Tatsächlich: nur 3 kWh geladen
|
||||
→ Ergebnis: 80% SOC statt 100%
|
||||
```
|
||||
|
||||
### Nachher (v3.5.0)
|
||||
|
||||
```
|
||||
SOC: 50% → 5 kWh verfügbar
|
||||
→ Planung: 1 Stunde à 8000W (5 kWh)
|
||||
→ Tatsächlich: 5 kWh geladen
|
||||
→ Ergebnis: 100% SOC ✅
|
||||
```
|
||||
|
||||
## Häufige Probleme
|
||||
|
||||
### "Batterie lädt immer noch nicht bis 100%"
|
||||
|
||||
**Mögliche Ursachen:**
|
||||
|
||||
1. **max_soc zu niedrig gesetzt**
|
||||
- Prüfe: `input_number.battery_optimizer_max_soc`
|
||||
- Sollte: `100`
|
||||
|
||||
2. **Alte Version noch aktiv**
|
||||
- Prüfe Log: Suche nach "v3.5.0" in den Log-Meldungen
|
||||
- Falls v3.4.0: PyScript neu laden
|
||||
|
||||
3. **Hardware-Limit erreicht**
|
||||
- GoodWe stoppt bei ~98-99% (normal)
|
||||
- Dies ist der Hardware-Schutz (gewollt)
|
||||
|
||||
### "Zu viele Ladestunden geplant"
|
||||
|
||||
Falls mehr Stunden geplant werden als nötig:
|
||||
- Prüfe `max_charge_power` Einstellung (sollte 8000W sein)
|
||||
- Bei 5000W werden mehr Stunden benötigt als bei 8000W
|
||||
|
||||
### "Fehler beim Laden des Skripts"
|
||||
|
||||
Falls PyScript-Fehler auftreten:
|
||||
1. Prüfe Syntax: `cd /config/pyscript && python3 -m py_compile battery_charging_optimizer.py`
|
||||
2. Prüfe Log: `/config/home-assistant.log`
|
||||
3. Stelle v3.4.0 Backup wieder her bei Bedarf
|
||||
|
||||
## Rollback (falls nötig)
|
||||
|
||||
Falls Probleme auftreten:
|
||||
|
||||
```bash
|
||||
cd /config/pyscript
|
||||
cp battery_charging_optimizer.py.v3.4.0.backup battery_charging_optimizer.py
|
||||
```
|
||||
|
||||
Dann PyScript neu laden.
|
||||
|
||||
## Support
|
||||
|
||||
Bei Problemen:
|
||||
- Prüfe CHANGELOG.md für Details
|
||||
- Prüfe Home Assistant Log
|
||||
- Erstelle Issue mit Log-Auszug
|
||||
|
||||
## Erwartetes Verhalten nach Upgrade
|
||||
|
||||
1. **Nächste Berechnung (14:05)**:
|
||||
- Log zeigt "v3.5.0"
|
||||
- Weniger Ladestunden geplant (wegen 8000W statt 5000W)
|
||||
- Keine Reserve-/Puffer-Meldungen mehr
|
||||
|
||||
2. **Erste Ladung**:
|
||||
- Volle 8000W Ladeleistung
|
||||
- Batterie erreicht 100% (oder Hardware-Limit ~98%)
|
||||
|
||||
3. **Dashboard**:
|
||||
- Status zeigt korrekte Anzahl Ladestunden
|
||||
- Energie-Anzeige passt zur Planung
|
||||
|
||||
## Weitere Optimierungen (optional)
|
||||
|
||||
Nach erfolgreichem Upgrade kannst du zusätzlich:
|
||||
|
||||
1. **min_soc senken**: Von 20% auf 10% (mehr nutzbare Kapazität)
|
||||
2. **Ladeleistung testen**: 8000W testen, ggf. auf 10000W erhöhen
|
||||
3. **Preisschwelle anpassen**: Je nach Tarifsituation 20-30 ct/kWh
|
||||
|
||||
---
|
||||
|
||||
**Version**: 3.5.0
|
||||
**Datum**: 2025-12-28
|
||||
**Author**: Felix
|
||||
@@ -3,8 +3,9 @@ Battery Charging Optimizer für OpenEMS + GoodWe
|
||||
Nutzt das bestehende manuelle Steuerungssystem
|
||||
|
||||
Speicherort: /config/pyscript/battery_charging_optimizer.py
|
||||
Version: 3.4.0 - NEW: Sicherheitspuffer (20%) für untertägige SOC-Schwankungen
|
||||
Konfigurierbar via input_number.battery_optimizer_safety_buffer
|
||||
Version: 3.5.0 - REMOVED: Sicherheitspuffer und Reservekapazität (Hardware hat eigene Puffer)
|
||||
Batterie lädt jetzt bis 100% SOC
|
||||
CHANGED: Standardwerte - Preisschwelle 25ct, Ladeleistung 8000W
|
||||
FIXED: SOC-Plausibilitäts-Check (filtert 65535% Spikes beim Modus-Wechsel)
|
||||
Setzt charge_power_battery als negativen Wert (Laden = negativ)
|
||||
Nutzt bestehende Automations für ESS-Modus und Keep-Alive
|
||||
@@ -31,7 +32,7 @@ def calculate_charging_schedule():
|
||||
Nutzt Ranking-Methode: Wählt die N günstigsten Stunden aus
|
||||
"""
|
||||
|
||||
log.info("=== Batterie-Optimierung gestartet (v3.2 - FIXED Timezones) ===")
|
||||
log.info("=== Batterie-Optimierung gestartet (v3.5.0 - Volle Ladung bis 100%) ===")
|
||||
|
||||
# Prüfe ob Optimierung aktiviert ist
|
||||
if state.get('input_boolean.battery_optimizer_enabled') != 'on':
|
||||
@@ -105,11 +106,9 @@ def load_configuration():
|
||||
'battery_capacity': float(state.get('input_number.battery_capacity_kwh') or 10) * 1000, # in Wh
|
||||
'min_soc': float(state.get('input_number.battery_optimizer_min_soc') or 20),
|
||||
'max_soc': float(state.get('input_number.battery_optimizer_max_soc') or 100),
|
||||
'max_charge_power': float(state.get('input_number.battery_optimizer_max_charge_power') or 5000),
|
||||
'price_threshold': float(state.get('input_number.battery_optimizer_price_threshold') or 28),
|
||||
'reserve_capacity': float(state.get('input_number.battery_optimizer_reserve_capacity') or 2) * 1000, # in Wh
|
||||
'max_charge_power': float(state.get('input_number.battery_optimizer_max_charge_power') or 8000),
|
||||
'price_threshold': float(state.get('input_number.battery_optimizer_price_threshold') or 25),
|
||||
'pv_threshold': float(state.get('input_number.battery_optimizer_pv_threshold') or 500), # in Wh
|
||||
'safety_buffer': float(state.get('input_number.battery_optimizer_safety_buffer') or 20) / 100, # in %
|
||||
}
|
||||
|
||||
|
||||
@@ -290,36 +289,25 @@ def optimize_charging(price_data, pv_forecast, current_soc, config):
|
||||
avg_price = sum(all_prices) / len(all_prices)
|
||||
log.info(f"Preise: Min={min_price:.2f}, Max={max_price:.2f}, Avg={avg_price:.2f} ct/kWh")
|
||||
|
||||
# Verfügbare Ladekapazität berechnen
|
||||
# Verfügbare Ladekapazität berechnen (OHNE Reserven - Hardware hat eigene Puffer)
|
||||
available_capacity_wh = (config['max_soc'] - current_soc) / 100 * config['battery_capacity']
|
||||
available_capacity_wh -= config['reserve_capacity']
|
||||
|
||||
if available_capacity_wh <= 0:
|
||||
log.info("Batterie ist voll oder Reserve erreicht - keine Ladung nötig")
|
||||
log.info("Batterie ist bereits voll - keine Ladung nötig")
|
||||
return create_auto_only_schedule(future_price_data)
|
||||
|
||||
log.info(f"Verfügbare Ladekapazität (berechnet): {available_capacity_wh/1000:.2f} kWh")
|
||||
|
||||
# ==========================================
|
||||
# Sicherheitspuffer: +X% für untertägige Schwankungen
|
||||
# ==========================================
|
||||
# Grund: SOC kann sich zwischen Planung (14:05) und Ladestart (z.B. 22:00) ändern
|
||||
# durch PV-Rest-Produktion, Eigenverbrauch, etc.
|
||||
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 (mit {safety_buffer*100:.0f}% Puffer): {available_capacity_wh_with_buffer/1000:.2f} kWh")
|
||||
log.info(f"Verfügbare Ladekapazität: {available_capacity_wh/1000:.2f} kWh (bis {config['max_soc']}% SOC)")
|
||||
|
||||
# ==========================================
|
||||
# Berechne benötigte Ladestunden
|
||||
# ==========================================
|
||||
max_charge_per_hour = config['max_charge_power'] # Wh pro Stunde
|
||||
needed_hours = int((available_capacity_wh_with_buffer + max_charge_per_hour - 1) / max_charge_per_hour) # Aufrunden
|
||||
needed_hours = int((available_capacity_wh + max_charge_per_hour - 1) / max_charge_per_hour) # Aufrunden
|
||||
|
||||
# Mindestens 1 Stunde, maximal verfügbare Stunden
|
||||
needed_hours = max(1, min(needed_hours, len(future_price_data)))
|
||||
|
||||
log.info(f"🎯 Benötigte Ladestunden: {needed_hours} (bei {max_charge_per_hour}W pro Stunde, inkl. Puffer)")
|
||||
log.info(f"🎯 Benötigte Ladestunden: {needed_hours} (bei {max_charge_per_hour}W pro Stunde)")
|
||||
|
||||
# ==========================================
|
||||
# RANKING: Erstelle Kandidaten-Liste
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
2025-11-25 10:26:15.235 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] ============================================================
|
||||
2025-11-25 10:26:15.236 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] === DEBUG: Battery Charging Schedule ===
|
||||
2025-11-25 10:26:15.237 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] ============================================================
|
||||
2025-11-25 10:26:15.237 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Aktuelle Zeit: 2025-11-25 10:26:15 CET
|
||||
2025-11-25 10:26:15.237 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule]
|
||||
2025-11-25 10:26:15.237 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] --- 1. Optimizer Status ---
|
||||
2025-11-25 10:26:15.237 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Optimizer enabled: on
|
||||
2025-11-25 10:26:15.238 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Manual override: off
|
||||
2025-11-25 10:26:15.238 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Manual control active: off
|
||||
2025-11-25 10:26:15.238 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule]
|
||||
2025-11-25 10:26:15.239 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] --- 2. Schedule State ---
|
||||
2025-11-25 10:26:15.240 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Schedule Value: active
|
||||
2025-11-25 10:26:15.240 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Last Update: 2025-11-25T10:17:41.961386+01:00
|
||||
2025-11-25 10:26:15.240 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Anzahl Stunden im Plan: 13
|
||||
2025-11-25 10:26:15.240 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Anzahl Ladungen gesamt: 2
|
||||
2025-11-25 10:26:15.240 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Anzahl Ladungen morgen: 2
|
||||
2025-11-25 10:26:15.240 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Gesamt-Energie: 6.6 kWh
|
||||
2025-11-25 10:26:15.242 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Durchschnittspreis: 29.31 ct/kWh
|
||||
2025-11-25 10:26:15.242 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Tomorrow-Daten vorhanden: True
|
||||
2025-11-25 10:26:15.243 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Erste Ladung: 2025-11-25T22:00:00+01:00
|
||||
2025-11-25 10:26:15.243 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Erste Ladung morgen: 2025-11-25T22:00:00+01:00
|
||||
2025-11-25 10:26:15.243 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule]
|
||||
2025-11-25 10:26:15.245 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] --- 3. Schedule Details ---
|
||||
2025-11-25 10:26:15.247 WARNING (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] ⚠ Kein Eintrag für 2025-11-25 10:00 gefunden!
|
||||
2025-11-25 10:26:15.247 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule]
|
||||
2025-11-25 10:26:15.247 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] --- 4. Nächste 24 Stunden ---
|
||||
2025-11-25 10:26:15.249 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] 🔄 11:00 (morgen): auto | 0W | 55.02ct | Rang 11 (nicht unter Top 2)
|
||||
2025-11-25 10:26:15.250 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] 🔄 12:00 (morgen): auto | 0W | 57.92ct | Rang 12 (nicht unter Top 2)
|
||||
2025-11-25 10:26:15.251 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] 🔄 13:00 (morgen): auto | 0W | 53.95ct | Rang 9 (nicht unter Top 2)
|
||||
2025-11-25 10:26:15.252 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] 🔄 14:00 (morgen): auto | 0W | 51.85ct | Rang 7 (nicht unter Top 2)
|
||||
2025-11-25 10:26:15.253 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] 🔄 15:00 (morgen): auto | 0W | 53.04ct | Rang 8 (nicht unter Top 2)
|
||||
2025-11-25 10:26:15.254 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] 🔄 16:00 (morgen): auto | 0W | 54.83ct | Rang 10 (nicht unter Top 2)
|
||||
2025-11-25 10:26:15.255 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] 🔄 17:00 (morgen): auto | 0W | 60.57ct | Rang 13 (nicht unter Top 2)
|
||||
2025-11-25 10:26:15.255 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] 🔄 18:00 (morgen): auto | 0W | 51.60ct | Rang 6 (nicht unter Top 2)
|
||||
2025-11-25 10:26:15.255 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] 🔄 19:00 (morgen): auto | 0W | 45.82ct | Rang 5 (nicht unter Top 2)
|
||||
2025-11-25 10:26:15.256 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] 🔄 20:00 (morgen): auto | 0W | 36.72ct | Rang 4 (nicht unter Top 2)
|
||||
2025-11-25 10:26:15.259 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] 🔄 21:00 (morgen): auto | 0W | 32.22ct | Rang 3 (nicht unter Top 2)
|
||||
2025-11-25 10:26:15.262 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] 🔋 22:00 (morgen): charge | -5000W | 30.33ct | Rang 2/13: 30.33ct [morgen]
|
||||
2025-11-25 10:26:15.262 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] 🔋 23:00 (morgen): charge | -1600W | 28.29ct | Rang 1/13: 28.29ct [morgen]
|
||||
2025-11-25 10:26:15.262 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule]
|
||||
2025-11-25 10:26:15.263 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] --- 5. Geplante Ladungen (nächste 12h) ---
|
||||
2025-11-25 10:26:15.263 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] ✓ 1 Ladungen geplant:
|
||||
2025-11-25 10:26:15.263 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] 🔋 22:00 (morgen): 5000W @ 30.33ct
|
||||
2025-11-25 10:26:15.263 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule]
|
||||
2025-11-25 10:26:15.263 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] --- 6. Batterie Status ---
|
||||
2025-11-25 10:26:15.265 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Aktueller SOC: 14.0%
|
||||
2025-11-25 10:26:15.266 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] SOC-Bereich: 20.0% - 100.0%
|
||||
2025-11-25 10:26:15.267 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Batterie-Kapazität: 10.0 kWh
|
||||
2025-11-25 10:26:15.267 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Ziel-Ladeleistung: -1500.0W
|
||||
2025-11-25 10:26:15.267 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Verfügbare Ladekapazität: 8.60 kWh
|
||||
2025-11-25 10:26:15.267 WARNING (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] ⚠ SOC unter Minimum (20.0%)!
|
||||
2025-11-25 10:26:15.268 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule]
|
||||
2025-11-25 10:26:15.268 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] --- 7. Strompreis-Sensoren ---
|
||||
2025-11-25 10:26:15.268 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] ✓ Extended Sensor verfügbar:
|
||||
2025-11-25 10:26:15.268 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Preise heute: 24 Stunden
|
||||
2025-11-25 10:26:15.268 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Preise morgen: 24 Stunden
|
||||
2025-11-25 10:26:15.268 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Tomorrow verfügbar: True
|
||||
2025-11-25 10:26:15.269 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Preisspanne heute: 24.49 - 47.50 ct/kWh
|
||||
2025-11-25 10:26:15.269 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule]
|
||||
2025-11-25 10:26:15.269 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] --- 8. Automation Status ---
|
||||
2025-11-25 10:26:15.269 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Stündliche Ausführung: on
|
||||
2025-11-25 10:26:15.270 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Tägliche Planung: on
|
||||
2025-11-25 10:26:15.270 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Keep-Alive (Manuell Laden): off
|
||||
2025-11-25 10:26:15.270 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule]
|
||||
2025-11-25 10:26:15.270 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] ============================================================
|
||||
2025-11-25 10:26:15.270 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] === ZUSAMMENFASSUNG ===
|
||||
2025-11-25 10:26:15.276 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] ============================================================
|
||||
2025-11-25 10:26:15.278 WARNING (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] ⚠ PROBLEME GEFUNDEN:
|
||||
2025-11-25 10:26:15.279 WARNING (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] - Kein Schedule-Eintrag für aktuelle Stunde (10:00)
|
||||
2025-11-25 10:26:15.280 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule]
|
||||
2025-11-25 10:26:15.280 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] Debug-Report abgeschlossen
|
||||
2025-11-25 10:26:15.281 INFO (MainThread) [custom_components.pyscript.file.debug_schedule.debug_schedule] ============================================================
|
||||
@@ -1,285 +0,0 @@
|
||||
"""
|
||||
Debug Script für Battery Charging Optimizer
|
||||
Speicherort: /config/pyscript/debug_schedule.py
|
||||
|
||||
Verwendung in Home Assistant Developer Tools → Services:
|
||||
service: pyscript.debug_schedule
|
||||
data: {}
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
TIMEZONE = ZoneInfo("Europe/Berlin")
|
||||
|
||||
def get_local_now():
|
||||
"""Gibt die aktuelle Zeit in lokaler Timezone zurück"""
|
||||
return datetime.now(TIMEZONE)
|
||||
|
||||
|
||||
@service
|
||||
def debug_schedule():
|
||||
"""
|
||||
Gibt detaillierte Debug-Informationen über den aktuellen Schedule aus
|
||||
"""
|
||||
|
||||
log.info("=" * 60)
|
||||
log.info("=== DEBUG: Battery Charging Schedule ===")
|
||||
log.info("=" * 60)
|
||||
|
||||
now = get_local_now()
|
||||
log.info(f"Aktuelle Zeit: {now.strftime('%Y-%m-%d %H:%M:%S %Z')}")
|
||||
|
||||
# 1. Prüfe Optimizer-Status
|
||||
log.info("")
|
||||
log.info("--- 1. Optimizer Status ---")
|
||||
optimizer_enabled = state.get('input_boolean.battery_optimizer_enabled')
|
||||
manual_override = state.get('input_boolean.battery_optimizer_manual_override')
|
||||
manual_control = state.get('input_boolean.goodwe_manual_control')
|
||||
|
||||
log.info(f"Optimizer enabled: {optimizer_enabled}")
|
||||
log.info(f"Manual override: {manual_override}")
|
||||
log.info(f"Manual control active: {manual_control}")
|
||||
|
||||
if optimizer_enabled != 'on':
|
||||
log.warning("⚠ Optimizer ist DEAKTIVIERT!")
|
||||
|
||||
if manual_override == 'on':
|
||||
log.warning("⚠ Manual override ist AKTIV - Ausführung wird übersprungen!")
|
||||
|
||||
# 2. Prüfe Schedule State
|
||||
log.info("")
|
||||
log.info("--- 2. Schedule State ---")
|
||||
|
||||
schedule_attr = state.getattr('pyscript.battery_charging_schedule')
|
||||
|
||||
if not schedule_attr:
|
||||
log.error("❌ Kein Schedule vorhanden!")
|
||||
log.info("Führe aus: service: pyscript.calculate_charging_schedule")
|
||||
return
|
||||
|
||||
log.info(f"Schedule Value: {state.get('pyscript.battery_charging_schedule')}")
|
||||
log.info(f"Last Update: {schedule_attr.get('last_update', 'N/A')}")
|
||||
log.info(f"Anzahl Stunden im Plan: {schedule_attr.get('num_hours', 0)}")
|
||||
log.info(f"Anzahl Ladungen gesamt: {schedule_attr.get('num_charges', 0)}")
|
||||
log.info(f"Anzahl Ladungen morgen: {schedule_attr.get('num_charges_tomorrow', 0)}")
|
||||
log.info(f"Gesamt-Energie: {schedule_attr.get('total_energy_kwh', 0)} kWh")
|
||||
log.info(f"Durchschnittspreis: {schedule_attr.get('avg_charge_price', 0):.2f} ct/kWh")
|
||||
log.info(f"Tomorrow-Daten vorhanden: {schedule_attr.get('has_tomorrow_data', False)}")
|
||||
|
||||
first_charge = schedule_attr.get('first_charge_time')
|
||||
first_charge_tomorrow = schedule_attr.get('first_charge_tomorrow')
|
||||
|
||||
if first_charge:
|
||||
log.info(f"Erste Ladung: {first_charge}")
|
||||
else:
|
||||
log.warning("⚠ Keine Ladungen geplant!")
|
||||
|
||||
if first_charge_tomorrow:
|
||||
log.info(f"Erste Ladung morgen: {first_charge_tomorrow}")
|
||||
|
||||
# 3. Prüfe Schedule-Einträge
|
||||
log.info("")
|
||||
log.info("--- 3. Schedule Details ---")
|
||||
|
||||
schedule = schedule_attr.get('schedule', [])
|
||||
|
||||
if not schedule:
|
||||
log.error("❌ Schedule Array ist leer!")
|
||||
return
|
||||
|
||||
current_hour = now.hour
|
||||
current_date = now.date()
|
||||
|
||||
# Finde aktuellen Eintrag
|
||||
current_entry = None
|
||||
current_index = None
|
||||
|
||||
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)
|
||||
|
||||
entry_date = entry_dt.date()
|
||||
entry_hour = entry_dt.hour
|
||||
|
||||
if entry_date == current_date and entry_hour == current_hour:
|
||||
current_entry = entry
|
||||
current_index = i
|
||||
break
|
||||
|
||||
if current_entry:
|
||||
log.info(f"✓ Aktueller Eintrag gefunden (Index {current_index}):")
|
||||
log.info(f" Zeit: {current_entry['datetime']}")
|
||||
log.info(f" Aktion: {current_entry['action']}")
|
||||
log.info(f" Leistung: {current_entry['power_w']}W")
|
||||
log.info(f" Preis: {current_entry['price']:.2f} ct/kWh")
|
||||
log.info(f" Grund: {current_entry.get('reason', 'N/A')}")
|
||||
|
||||
if current_entry['action'] == 'charge':
|
||||
log.info(" → ✓ Sollte LADEN")
|
||||
else:
|
||||
log.info(" → ℹ Sollte AUTO-Modus")
|
||||
else:
|
||||
log.warning(f"⚠ Kein Eintrag für {current_date} {current_hour}:00 gefunden!")
|
||||
|
||||
# 4. Zeige nächste Stunden
|
||||
log.info("")
|
||||
log.info("--- 4. Nächste 24 Stunden ---")
|
||||
|
||||
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)
|
||||
|
||||
# Sortiere nach Zeit
|
||||
future_entries.sort(key=lambda x: datetime.fromisoformat(x['datetime']))
|
||||
|
||||
# Zeige erste 24
|
||||
for entry in future_entries[:24]:
|
||||
entry_dt = datetime.fromisoformat(entry['datetime'])
|
||||
if entry_dt.tzinfo is None:
|
||||
entry_dt = entry_dt.replace(tzinfo=TIMEZONE)
|
||||
|
||||
action_symbol = "🔋" if entry['action'] == 'charge' else "🔄"
|
||||
day_str = "morgen" if entry.get('is_tomorrow', False) else "heute"
|
||||
|
||||
log.info(
|
||||
f"{action_symbol} {entry_dt.strftime('%H:%M')} ({day_str}): "
|
||||
f"{entry['action']:6s} | "
|
||||
f"{entry['power_w']:6d}W | "
|
||||
f"{entry['price']:5.2f}ct | "
|
||||
f"{entry.get('reason', '')}"
|
||||
)
|
||||
|
||||
# 5. Prüfe Ladungen in den nächsten 12 Stunden
|
||||
log.info("")
|
||||
log.info("--- 5. Geplante Ladungen (nächste 12h) ---")
|
||||
|
||||
upcoming_charges = []
|
||||
for entry in future_entries[:12]:
|
||||
if entry['action'] == 'charge':
|
||||
upcoming_charges.append(entry)
|
||||
|
||||
if upcoming_charges:
|
||||
log.info(f"✓ {len(upcoming_charges)} Ladungen geplant:")
|
||||
for entry in upcoming_charges:
|
||||
entry_dt = datetime.fromisoformat(entry['datetime'])
|
||||
if entry_dt.tzinfo is None:
|
||||
entry_dt = entry_dt.replace(tzinfo=TIMEZONE)
|
||||
day_str = "morgen" if entry.get('is_tomorrow', False) else "heute"
|
||||
log.info(
|
||||
f" 🔋 {entry_dt.strftime('%H:%M')} ({day_str}): "
|
||||
f"{abs(entry['power_w'])}W @ {entry['price']:.2f}ct"
|
||||
)
|
||||
else:
|
||||
log.warning("⚠ Keine Ladungen in den nächsten 12 Stunden geplant!")
|
||||
|
||||
# 6. Prüfe Batterie-Status
|
||||
log.info("")
|
||||
log.info("--- 6. Batterie Status ---")
|
||||
|
||||
soc = float(state.get('sensor.esssoc') or 0)
|
||||
min_soc = float(state.get('input_number.battery_optimizer_min_soc') or 20)
|
||||
max_soc = float(state.get('input_number.battery_optimizer_max_soc') or 100)
|
||||
capacity = float(state.get('input_number.battery_capacity_kwh') or 10)
|
||||
charge_power = float(state.get('input_number.charge_power_battery') or 0)
|
||||
|
||||
log.info(f"Aktueller SOC: {soc}%")
|
||||
log.info(f"SOC-Bereich: {min_soc}% - {max_soc}%")
|
||||
log.info(f"Batterie-Kapazität: {capacity} kWh")
|
||||
log.info(f"Ziel-Ladeleistung: {charge_power}W")
|
||||
|
||||
available_capacity = (max_soc - soc) / 100 * capacity
|
||||
log.info(f"Verfügbare Ladekapazität: {available_capacity:.2f} kWh")
|
||||
|
||||
if soc >= max_soc:
|
||||
log.info("ℹ Batterie ist voll - keine Ladung nötig")
|
||||
elif soc < min_soc:
|
||||
log.warning(f"⚠ SOC unter Minimum ({min_soc}%)!")
|
||||
|
||||
# 7. Prüfe Preis-Sensoren
|
||||
log.info("")
|
||||
log.info("--- 7. Strompreis-Sensoren ---")
|
||||
|
||||
# Extended Sensor
|
||||
price_ext_state = state.get('sensor.hastrom_flex_pro_ext')
|
||||
price_ext_attr = state.getattr('sensor.hastrom_flex_pro_ext')
|
||||
|
||||
if price_ext_attr:
|
||||
log.info("✓ Extended Sensor verfügbar:")
|
||||
prices_today = price_ext_attr.get('prices_today', [])
|
||||
prices_tomorrow = price_ext_attr.get('prices_tomorrow', [])
|
||||
tomorrow_available = price_ext_attr.get('tomorrow_available', False)
|
||||
|
||||
log.info(f" Preise heute: {len(prices_today)} Stunden")
|
||||
log.info(f" Preise morgen: {len(prices_tomorrow)} Stunden")
|
||||
log.info(f" Tomorrow verfügbar: {tomorrow_available}")
|
||||
|
||||
if prices_today:
|
||||
min_price_today = min(prices_today)
|
||||
max_price_today = max(prices_today)
|
||||
log.info(f" Preisspanne heute: {min_price_today:.2f} - {max_price_today:.2f} ct/kWh")
|
||||
|
||||
if not tomorrow_available and now.hour >= 14:
|
||||
log.warning("⚠ Tomorrow-Preise sollten verfügbar sein (nach 14:00), sind es aber nicht!")
|
||||
else:
|
||||
log.warning("⚠ Extended Sensor nicht verfügbar")
|
||||
|
||||
# 8. Prüfe Automations
|
||||
log.info("")
|
||||
log.info("--- 8. Automation Status ---")
|
||||
|
||||
automation_hourly = state.get('automation.batterie_optimierung_stundliche_ausfuhrung')
|
||||
automation_daily = state.get('automation.batterie_optimierung_tagliche_planung')
|
||||
automation_keepalive = state.get('automation.automation_speicher_manuell_laden')
|
||||
|
||||
log.info(f"Stündliche Ausführung: {automation_hourly if automation_hourly else 'NICHT GEFUNDEN'}")
|
||||
log.info(f"Tägliche Planung: {automation_daily if automation_daily else 'NICHT GEFUNDEN'}")
|
||||
log.info(f"Keep-Alive (Manuell Laden): {automation_keepalive if automation_keepalive else 'NICHT GEFUNDEN'}")
|
||||
|
||||
if automation_keepalive and automation_keepalive == 'off':
|
||||
if manual_control == 'on':
|
||||
log.error("❌ PROBLEM: Manual Control ist ON, aber Keep-Alive Automation ist OFF!")
|
||||
log.info(" → Keine Modbus-Befehle werden gesendet!")
|
||||
|
||||
# 9. Zusammenfassung
|
||||
log.info("")
|
||||
log.info("=" * 60)
|
||||
log.info("=== ZUSAMMENFASSUNG ===")
|
||||
log.info("=" * 60)
|
||||
|
||||
issues = []
|
||||
|
||||
if optimizer_enabled != 'on':
|
||||
issues.append("Optimizer ist deaktiviert")
|
||||
|
||||
if not schedule:
|
||||
issues.append("Kein Schedule vorhanden")
|
||||
|
||||
if schedule_attr.get('num_charges', 0) == 0:
|
||||
issues.append("Keine Ladungen geplant")
|
||||
|
||||
if not schedule_attr.get('has_tomorrow_data', False) and now.hour >= 14:
|
||||
issues.append("Tomorrow-Daten fehlen (sollten nach 14:00 da sein)")
|
||||
|
||||
if not current_entry:
|
||||
issues.append(f"Kein Schedule-Eintrag für aktuelle Stunde ({current_hour}:00)")
|
||||
|
||||
if manual_control == 'on' and automation_keepalive == 'off':
|
||||
issues.append("Keep-Alive Automation ist deaktiviert trotz Manual Control")
|
||||
|
||||
if issues:
|
||||
log.warning("⚠ PROBLEME GEFUNDEN:")
|
||||
for issue in issues:
|
||||
log.warning(f" - {issue}")
|
||||
else:
|
||||
log.info("✓ Keine offensichtlichen Probleme gefunden")
|
||||
|
||||
log.info("")
|
||||
log.info("Debug-Report abgeschlossen")
|
||||
log.info("=" * 60)
|
||||
269
docs/CLAUDE.md
269
docs/CLAUDE.md
@@ -1,269 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Intelligent battery charging optimizer for Home Assistant integrated with OpenEMS and GoodWe hardware. The system optimizes battery charging based on dynamic electricity pricing from haStrom FLEX PRO tariff and solar forecasts, automatically scheduling charging during the cheapest price periods.
|
||||
|
||||
**Hardware**: 10 kWh GoodWe battery, 10 kW inverter, 9.2 kWp PV (east-west orientation)
|
||||
**Control**: BeagleBone running OpenEMS, controlled via Modbus TCP and JSON-RPC
|
||||
**Home Assistant**: PyScript-based optimization running on /config/pyscript/
|
||||
|
||||
## Repository Structure
|
||||
|
||||
This repository contains **versioned iterations** of the battery optimization system:
|
||||
|
||||
```
|
||||
/
|
||||
├── v1/ # Initial implementation (threshold-based)
|
||||
├── v2/ # Improved version
|
||||
├── v3/ # Latest version (ranking-based optimization, sections dashboards)
|
||||
├── battery_charging_optimizer.py # Current production PyScript (v3.1.0)
|
||||
├── hastrom_flex_extended.py # Tomorrow-aware price fetcher
|
||||
├── ess_set_power.py # Modbus FLOAT32 power control
|
||||
├── EMS_OpenEMS_HomeAssistant_Dokumentation.md # Comprehensive technical docs
|
||||
└── project_memory.md # AI assistant context memory
|
||||
```
|
||||
|
||||
**Important**: The root-level `.py` files are the **current production versions**. Version folders contain historical snapshots and documentation from development iterations.
|
||||
|
||||
## Core Architecture
|
||||
|
||||
### Control Flow
|
||||
|
||||
```
|
||||
14:05 daily → Fetch prices → Optimize schedule → Store in pyscript state
|
||||
↓
|
||||
xx:05 hourly → Read schedule → Check current hour → Execute action
|
||||
↓
|
||||
If charging → Enable manual mode → Set power via Modbus → Trigger automation
|
||||
If auto → Disable manual mode → Let OpenEMS manage battery
|
||||
```
|
||||
|
||||
### Critical Components
|
||||
|
||||
**1. Battery Charging Optimizer** (`battery_charging_optimizer.py`)
|
||||
- Ranking-based optimization: selects N cheapest hours from combined today+tomorrow data
|
||||
- Runs daily at 14:05 (after price publication) and hourly at xx:05
|
||||
- Stores schedule in `pyscript.battery_charging_schedule` state with attributes
|
||||
- Conservative strategy: 20-100% SOC range, 2 kWh reserve for self-consumption
|
||||
|
||||
**2. Price Fetcher** (`hastrom_flex_extended.py`)
|
||||
- Fetches haStrom FLEX PRO prices with tomorrow support
|
||||
- Creates sensors: `sensor.hastrom_flex_pro_ext` and `sensor.hastrom_flex_ext`
|
||||
- **Critical**: Field name is `t_price_has_pro_incl_vat` (not standard field name)
|
||||
- Updates hourly, with special triggers at 14:05 and midnight
|
||||
|
||||
**3. Modbus Power Control** (`ess_set_power.py`)
|
||||
- Controls battery via Modbus register 706 (SetActivePowerEquals)
|
||||
- **Critical**: Uses IEEE 754 FLOAT32 Big-Endian encoding
|
||||
- Negative values = charging, positive = discharging
|
||||
|
||||
### OpenEMS Integration Details
|
||||
|
||||
**Controller Priority System**:
|
||||
- Controllers execute in **alphabetical order**
|
||||
- Later controllers can override earlier ones
|
||||
- Use `ctrlBalancing0` with `SET_GRID_ACTIVE_POWER` for highest priority
|
||||
- Direct ESS register writes can be overridden by subsequent controllers
|
||||
|
||||
**ESS Modes**:
|
||||
- `REMOTE`: External Modbus control active
|
||||
- `INTERNAL`: OpenEMS manages battery
|
||||
- Mode switching via JSON-RPC API on port 8074
|
||||
|
||||
**Modbus Communication**:
|
||||
- IP: 192.168.89.144, Port: 502
|
||||
- Register pairs use 2 consecutive registers for FLOAT32 values
|
||||
- Example: Register 2752/2753 for SET_GRID_ACTIVE_POWER
|
||||
|
||||
### PyScript-Specific Considerations
|
||||
|
||||
**Limitations**:
|
||||
- Generator expressions with `selectattr()` not supported
|
||||
- Use explicit `for` loops instead of complex comprehensions
|
||||
- State values limited to 255 characters; use attributes for complex data
|
||||
|
||||
**Timezone Handling**:
|
||||
- PyScript `datetime.now()` returns UTC
|
||||
- Home Assistant stores times in local (Europe/Berlin)
|
||||
- Always use `datetime.now().astimezone()` for local time
|
||||
- Explicit timezone conversion required when comparing PyScript times with HA states
|
||||
|
||||
**State Management**:
|
||||
```python
|
||||
# Store complex data in attributes, not state value
|
||||
state.set('pyscript.battery_charging_schedule',
|
||||
value='active', # Simple status
|
||||
new_attributes={'schedule': [...]} # Complex data here
|
||||
)
|
||||
```
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Testing PyScript Changes
|
||||
|
||||
```bash
|
||||
# In Home Assistant Developer Tools → Services:
|
||||
service: pyscript.reload
|
||||
```
|
||||
|
||||
### Manual Schedule Calculation
|
||||
|
||||
```bash
|
||||
# In Home Assistant Developer Tools → Services:
|
||||
service: pyscript.calculate_charging_schedule
|
||||
```
|
||||
|
||||
### Manual Execution Test
|
||||
|
||||
```bash
|
||||
# In Home Assistant Developer Tools → Services:
|
||||
service: pyscript.execute_charging_schedule
|
||||
```
|
||||
|
||||
### Check Schedule State
|
||||
|
||||
```bash
|
||||
# In Home Assistant Developer Tools → States, search for:
|
||||
pyscript.battery_charging_schedule
|
||||
```
|
||||
|
||||
### OpenEMS Logs (on BeagleBone)
|
||||
|
||||
```bash
|
||||
tail -f /var/log/openems/openems.log
|
||||
```
|
||||
|
||||
## Key Integration Points
|
||||
|
||||
### Required Home Assistant Entities
|
||||
|
||||
**Input Booleans**:
|
||||
- `input_boolean.battery_optimizer_enabled` - Master enable/disable
|
||||
- `input_boolean.goodwe_manual_control` - Manual vs auto mode
|
||||
- `input_boolean.battery_optimizer_manual_override` - Skip automation
|
||||
|
||||
**Input Numbers**:
|
||||
- `input_number.battery_capacity_kwh` - Battery capacity (10 kWh)
|
||||
- `input_number.battery_optimizer_min_soc` - Minimum SOC (20%)
|
||||
- `input_number.battery_optimizer_max_soc` - Maximum SOC (100%)
|
||||
- `input_number.battery_optimizer_max_charge_power` - Max charge power (5000W)
|
||||
- `input_number.charge_power_battery` - Target charging power
|
||||
|
||||
**Sensors**:
|
||||
- `sensor.esssoc` - Current battery SOC
|
||||
- `sensor.openems_ess0_activepower` - Battery power
|
||||
- `sensor.openems_grid_activepower` - Grid power
|
||||
- `sensor.openems_production_activepower` - PV production
|
||||
- `sensor.energy_production_today` / `sensor.energy_production_today_2` - PV forecast (east/west)
|
||||
- `sensor.energy_production_tomorrow` / `sensor.energy_production_tomorrow_2` - PV tomorrow
|
||||
|
||||
### Existing Automations
|
||||
|
||||
Three manual control automations exist (in Home Assistant automations.yaml):
|
||||
- Battery charge start (ID: `1730457901370`)
|
||||
- Battery charge stop (ID: `1730457994517`)
|
||||
- Battery discharge
|
||||
|
||||
**Important**: These automations are **used by** the optimizer, not replaced. The PyScript sets input helpers that trigger these automations.
|
||||
|
||||
## Dashboard Variants
|
||||
|
||||
Multiple dashboard configurations exist in `v3/`:
|
||||
|
||||
- **Standard** (`battery_optimizer_dashboard.yaml`): Detailed view with all metrics
|
||||
- **Compact** (`battery_optimizer_dashboard_compact.yaml`): Balanced mobile-friendly view
|
||||
- **Minimal** (`battery_optimizer_dashboard_minimal.yaml`): Quick status check
|
||||
- **Sections variants**: Modern HA 2024.2+ layouts with auto-responsive behavior
|
||||
|
||||
All use maximum 4-column layouts for mobile compatibility.
|
||||
|
||||
**Required HACS Custom Cards**:
|
||||
- Mushroom Cards
|
||||
- Bubble Card
|
||||
- Plotly Graph Card
|
||||
- Power Flow Card Plus
|
||||
- Stack-in-Card
|
||||
|
||||
## Common Troubleshooting
|
||||
|
||||
### Battery Not Charging Despite Schedule
|
||||
|
||||
**Symptom**: Schedule shows charging hour but battery stays idle
|
||||
**Causes**:
|
||||
1. Controller priority issue - another controller overriding
|
||||
2. Manual override active (`input_boolean.battery_optimizer_manual_override == on`)
|
||||
3. Optimizer disabled (`input_boolean.battery_optimizer_enabled == off`)
|
||||
|
||||
**Solution**: Check OpenEMS logs for controller execution order, verify input boolean states
|
||||
|
||||
### Wrong Charging Time (Off by Hours)
|
||||
|
||||
**Symptom**: Charging starts at wrong hour
|
||||
**Cause**: UTC/local timezone mismatch in PyScript
|
||||
**Solution**: Verify all datetime operations use `.astimezone()` for local time
|
||||
|
||||
### No Tomorrow Prices in Schedule
|
||||
|
||||
**Symptom**: Schedule only covers today
|
||||
**Cause**: Tomorrow prices not yet available (published at 14:00)
|
||||
**Solution**: Normal before 14:00; if persists after 14:05, check `sensor.hastrom_flex_pro_ext` attributes for `tomorrow_available`
|
||||
|
||||
### Modbus Write Failures
|
||||
|
||||
**Symptom**: Modbus errors in logs when setting power
|
||||
**Cause**: Incorrect FLOAT32 encoding or wrong byte order
|
||||
**Solution**: Verify Big-Endian format in `ess_set_power.py`, check OpenEMS Modbus configuration
|
||||
|
||||
## Data Sources
|
||||
|
||||
**haStrom FLEX PRO API**:
|
||||
- Endpoint: `http://eex.stwhas.de/api/spotprices/flexpro?start_date=YYYYMMDD&end_date=YYYYMMDD`
|
||||
- Price field: `t_price_has_pro_incl_vat` (specific to FLEX PRO tariff)
|
||||
- Supports date range queries for multi-day optimization
|
||||
|
||||
**Forecast.Solar**:
|
||||
- Two arrays configured: East (90°) and West (270°) on flat roof
|
||||
- Daily totals available, hourly breakdown simplified
|
||||
|
||||
**InfluxDB2**:
|
||||
- Long-term storage for historical analysis
|
||||
- Configuration in Home Assistant `configuration.yaml`
|
||||
|
||||
## File Locations in Production
|
||||
|
||||
When this code runs in production Home Assistant:
|
||||
|
||||
```
|
||||
/config/
|
||||
├── pyscripts/
|
||||
│ ├── battery_charging_optimizer.py # Main optimizer
|
||||
│ ├── hastrom_flex_extended.py # Price fetcher
|
||||
│ └── ess_set_power.py # Modbus control
|
||||
├── automations.yaml # Contains battery control automations
|
||||
├── configuration.yaml # Modbus, InfluxDB configs
|
||||
└── dashboards/
|
||||
└── battery_optimizer.yaml # Dashboard config
|
||||
```
|
||||
|
||||
## Algorithm Overview
|
||||
|
||||
**Ranking-Based Optimization** (v3.1.0):
|
||||
1. Calculate needed charging hours: `(target_SOC - current_SOC) × capacity ÷ charge_power`
|
||||
2. Combine today + tomorrow price data into single dataset
|
||||
3. Score each hour: `price - (pv_forecast_wh / 1000)`
|
||||
4. Sort by score (lowest = best)
|
||||
5. Select top N hours where N = needed charging hours
|
||||
6. Execute chronologically
|
||||
|
||||
**Key Insight**: This approach finds globally optimal charging times across midnight boundaries, unlike threshold-based methods that treat days separately.
|
||||
|
||||
## Version History Context
|
||||
|
||||
- **v1**: Threshold-based optimization, single-day planning
|
||||
- **v2**: Enhanced with better dashboard, improved error handling
|
||||
- **v3**: Ranking-based optimization, tomorrow support, modern sections dashboards
|
||||
|
||||
Each version folder contains complete snapshots including installation guides and checklists for that iteration.
|
||||
@@ -1,359 +0,0 @@
|
||||
# EMS mit openEMS und Home Assistant - Projektdokumentation
|
||||
|
||||
## Projektübersicht
|
||||
|
||||
Intelligentes Batterie-Optimierungssystem für eine Wohnanlage mit dynamischer Strompreissteuerung und Solarprognose-Integration.
|
||||
|
||||
### Hardware-Setup
|
||||
- **Batterie**: GoodWe 10 kWh
|
||||
- **Wechselrichter**: 10 kW GoodWe
|
||||
- **PV-Anlage**: 9,2 kWp (Ost-West-Ausrichtung auf Flachdach)
|
||||
- **Steuerung**: BeagleBone mit openEMS
|
||||
- **Kommunikation**: Modbus TCP (IP: 192.168.89.144, Port: 502)
|
||||
|
||||
### Stromtarif & APIs
|
||||
- **Tarif**: haStrom FLEX PRO (dynamische Preise)
|
||||
- **Preisabruf**: Täglich um 14:00 Uhr für Folgetag
|
||||
- **PV-Prognose**: Forecast.Solar API
|
||||
- **API-Feldname**: `t_price_has_pro_incl_vat` (spezifisch für FLEX PRO)
|
||||
|
||||
## Systemarchitektur
|
||||
|
||||
### Komponenten-Übersicht
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Home Assistant + PyScript │
|
||||
│ - Optimierungsalgorithmus (täglich 14:05) │
|
||||
│ - Stündliche Ausführung (xx:05) │
|
||||
│ - Zeitzonenhandling (UTC → Local) │
|
||||
└──────────────────┬──────────────────────────────┘
|
||||
│
|
||||
┌─────────┴─────────┐
|
||||
│ │
|
||||
┌────────▼────────┐ ┌──────▼───────────────┐
|
||||
│ Modbus TCP │ │ JSON-RPC API │
|
||||
│ Port 502 │ │ Port 8074 │
|
||||
│ (Batterie- │ │ (ESS Mode Switch) │
|
||||
│ steuerung) │ │ │
|
||||
└────────┬────────┘ └──────┬───────────────┘
|
||||
│ │
|
||||
└─────────┬─────────┘
|
||||
│
|
||||
┌─────────▼──────────┐
|
||||
│ openEMS │
|
||||
│ - Controller-Prio │
|
||||
│ - ESS Management │
|
||||
│ - GoodWe Integration│
|
||||
└────────────────────┘
|
||||
```
|
||||
|
||||
## Kritische technische Details
|
||||
|
||||
### 1. Modbus-Kommunikation
|
||||
|
||||
#### Register-Format
|
||||
- **Datentyp**: IEEE 754 FLOAT32
|
||||
- **Register-Paare**: Verwenden 2 aufeinanderfolgende Register
|
||||
- **Beispiel**: Register 2752/2753 für SET_GRID_ACTIVE_POWER
|
||||
|
||||
#### Wichtige Register (aus Modbustcp0_3.xlsx)
|
||||
- **Batteriesteuerung**: Register 2752/2753 (SET_GRID_ACTIVE_POWER)
|
||||
- **SOC-Abfrage**: Register für State of Charge
|
||||
- **Leistungswerte**: FLOAT32-codiert
|
||||
|
||||
#### Python-Implementierung für Register-Schreiben
|
||||
```python
|
||||
import struct
|
||||
from pymodbus.client import ModbusTcpClient
|
||||
|
||||
def write_float32_register(client, address, value):
|
||||
"""
|
||||
Schreibt einen FLOAT32-Wert in zwei Modbus-Register
|
||||
"""
|
||||
# FLOAT32 in zwei 16-bit Register konvertieren
|
||||
bytes_value = struct.pack('>f', value) # Big-Endian
|
||||
registers = [
|
||||
int.from_bytes(bytes_value[0:2], 'big'),
|
||||
int.from_bytes(bytes_value[2:4], 'big')
|
||||
]
|
||||
client.write_registers(address, registers)
|
||||
```
|
||||
|
||||
### 2. openEMS Controller-Prioritäten
|
||||
|
||||
#### Kritisches Verhalten
|
||||
- **Alphabetische Ausführungsreihenfolge**: Controller werden alphabetisch sortiert ausgeführt
|
||||
- **Override-Problem**: Spätere Controller können frühere überschreiben
|
||||
- **Lösung**: `ctrlBalancing0` mit SET_GRID_ACTIVE_POWER nutzt
|
||||
|
||||
#### Controller-Hierarchie
|
||||
```
|
||||
1. ctrlBalancing0 (SET_GRID_ACTIVE_POWER) ← HÖCHSTE PRIORITÄT
|
||||
2. Andere Controller (können nicht überschreiben)
|
||||
3. ESS-Register (können überschrieben werden)
|
||||
```
|
||||
|
||||
#### ESS-Modi
|
||||
- **REMOTE**: Externe Steuerung via Modbus aktiv
|
||||
- **INTERNAL**: openEMS-interne Steuerung
|
||||
- **Mode-Switch**: Via JSON-RPC API Port 8074
|
||||
|
||||
### 3. Existierende Home Assistant Automationen
|
||||
|
||||
Drei bewährte Automationen für Batteriesteuerung:
|
||||
|
||||
1. **Batterieladung starten** (ID: `1730457901370`)
|
||||
2. **Batterieladung stoppen** (ID: `1730457994517`)
|
||||
3. **Batterieentladung** (ID: weitere Details erforderlich)
|
||||
|
||||
**Wichtig**: Diese Automationen werden vom Optimierungssystem gesteuert, nicht ersetzt!
|
||||
|
||||
## Optimierungsalgorithmus
|
||||
|
||||
### Strategie
|
||||
- **Konservativ**: Nur in günstigsten Stunden laden
|
||||
- **SOC-Bereich**: 20-100% (2 kWh Reserve für Eigenverbrauch)
|
||||
- **Ranking-basiert**: N günstigste Stunden aus Heute+Morgen
|
||||
- **Mitternachtsoptimierung**: Berücksichtigt Preise über Tageswechsel hinweg
|
||||
|
||||
### Berechnung
|
||||
```python
|
||||
# Benötigte Ladestunden
|
||||
needed_kwh = (target_soc - current_soc) / 100 * battery_capacity
|
||||
needed_hours = needed_kwh / charging_power
|
||||
|
||||
# Sortierung nach Preis
|
||||
combined_prices = today_prices + tomorrow_prices
|
||||
sorted_hours = sorted(combined_prices, key=lambda x: x['price'])
|
||||
|
||||
# N günstigste Stunden auswählen
|
||||
charging_hours = sorted_hours[:needed_hours]
|
||||
```
|
||||
|
||||
### PyScript-Ausführung
|
||||
|
||||
#### Zeitplan
|
||||
- **Optimierung**: Täglich 14:05 (nach Preisveröffentlichung um 14:00)
|
||||
- **Ausführung**: Stündlich xx:05
|
||||
- **Prüfung**: Ist aktuelle Stunde eine Ladestunde?
|
||||
|
||||
#### Zeitzonenhandling
|
||||
```python
|
||||
# PROBLEM: PyScript verwendet UTC
|
||||
from datetime import datetime
|
||||
import pytz
|
||||
|
||||
# UTC datetime von PyScript
|
||||
utc_now = datetime.now()
|
||||
|
||||
# Konvertierung nach deutscher Zeit
|
||||
berlin_tz = pytz.timezone('Europe/Berlin')
|
||||
local_now = utc_now.astimezone(berlin_tz)
|
||||
|
||||
# Speicherung in Home Assistant (local time)
|
||||
state.set('sensor.charging_schedule', attributes={'data': schedule_data})
|
||||
```
|
||||
|
||||
**KRITISCH**: Home Assistant speichert Zeiten in lokaler Zeit, PyScript arbeitet in UTC!
|
||||
|
||||
## Datenquellen & Integration
|
||||
|
||||
### haStrom FLEX PRO API
|
||||
```python
|
||||
# Endpoint-Unterschiede beachten!
|
||||
url = "https://api.hastrom.de/api/prices"
|
||||
params = {
|
||||
'start': '2024-01-01',
|
||||
'end': '2024-01-02' # Unterstützt Datumsbereichsabfragen
|
||||
}
|
||||
|
||||
# Feldname ist spezifisch!
|
||||
price = data['t_price_has_pro_incl_vat'] # Nicht der Standard-Feldname!
|
||||
```
|
||||
|
||||
### Forecast.Solar
|
||||
- **Standortdaten**: Lat/Lon für Ost-West-Arrays
|
||||
- **Dachneigung**: Flachdach-spezifisch
|
||||
- **Azimut**: Ost (90°) und West (270°)
|
||||
|
||||
### InfluxDB2
|
||||
- **Historische Daten**: Langzeit-Speicherung
|
||||
- **Analyse**: Performance-Tracking
|
||||
- **Backup**: Datenredundanz
|
||||
|
||||
## Home Assistant Dashboard
|
||||
|
||||
### Design-Prinzipien
|
||||
- **Maximum 4 Spalten**: Mobile-First Design
|
||||
- **Sections Layout**: Moderne HA 2024.2+ Standard
|
||||
- **Keine verschachtelten Listen**: Flache Hierarchie bevorzugen
|
||||
|
||||
### Verwendete Custom Cards (HACS)
|
||||
- **Mushroom Cards**: Basis-UI-Elemente
|
||||
- **Bubble Card**: Erweiterte Visualisierung
|
||||
- **Plotly Graph Card**: Detaillierte Diagramme
|
||||
- **Power Flow Card Plus**: Energiefluss-Darstellung
|
||||
- **Stack-in-Card**: Layout-Organisation
|
||||
|
||||
### Dashboard-Varianten
|
||||
1. **Minimal**: Schneller Status-Check
|
||||
2. **Standard**: Tägliche Nutzung
|
||||
3. **Detail**: Analyse und Debugging
|
||||
|
||||
## PyScript-Besonderheiten
|
||||
|
||||
### Bekannte Limitierungen
|
||||
```python
|
||||
# NICHT UNTERSTÜTZT in PyScript:
|
||||
# - Generator Expressions mit selectattr()
|
||||
result = [x for x in items if x.attr == value] # ✗
|
||||
|
||||
# STATTDESSEN:
|
||||
result = []
|
||||
for x in items:
|
||||
if x.attr == value:
|
||||
result.append(x) # ✓
|
||||
```
|
||||
|
||||
### State vs. Attributes
|
||||
```python
|
||||
# State Value: Max 255 Zeichen
|
||||
state.set('sensor.status', 'charging')
|
||||
|
||||
# Attributes: Unbegrenzt (JSON)
|
||||
state.set('sensor.schedule',
|
||||
value='active',
|
||||
attributes={
|
||||
'schedule': complete_schedule_dict, # ✓ Kann sehr groß sein
|
||||
'prices': all_price_data
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Debugging & Monitoring
|
||||
|
||||
### Logging-Strategie
|
||||
```python
|
||||
# PyScript Logging
|
||||
log.info(f"Optimization completed: {len(charging_hours)} hours")
|
||||
log.debug(f"Price data: {prices}")
|
||||
log.error(f"Modbus connection failed: {error}")
|
||||
```
|
||||
|
||||
### Transparenz
|
||||
- **Geplant vs. Ausgeführt**: Vergleich in Logs
|
||||
- **Controller-Execution**: openEMS-Logs prüfen
|
||||
- **Modbus-Traffic**: Register-Scan-Tools
|
||||
|
||||
### Typische Probleme
|
||||
|
||||
#### Problem 1: Controller Override
|
||||
**Symptom**: Batterie lädt nicht trotz Befehl
|
||||
**Ursache**: Anderer Controller überschreibt SET_GRID_ACTIVE_POWER
|
||||
**Lösung**: ctrlBalancing0 verwenden, nicht direkte ESS-Register
|
||||
|
||||
#### Problem 2: Zeitzone-Mismatch
|
||||
**Symptom**: Ladung startet zur falschen Stunde
|
||||
**Ursache**: UTC/Local-Zeit-Verwechslung
|
||||
**Lösung**: Explizite Timezone-Konvertierung in PyScript
|
||||
|
||||
#### Problem 3: FLOAT32-Encoding
|
||||
**Symptom**: Falsche Werte in Modbus-Registern
|
||||
**Ursache**: Falsche Byte-Reihenfolge
|
||||
**Lösung**: Big-Endian IEEE 754 verwenden
|
||||
|
||||
## Datei-Struktur
|
||||
|
||||
```
|
||||
/config/
|
||||
├── pyscripts/
|
||||
│ ├── battery_optimizer.py # Hauptoptimierung
|
||||
│ └── helpers/
|
||||
│ ├── modbus_client.py # Modbus-Funktionen
|
||||
│ └── price_fetcher.py # API-Aufrufe
|
||||
├── automations/
|
||||
│ ├── battery_charge_start.yaml
|
||||
│ ├── battery_charge_stop.yaml
|
||||
│ └── battery_discharge.yaml
|
||||
├── dashboards/
|
||||
│ ├── energy_overview.yaml # Hauptdashboard
|
||||
│ └── energy_detail.yaml # Detail-Analyse
|
||||
└── configuration.yaml # InfluxDB, Modbus, etc.
|
||||
```
|
||||
|
||||
## Nächste Schritte & Erweiterungen
|
||||
|
||||
### Kurzfristig
|
||||
- [ ] Dashboard-Feinschliff (Sections Layout)
|
||||
- [ ] Logging-Verbesserungen
|
||||
- [ ] Performance-Monitoring
|
||||
|
||||
### Mittelfristig
|
||||
- [ ] Erweiterte Algorithmen (ML-basiert?)
|
||||
- [ ] Wetterprognose-Integration
|
||||
- [ ] Community-Sharing vorbereiten
|
||||
|
||||
### Langfristig
|
||||
- [ ] Multi-Tarif-Support
|
||||
- [ ] V2G-Integration (Vehicle-to-Grid)
|
||||
- [ ] Peer-to-Peer-Energiehandel
|
||||
|
||||
## Wichtige Ressourcen
|
||||
|
||||
### Dokumentation
|
||||
- openEMS Docs: https://openems.github.io/openems.io/
|
||||
- Home Assistant Modbus: https://www.home-assistant.io/integrations/modbus/
|
||||
- PyScript Docs: https://hacs-pyscript.readthedocs.io/
|
||||
|
||||
### Tools
|
||||
- Modbus-Scanner: Für Register-Mapping
|
||||
- Excel-Export: openEMS Register-Dokumentation (Modbustcp0_3.xlsx)
|
||||
- HA Logs: `/config/home-assistant.log`
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
1. **Controller-Priorität ist kritisch**: Immer höchsten verfügbaren Channel nutzen
|
||||
2. **Zeitzone-Handling explizit**: UTC/Local nie vermischen
|
||||
3. **API-Spezifikationen prüfen**: Feldnamen können abweichen (haStrom!)
|
||||
4. **PyScript-Limitierungen kennen**: Keine komplexen Comprehensions
|
||||
5. **Attributes > State**: Für komplexe Datenstrukturen
|
||||
6. **Bestehende Automationen nutzen**: Nicht neuerfinden
|
||||
7. **Transparent loggen**: Nachvollziehbarkeit ist Schlüssel
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference - Häufige Befehle
|
||||
|
||||
### Modbus Register auslesen
|
||||
```bash
|
||||
# Via HA Developer Tools > States
|
||||
sensor.openems_battery_soc
|
||||
sensor.openems_grid_power
|
||||
```
|
||||
|
||||
### PyScript neu laden
|
||||
```bash
|
||||
# HA Developer Tools > Services
|
||||
service: pyscript.reload
|
||||
```
|
||||
|
||||
### openEMS Logs prüfen
|
||||
```bash
|
||||
# Auf BeagleBone
|
||||
tail -f /var/log/openems/openems.log
|
||||
```
|
||||
|
||||
### Manueller Ladetest
|
||||
```yaml
|
||||
# HA Developer Tools > Services
|
||||
service: automation.trigger
|
||||
target:
|
||||
entity_id: automation.batterieladung_starten
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Version**: 1.0
|
||||
**Letzte Aktualisierung**: November 2024
|
||||
**Autor**: Felix
|
||||
**Status**: Produktiv im Einsatz
|
||||
@@ -1,26 +0,0 @@
|
||||
## Purpose & context
|
||||
Felix is developing an advanced Home Assistant battery optimization system for his residential energy setup, which includes a 10 kWh GoodWe battery, 10 kW inverter, and 9.2 kWp PV installation split between east and west orientations on a flat roof. The system integrates with OpenEMS energy management software running on a BeagleBone single-board computer, using dynamic electricity pricing from haStrom FLEX PRO tariff and Forecast.Solar for PV predictions. The primary goal is intelligent automated battery charging that schedules charging during the cheapest electricity price periods while considering solar forecasts and maintaining optimal battery management.
|
||||
The project represents a sophisticated energy optimization approach that goes beyond simple time-of-use scheduling, incorporating real-time pricing data (available daily at 14:00 for next-day optimization), weather forecasting, and cross-midnight optimization capabilities. Felix has demonstrated strong technical expertise throughout the development process, providing corrections and improvements to initial implementations, and has expressed interest in eventually sharing this project with the Home Assistant community.
|
||||
|
||||
## Current state
|
||||
The battery optimization system is operational with comprehensive PyScript-based automation that calculates daily charging schedules at 14:05 and executes hourly at xx:05. The system successfully integrates multiple data sources: haStrom FLEX PRO API for dynamic pricing, Forecast.Solar for PV forecasting, and OpenEMS Modbus sensors for battery monitoring. Recent work focused on dashboard optimization, moving from cluttered multi-column layouts to clean 4-column maximum designs using both traditional Home Assistant layouts and modern sections-based approaches.
|
||||
Key technical challenges have been resolved, including timezone mismatches between PyScript's UTC datetime handling and local German time storage, proper Modbus communication with FLOAT32 register handling, and controller priority conflicts in OpenEMS where balancing controllers were overriding manual charging commands. The system now uses proven manual control infrastructure with three existing automations for battery control via Modbus communication, switching between REMOTE and INTERNAL ESS modes as needed.
|
||||
|
||||
## On the horizon
|
||||
Felix is working on dashboard refinements using the new Home Assistant Sections layout, which represents the modern standard for dashboard creation in Home Assistant 2024.2+. The sections-based approach provides better organization and automatic responsive behavior compared to traditional horizontal/vertical stack configurations. Multiple dashboard variants have been created with different complexity levels to accommodate various use cases from quick status checks to detailed analysis.
|
||||
Future considerations include expanding the optimization algorithm's sophistication and potentially integrating additional data sources or control mechanisms. The system architecture is designed to be extensible, with clear separation between optimization logic, data collection, and execution components.
|
||||
|
||||
## Key learnings & principles
|
||||
Critical technical insights emerged around OpenEMS controller priority and execution order. The system uses alphabetical scheduling where controllers execute in sequence, and later controllers can override earlier ones. Manual battery control requires careful attention to controller hierarchy - using ctrlBalancing0's SET_GRID_ACTIVE_POWER channel provides highest priority and prevents override by other controllers, while direct ESS register writes can be overridden by subsequent controller execution.
|
||||
PyScript integration has specific limitations that require workarounds: generator expressions and list comprehensions with selectattr() are not supported and must be replaced with explicit for loops. Home Assistant state attributes can store unlimited JSON data while state values are limited to 255 characters, making attributes ideal for complex scheduling data storage.
|
||||
Timezone handling requires careful consideration when mixing PyScript's UTC datetime.now() with local time storage. The haStrom FLEX PRO API uses different field names (t_price_has_pro_incl_vat) than standard endpoints and supports efficient date range queries for multi-day optimization across midnight boundaries.
|
||||
|
||||
## Approach & patterns
|
||||
The system follows a conservative optimization strategy, charging only during the cheapest price periods while maintaining battery SOC between 20-100% with a 2 kWh reserve for self-consumption. The optimization algorithm uses ranking-based selection rather than threshold-based approaches, calculating needed charging hours based on battery capacity and selecting the N cheapest hours from combined today-plus-tomorrow datasets.
|
||||
Development follows a systematic troubleshooting approach with comprehensive logging and debugging capabilities. Felix emphasizes transparent operation where the system can verify planned versus actual charging execution. The architecture separates concerns cleanly: PyScript handles optimization calculations and scheduling, existing Home Assistant automations manage physical battery control, and Modbus communication provides the interface layer to OpenEMS.
|
||||
Dashboard design prioritizes readability and mobile compatibility with maximum 4-column layouts, using Mushroom Cards and custom components like Bubble Card, Plotly Graph Card, and Power Flow Card Plus for enhanced visualization.
|
||||
|
||||
## Tools & resources
|
||||
The system integrates multiple specialized components: OpenEMS for energy management with GoodWe ESS integration, InfluxDB2 for historical data storage, haStrom FLEX PRO API for dynamic electricity pricing, and Forecast.Solar for PV generation forecasting. Home Assistant serves as the central automation platform with PyScript for complex logic implementation.
|
||||
Technical infrastructure includes Modbus TCP communication on port 502 (IP 192.168.89.144), OpenEMS JSON-RPC API on port 8074 for ESS mode switching, and proper IEEE 754 FLOAT32 encoding for register value conversion. The system uses HACS custom components including Bubble Card, Plotly Graph Card, Power Flow Card Plus, and Stack-in-Card for enhanced dashboard functionality.
|
||||
Development tools include Python-based Modbus register scanning utilities, comprehensive logging systems for debugging controller execution, and Excel exports from OpenEMS for register mapping verification.
|
||||
@@ -3,7 +3,10 @@ Battery Charging Optimizer für OpenEMS + GoodWe
|
||||
Nutzt das bestehende manuelle Steuerungssystem
|
||||
|
||||
Speicherort: /config/pyscript/battery_charging_optimizer.py
|
||||
Version: 3.3.1 - FIXED: SOC-Plausibilitäts-Check (filtert 65535% Spikes beim Modus-Wechsel)
|
||||
Version: 3.5.0 - REMOVED: Sicherheitspuffer und Reservekapazität (Hardware hat eigene Puffer)
|
||||
Batterie lädt jetzt bis 100% SOC
|
||||
CHANGED: Standardwerte - Preisschwelle 25ct, Ladeleistung 8000W
|
||||
FIXED: SOC-Plausibilitäts-Check (filtert 65535% Spikes beim Modus-Wechsel)
|
||||
Setzt charge_power_battery als negativen Wert (Laden = negativ)
|
||||
Nutzt bestehende Automations für ESS-Modus und Keep-Alive
|
||||
"""
|
||||
@@ -29,7 +32,7 @@ def calculate_charging_schedule():
|
||||
Nutzt Ranking-Methode: Wählt die N günstigsten Stunden aus
|
||||
"""
|
||||
|
||||
log.info("=== Batterie-Optimierung gestartet (v3.2 - FIXED Timezones) ===")
|
||||
log.info("=== Batterie-Optimierung gestartet (v3.5.0 - Volle Ladung bis 100%) ===")
|
||||
|
||||
# Prüfe ob Optimierung aktiviert ist
|
||||
if state.get('input_boolean.battery_optimizer_enabled') != 'on':
|
||||
@@ -103,9 +106,8 @@ def load_configuration():
|
||||
'battery_capacity': float(state.get('input_number.battery_capacity_kwh') or 10) * 1000, # in Wh
|
||||
'min_soc': float(state.get('input_number.battery_optimizer_min_soc') or 20),
|
||||
'max_soc': float(state.get('input_number.battery_optimizer_max_soc') or 100),
|
||||
'max_charge_power': float(state.get('input_number.battery_optimizer_max_charge_power') or 5000),
|
||||
'price_threshold': float(state.get('input_number.battery_optimizer_price_threshold') or 28),
|
||||
'reserve_capacity': float(state.get('input_number.battery_optimizer_reserve_capacity') or 2) * 1000, # in Wh
|
||||
'max_charge_power': float(state.get('input_number.battery_optimizer_max_charge_power') or 8000),
|
||||
'price_threshold': float(state.get('input_number.battery_optimizer_price_threshold') or 25),
|
||||
'pv_threshold': float(state.get('input_number.battery_optimizer_pv_threshold') or 500), # in Wh
|
||||
}
|
||||
|
||||
@@ -287,15 +289,14 @@ def optimize_charging(price_data, pv_forecast, current_soc, config):
|
||||
avg_price = sum(all_prices) / len(all_prices)
|
||||
log.info(f"Preise: Min={min_price:.2f}, Max={max_price:.2f}, Avg={avg_price:.2f} ct/kWh")
|
||||
|
||||
# Verfügbare Ladekapazität berechnen
|
||||
# Verfügbare Ladekapazität berechnen (OHNE Reserven - Hardware hat eigene Puffer)
|
||||
available_capacity_wh = (config['max_soc'] - current_soc) / 100 * config['battery_capacity']
|
||||
available_capacity_wh -= config['reserve_capacity']
|
||||
|
||||
if available_capacity_wh <= 0:
|
||||
log.info("Batterie ist voll oder Reserve erreicht - keine Ladung nötig")
|
||||
log.info("Batterie ist bereits voll - keine Ladung nötig")
|
||||
return create_auto_only_schedule(future_price_data)
|
||||
|
||||
log.info(f"Verfügbare Ladekapazität: {available_capacity_wh/1000:.2f} kWh")
|
||||
log.info(f"Verfügbare Ladekapazität: {available_capacity_wh/1000:.2f} kWh (bis {config['max_soc']}% SOC)")
|
||||
|
||||
# ==========================================
|
||||
# Berechne benötigte Ladestunden
|
||||
@@ -628,6 +629,13 @@ def execute_charging_schedule():
|
||||
log.info(f" Grund: {reason}")
|
||||
|
||||
if action == 'charge':
|
||||
# Prüfe aktuellen SOC beim Ladestart
|
||||
current_soc_now = float(state.get('sensor.esssoc') or 50)
|
||||
if current_soc_now > 100 or current_soc_now < 0:
|
||||
log.warning(f"⚠ Ungültiger SOC beim Ladestart: {current_soc_now}%. Verwende trotzdem geplante Leistung.")
|
||||
else:
|
||||
log.info(f"📊 SOC beim Ladestart: {current_soc_now}%")
|
||||
|
||||
log.info(f"🔋 AKTIVIERE LADEN mit {abs(power_w)}W")
|
||||
|
||||
# Setze Ziel-Leistung als NEGATIVEN Wert (Laden = negativ)
|
||||
|
||||
Reference in New Issue
Block a user