Update: Battery Optimizer v3.4.0 mit allen Fixes und Features

This commit is contained in:
felix.zoesch
2025-12-12 08:20:19 +01:00
commit d2a41aad2d
78 changed files with 18053 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
# ✅ Entitäten-Checkliste
## Nach Installation sollten folgende Entitäten existieren:
### Input Boolean (2 Stück)
- [ ] `input_boolean.battery_optimizer_enabled`
- [ ] `input_boolean.battery_optimizer_manual_override`
### Input Number (7 Stück)
- [ ] `input_number.battery_capacity_kwh`
- [ ] `input_number.battery_optimizer_min_soc`
- [ ] `input_number.battery_optimizer_max_soc`
- [ ] `input_number.battery_optimizer_max_charge_power`
- [ ] `input_number.battery_optimizer_price_threshold`
- [ ] `input_number.battery_optimizer_reserve_capacity`
- [ ] `input_number.battery_optimizer_pv_threshold`
### Input Text (1 Stück)
- [ ] `input_text.battery_optimizer_status`
### Template Sensors (3 Stück) - werden AUTOMATISCH erstellt
Diese Sensoren werden erst nach Home Assistant Neustart und Laden der Templates erstellt:
- [ ] `sensor.battery_charging_plan_status`
- [ ] `sensor.battery_next_action`
- [ ] `sensor.battery_estimated_savings`
**Hinweis:** Template Sensors zeigen "unavailable" bis der erste Plan berechnet wurde!
### PyScript States (1 Stück) - wird AUTOMATISCH erstellt
Dieser State wird beim ersten Aufruf von `calculate_charging_schedule()` erstellt:
- [ ] `pyscript.battery_charging_schedule`
### Bestehende Entitäten (müssen bereits vorhanden sein)
- [ ] `input_boolean.goodwe_manual_control` (dein bestehendes System)
- [ ] `input_number.charge_power_battery` (dein bestehendes System)
- [ ] `sensor.openems_ess0_soc` (OpenEMS Modbus)
- [ ] `sensor.hastrom_flex_pro` (Strompreis-Sensor)
- [ ] `sensor.energy_production_today` (Forecast.Solar Ost)
- [ ] `sensor.energy_production_today_2` (Forecast.Solar West)
- [ ] `sensor.energy_production_tomorrow` (Forecast.Solar Ost)
- [ ] `sensor.energy_production_tomorrow_2` (Forecast.Solar West)
## Prüfen nach Installation
### Schritt 1: Input Helper prüfen
```
Einstellungen → Geräte & Dienste → Helfer
```
Suche nach "battery_optimizer" - sollte 10 Einträge zeigen
### Schritt 2: Template Sensors prüfen
```
Entwicklerwerkzeuge → Zustände
```
Suche nach "battery_" - Template Sensors sollten existieren (können "unavailable" sein)
### Schritt 3: Ersten Plan berechnen
```
Entwicklerwerkzeuge → Dienste
service: pyscript.calculate_charging_schedule
```
### Schritt 4: PyScript State prüfen
```
Entwicklerwerkzeuge → Zustände
```
Suche nach "pyscript.battery_charging_schedule" - sollte jetzt existieren!
### Schritt 5: Template Sensors sollten jetzt Werte haben
```
Entwicklerwerkzeuge → Zustände
```
- `sensor.battery_charging_plan_status` sollte z.B. "3 Ladungen geplant" zeigen
- `sensor.battery_next_action` sollte nächste Aktion zeigen
- `sensor.battery_estimated_savings` zeigt Ersparnis (oder 0)
## Fehlende Entitäten beheben
### Wenn Input Helper fehlen:
1. Prüfe ob `battery_optimizer_config.yaml` richtig in `/config/packages/` liegt
2. Prüfe ob in `configuration.yaml` Packages aktiviert sind:
```yaml
homeassistant:
packages: !include_dir_named packages
```
3. Home Assistant neu starten
4. Logs prüfen auf Fehler
### Wenn Template Sensors fehlen:
1. Prüfe ob der `template:` Abschnitt in der Config ist
2. Home Assistant neu starten
3. Yaml-Konfiguration prüfen in Developer Tools
### Wenn PyScript State fehlt:
1. PyScript muss installiert sein
2. `battery_optimizer.py` muss in `/config/pyscript/` sein
3. Dienst `pyscript.calculate_charging_schedule` manuell aufrufen
4. Logs prüfen auf Fehler

377
legacy/v2/INSTALLATION.md Normal file
View File

@@ -0,0 +1,377 @@
# 🚀 Battery Charging Optimizer - Installations-Anleitung
## Übersicht
Dieses System optimiert die Batterieladung basierend auf:
- ✅ Dynamischen Strompreisen (haStrom FLEX PRO)
- ✅ PV-Prognose (Forecast.Solar Ost/West)
- ✅ Batterie-SOC und Kapazität
- ✅ Haushalts-Reserve
**Besonderheit:** Nutzt dein bestehendes, bewährtes manuelles Steuerungssystem!
---
## 📋 Voraussetzungen
### Bereits vorhanden (✅):
1. **PyScript Integration** installiert und aktiviert
2. **OpenEMS** läuft auf BeagleBone mit GoodWe Batterie
3. **Modbus TCP Integration** für OpenEMS
4. **haStrom FLEX PRO** Sensor für Strompreise
5. **Forecast.Solar** Integration für PV-Prognose (Ost + West)
6. **Funktionierendes manuelles System**:
- `pyscript/ess_set_power.py`
- 3 Automationen für manuelles Laden
- REST Commands für Mode-Wechsel
- Input Helper `goodwe_manual_control` und `charge_power_battery`
### Zu installieren:
- Neue Konfigurationsdateien
- Neues Optimierungs-Script
- Dashboard (optional)
---
## 🔧 Installation
### Schritt 1: Konfiguration installieren
**Methode A: Als Package (empfohlen)**
1. Erstelle Verzeichnis `/config/packages/` falls nicht vorhanden
2. Kopiere `battery_optimizer_config.yaml` nach `/config/packages/`
3. In `configuration.yaml` sicherstellen:
```yaml
homeassistant:
packages: !include_dir_named packages
```
**Methode B: In configuration.yaml**
Kopiere den Inhalt von `battery_optimizer_config.yaml` in die entsprechenden Sektionen deiner `configuration.yaml`.
### Schritt 2: PyScript installieren
1. Kopiere `battery_optimizer.py` nach `/config/pyscript/`
2. **Wichtig:** Dein bestehendes `ess_set_power.py` bleibt unverändert!
### Schritt 3: Home Assistant neu starten
```bash
# Developer Tools → YAML → Restart
```
Oder über CLI:
```bash
ha core restart
```
### Schritt 4: Input Helper prüfen
Nach dem Neustart, gehe zu **Einstellungen → Geräte & Dienste → Helfer**
Folgende Helper sollten jetzt existieren:
- `input_boolean.battery_optimizer_enabled`
- `input_boolean.battery_optimizer_manual_override`
- `input_number.battery_capacity_kwh`
- `input_number.battery_optimizer_min_soc`
- `input_number.battery_optimizer_max_soc`
- `input_number.battery_max_charge_power`
- `input_number.battery_optimizer_price_threshold`
- `input_number.battery_reserve_capacity_kwh`
- `input_number.battery_optimizer_pv_threshold`
- `input_text.battery_optimizer_status`
**Falls nicht:** Prüfe die Konfiguration und Logs!
### Schritt 5: PyScript-Dienste prüfen
Gehe zu **Entwicklerwerkzeuge → Dienste**
Suche nach `pyscript` - folgende Dienste sollten vorhanden sein:
- `pyscript.calculate_charging_schedule`
- `pyscript.execute_charging_schedule`
- `pyscript.ess_set_power` (bestehend)
**Falls nicht:**
- Prüfe `/config/pyscript/battery_optimizer.py` existiert
- Prüfe Logs: `custom_components.pyscript`
---
## ⚙️ Konfiguration
### Batterie-Parameter einstellen
Gehe zu **Einstellungen → Geräte & Dienste → Helfer** und setze:
1. **Batterie-Kapazität**: `10 kWh` (GoodWe)
2. **Min. SOC**: `20%` (Schutz vor Tiefentladung)
3. **Max. SOC**: `100%` (oder z.B. 90% für Langlebigkeit)
4. **Max. Ladeleistung**: `5000W` (5 kW - dein System-Limit)
### Optimierungs-Parameter anpassen
1. **Preis-Schwellwert**: `28 ct/kWh` (Startwert, wird automatisch angepasst)
2. **Reserve-Kapazität**: `2 kWh` (Reserve für Haushalt)
3. **PV-Schwellwert**: `500 Wh` (Bei mehr PV keine Netz-Ladung)
### Sensor-Namen prüfen
Falls deine Sensoren andere Namen haben, passe diese in `battery_optimizer.py` an:
```python
# Zeile ~100
sensor_east = 'sensor.energy_production_today' # Dein Ost-Array
sensor_west = 'sensor.energy_production_today_2' # Dein West-Array
# Zeile ~35
price_entity = 'sensor.hastrom_flex_pro' # Dein Strompreis-Sensor
# Zeile ~185
current_soc = float(state.get('sensor.openems_ess0_soc') or 50) # Dein SOC-Sensor
```
---
## 🧪 Testen
### Test 1: Manuelle Berechnung
```yaml
# Entwicklerwerkzeuge → Dienste
service: pyscript.calculate_charging_schedule
```
**Erwartetes Ergebnis:**
- Log-Einträge in **Einstellungen → System → Protokolle**
- State `pyscript.battery_charging_schedule` existiert
- Attribute `schedule` enthält Array mit Stunden
**Prüfen in Developer Tools → Zustände:**
```
pyscript.battery_charging_schedule
Attributes:
schedule: [...]
last_update: 2025-11-09T17:30:00
num_hours: 24
num_charges: 3
```
### Test 2: Plan ansehen
```yaml
# Entwicklerwerkzeuge → Template
{{ state_attr('pyscript.battery_charging_schedule', 'schedule') }}
```
Sollte eine Liste mit Stunden-Einträgen zeigen:
```json
[
{
"datetime": "2025-11-09T23:00:00",
"hour": 23,
"action": "charge",
"power_w": -5000,
"price": 26.85,
"reason": "Günstig (26.85 ct) + wenig PV (0 Wh)"
},
...
]
```
### Test 3: Manuelle Ausführung
**ACHTUNG:** Nur testen wenn Batterie geladen werden kann!
```yaml
# Entwicklerwerkzeuge → Dienste
service: pyscript.execute_charging_schedule
```
**Was passiert:**
- Wenn aktuelle Stunde = Ladezeit → Aktiviert `input_boolean.goodwe_manual_control`
- Wenn nicht → Deaktiviert manuellen Modus
**Prüfen:**
- Log-Einträge zeigen welche Aktion ausgeführt wird
- Bei Ladung: `goodwe_manual_control` schaltet auf "on"
- Deine bestehende Automation übernimmt → Batterie lädt!
### Test 4: Automatische Zeitsteuerung warten
Die Zeit-Trigger sind aktiv:
- **14:05 Uhr täglich**: Neue Berechnung
- **xx:05 Uhr stündlich**: Ausführung
Warte bis zur nächsten vollen Stunde + 5 Min und prüfe Logs!
---
## 📊 Dashboard installieren (Optional)
1. Gehe zu deinem Lovelace Dashboard
2. **Bearbeiten** → **Raw-Konfigurations-Editor** (3 Punkte oben rechts)
3. Füge den Inhalt von `battery_optimizer_dashboard.yaml` ein
**Oder:** Manuell Cards erstellen über die UI
---
## 🔍 Troubleshooting
### Problem: "Keine Strompreis-Daten"
**Lösung:**
1. Prüfe Sensor `sensor.hastrom_flex_pro` existiert
2. Prüfe Attribut `hours` ist vorhanden:
```yaml
# Developer Tools → Zustände
sensor.hastrom_flex_pro
Attributes:
hours: [...]
```
3. Falls anderer Name → Anpassen in `battery_optimizer.py` Zeile ~35
### Problem: "Keine PV-Prognose"
**Lösung:**
1. Prüfe Forecast.Solar Sensoren existieren:
- `sensor.energy_production_today`
- `sensor.energy_production_today_2`
- `sensor.energy_production_tomorrow`
- `sensor.energy_production_tomorrow_2`
2. Falls andere Namen → Anpassen in `battery_optimizer.py` Zeile ~100
### Problem: PyScript-Dienste nicht sichtbar
**Lösung:**
1. Prüfe PyScript ist installiert: **HACS → Integrationen → PyScript**
2. Prüfe `/config/pyscript/battery_optimizer.py` existiert
3. Home Assistant neu starten
4. Logs prüfen: `grep pyscript /config/home-assistant.log`
### Problem: Batterie lädt nicht trotz Plan
**Lösung:**
1. Prüfe `input_boolean.battery_optimizer_enabled` ist "on"
2. Prüfe `input_boolean.battery_optimizer_manual_override` ist "off"
3. Prüfe deine bestehenden Automationen sind aktiv:
- "Switch: Manuelle Speicherbeladung aktivieren"
- "Automation: Speicher manuell laden"
4. Manuell testen:
```yaml
service: input_boolean.turn_on
target:
entity_id: input_boolean.goodwe_manual_control
```
→ Sollte Ladung starten über deine Automation!
### Problem: "Keine Daten für aktuelle Stunde"
**Ursache:** Plan wurde zu spät erstellt oder enthält nicht alle Stunden
**Lösung:**
1. Plan manuell neu erstellen:
```yaml
service: pyscript.calculate_charging_schedule
```
2. Prüfe ob tägliche Automation um 14:05 läuft
3. Plan sollte **aktuelle Stunde inkludieren**
---
## 🎓 Wie das System funktioniert
### Täglicher Ablauf
**14:05 Uhr:**
1. PyScript berechnet optimalen Ladeplan für nächste 24-36h
2. Berücksichtigt:
- Strompreise (dynamischer Schwellwert = 90. Perzentil)
- PV-Prognose Ost + West kombiniert
- Aktueller Batterie-SOC
- Konfigurierte Parameter
3. Speichert Plan als `pyscript.battery_charging_schedule` State
**Jede Stunde um xx:05:**
1. PyScript prüft was für aktuelle Stunde geplant ist
2. **Wenn "charge":**
- Setzt `input_number.charge_power_battery` auf Ziel-Leistung
- Aktiviert `input_boolean.goodwe_manual_control`
- Deine Automation übernimmt → Schreibt alle 30s via Modbus
3. **Wenn "auto":**
- Deaktiviert `goodwe_manual_control`
- System läuft im Automatik-Modus
### Strategie-Logik
**Laden wenn:**
- Strompreis < dynamischer Schwellwert (90. Perzentil)
- UND PV-Prognose < 500 Wh für diese Stunde
- UND Batterie nicht voll
- Sortiert nach: Niedrigster Preis zuerst
**Nicht laden wenn:**
- Preis zu hoch
- Genug PV-Erzeugung erwartet
- Batterie voll (inkl. Reserve)
---
## 🚀 Nächste Schritte
### Nach erfolgreicher Installation:
1. **Erste Woche beobachten**
- Prüfe Logs täglich
- Verifiziere dass Plan erstellt wird (14:05)
- Verifiziere dass Ausführung läuft (stündlich)
- Prüfe ob Batterie zur richtigen Zeit lädt
2. **Parameter optimieren**
- Preis-Schwellwert anpassen falls zu oft/selten lädt
- PV-Schwellwert anpassen basierend auf Erfahrung
- Reserve-Kapazität optimieren
3. **Statistiken sammeln**
- Notiere Einsparungen
- Vergleiche mit vorherigem Verbrauch
- Dokumentiere für Community
4. **Community-Veröffentlichung vorbereiten**
- Anonymisiere IP-Adressen und Passwörter
- Erstelle README mit deinen Erfahrungen
- Screenshots vom Dashboard
- Beispiel-Logs
---
## 📝 Wartung
### Regelmäßig prüfen:
- Logs auf Fehler durchsuchen
- Plan-Qualität bewerten (gute Vorhersagen?)
- Sensor-Verfügbarkeit (Strompreis, PV-Forecast)
### Bei Problemen:
1. Logs prüfen: `custom_components.pyscript`
2. Sensor-Zustände prüfen
3. Manuell Plan neu berechnen
4. Bei Bedarf Parameter anpassen
---
## 🎉 Fertig!
Dein intelligentes Batterie-Optimierungs-System ist jetzt installiert und nutzt dein bewährtes manuelles Steuerungssystem als solide Basis.
**Das System wird:**
- ✅ Automatisch täglich planen (14:05)
- ✅ Automatisch stündlich ausführen (xx:05)
- ✅ Zu günstigsten Zeiten laden
- ✅ PV-Eigenverbrauch maximieren
- ✅ Stromkosten minimieren
**Viel Erfolg! ⚡💰**

View File

@@ -0,0 +1,551 @@
"""
Battery Charging Optimizer für OpenEMS + GoodWe
Nutzt das bestehende manuelle Steuerungssystem
Speicherort: /config/pyscript/battery_optimizer.py
Version: 2.0.0
"""
import json
from datetime import datetime, timedelta
@service
def calculate_charging_schedule():
"""
Berechnet den optimalen Ladeplan für die nächsten 24-36 Stunden
Wird täglich um 14:05 Uhr nach Strompreis-Update ausgeführt
"""
log.info("=== Batterie-Optimierung gestartet ===")
# Prüfe ob Optimierung aktiviert ist
if state.get('input_boolean.battery_optimizer_enabled') != 'on':
log.info("Optimierung ist deaktiviert")
input_text.battery_optimizer_status = "Deaktiviert"
return
try:
# Konfiguration laden
config = load_configuration()
log.info(f"Konfiguration geladen: SOC {config['min_soc']}-{config['max_soc']}%, Max {config['max_charge_power']}W")
# Strompreise laden
price_data = get_electricity_prices()
if not price_data:
log.error("Keine Strompreis-Daten verfügbar")
input_text.battery_optimizer_status = "Fehler: Keine Preisdaten"
return
log.info(f"Strompreise geladen: {len(price_data)} Stunden")
# PV-Prognose laden
pv_forecast = get_pv_forecast()
pv_today = pv_forecast.get('today', 0)
pv_tomorrow = pv_forecast.get('tomorrow', 0)
log.info(f"PV-Prognose: Heute {pv_today} kWh, Morgen {pv_tomorrow} kWh")
# Batterie-Status laden
current_soc = float(state.get('sensor.openems_ess0_soc') or 50)
log.info(f"Aktueller SOC: {current_soc}%")
# Optimierung durchführen
schedule = optimize_charging(
price_data=price_data,
pv_forecast=pv_forecast,
current_soc=current_soc,
config=config
)
# Plan speichern
save_schedule(schedule)
# Statistiken ausgeben
log_statistics(schedule, price_data)
log.info("=== Optimierung abgeschlossen ===")
except Exception as e:
log.error(f"Fehler bei Optimierung: {e}")
input_text.battery_optimizer_status = f"Fehler: {str(e)[:100]}"
def load_configuration():
"""Lädt alle Konfigurations-Parameter aus Input Helpern"""
return {
'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
'pv_threshold': float(state.get('input_number.battery_optimizer_pv_threshold') or 500), # in Wh
}
def get_electricity_prices():
"""
Holt Strompreise von haStrom FLEX PRO
Erwartet Attribute 'prices_today', 'datetime_today' (und optional tomorrow)
"""
from datetime import datetime
price_entity = 'sensor.hastrom_flex_pro'
prices_attr = state.getattr(price_entity)
if not prices_attr:
log.error(f"Sensor {price_entity} nicht gefunden")
return None
# Heute
prices_today = prices_attr.get('prices_today', [])
datetime_today = prices_attr.get('datetime_today', [])
# Morgen (falls verfügbar)
prices_tomorrow = prices_attr.get('prices_tomorrow', [])
datetime_tomorrow = prices_attr.get('datetime_tomorrow', [])
if not prices_today or not datetime_today:
log.error(f"Keine Preis-Daten in {price_entity} (prices_today oder datetime_today fehlt)")
return None
if len(prices_today) != len(datetime_today):
log.error(f"Preis-Array und DateTime-Array haben unterschiedliche Längen")
return None
# Konvertiere zu einheitlichem Format
price_data = []
# Heute
for i, price in enumerate(prices_today):
try:
# datetime_today enthält Strings wie "2025-11-09 00:00:00"
dt_str = datetime_today[i]
dt = datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S")
price_data.append({
'datetime': dt,
'hour': dt.hour,
'date': dt.date(),
'price': float(price)
})
except Exception as e:
log.warning(f"Fehler beim Verarbeiten von Preis {i}: {e}")
continue
# Morgen (falls vorhanden)
if prices_tomorrow and datetime_tomorrow and len(prices_tomorrow) == len(datetime_tomorrow):
for i, price in enumerate(prices_tomorrow):
try:
dt_str = datetime_tomorrow[i]
dt = datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S")
price_data.append({
'datetime': dt,
'hour': dt.hour,
'date': dt.date(),
'price': float(price)
})
except Exception as e:
log.warning(f"Fehler beim Verarbeiten von Morgen-Preis {i}: {e}")
continue
return price_data
def get_pv_forecast():
"""
Holt PV-Prognose von Forecast.Solar (Ost + West Array)
"""
# Sensor-Namen für deine beiden Arrays
sensor_east = 'sensor.energy_production_today'
sensor_west = 'sensor.energy_production_today_2'
sensor_east_tomorrow = 'sensor.energy_production_tomorrow'
sensor_west_tomorrow = 'sensor.energy_production_tomorrow_2'
# Heute
east_today_attr = state.getattr(sensor_east) or {}
west_today_attr = state.getattr(sensor_west) or {}
pv_today = (float(east_today_attr.get('wh_period', 0)) +
float(west_today_attr.get('wh_period', 0)))
# Morgen
east_tomorrow_attr = state.getattr(sensor_east_tomorrow) or {}
west_tomorrow_attr = state.getattr(sensor_west_tomorrow) or {}
pv_tomorrow = (float(east_tomorrow_attr.get('wh_period', 0)) +
float(west_tomorrow_attr.get('wh_period', 0)))
# Stündliche Werte kombinieren
pv_wh_per_hour = {}
# Ost-Array
for hour_str, wh in (east_today_attr.get('wh_hours', {}) or {}).items():
pv_wh_per_hour[int(hour_str)] = wh
# West-Array addieren
for hour_str, wh in (west_today_attr.get('wh_hours', {}) or {}).items():
hour = int(hour_str)
pv_wh_per_hour[hour] = pv_wh_per_hour.get(hour, 0) + wh
# Morgen-Werte (24+ Stunden)
for hour_str, wh in (east_tomorrow_attr.get('wh_hours', {}) or {}).items():
hour = int(hour_str) + 24
pv_wh_per_hour[hour] = wh
for hour_str, wh in (west_tomorrow_attr.get('wh_hours', {}) or {}).items():
hour = int(hour_str) + 24
pv_wh_per_hour[hour] = pv_wh_per_hour.get(hour, 0) + wh
return {
'today': pv_today / 1000, # in kWh
'tomorrow': pv_tomorrow / 1000, # in kWh
'hourly': pv_wh_per_hour # in Wh per Stunde
}
def optimize_charging(price_data, pv_forecast, current_soc, config):
"""
Kern-Optimierungs-Algorithmus
Strategie:
1. Finde Stunden mit niedrigen Preisen UND wenig PV
2. Berechne verfügbare Ladekapazität
3. Erstelle Ladeplan für günstigste Stunden
"""
# Berechne dynamischen Preis-Schwellwert (90. Perzentil)
all_prices = [p['price'] for p in price_data]
all_prices.sort()
threshold_index = int(len(all_prices) * 0.9)
price_threshold = all_prices[threshold_index] if all_prices else config['price_threshold']
log.info(f"Preis-Schwellwert: {price_threshold:.2f} ct/kWh")
# Verfügbare Ladekapazität berechnen
available_capacity_wh = (config['max_soc'] - current_soc) / 100 * config['battery_capacity']
available_capacity_wh -= config['reserve_capacity'] # Reserve abziehen
if available_capacity_wh <= 0:
log.info("Batterie ist voll oder Reserve erreicht - keine Ladung nötig")
# Erstelle Plan nur mit "auto" Einträgen
return create_auto_only_schedule(price_data)
log.info(f"Verfügbare Ladekapazität: {available_capacity_wh/1000:.2f} kWh")
# Finde günstige Lade-Gelegenheiten
charging_opportunities = []
current_hour = datetime.now().hour
current_date = datetime.now().date()
for price_hour in price_data:
hour = price_hour['hour']
hour_date = price_hour['date']
price = price_hour['price']
# Berechne absolute Stunde (0-47 für heute+morgen)
if hour_date == current_date:
abs_hour = hour
elif hour_date > current_date:
abs_hour = hour + 24
else:
continue # Vergangenheit ignorieren
# Nur zukünftige Stunden
if abs_hour < current_hour:
continue
# PV-Prognose für diese Stunde
pv_wh = pv_forecast['hourly'].get(abs_hour, 0)
# Kriterien: Günstiger Preis UND wenig PV
if price < price_threshold and pv_wh < config['pv_threshold']:
charging_opportunities.append({
'datetime': price_hour['datetime'],
'hour': hour,
'abs_hour': abs_hour,
'price': price,
'pv_wh': pv_wh,
'score': price - (pv_wh / 1000) # Je niedriger, desto besser
})
# Sortiere nach Score (beste zuerst)
charging_opportunities.sort(key=lambda x: x['score'])
log.info(f"Gefundene Lade-Gelegenheiten: {len(charging_opportunities)}")
# Erstelle Ladeplan
schedule = []
remaining_capacity = available_capacity_wh
total_charge_energy = 0
total_charge_cost = 0
for price_hour in price_data:
hour = price_hour['hour']
abs_hour = price_hour['hour']
hour_date = price_hour['date']
# Berechne absolute Stunde
if hour_date == current_date:
abs_hour = hour
elif hour_date > current_date:
abs_hour = hour + 24
else:
continue
# Nur zukünftige Stunden (inkl. aktuelle!)
if abs_hour < current_hour:
continue
# Prüfe ob diese Stunde zum Laden vorgesehen ist
should_charge = False
charge_opportunity = None
for opp in charging_opportunities:
if opp['abs_hour'] == abs_hour:
should_charge = True
charge_opportunity = opp
break
if should_charge and remaining_capacity > 0:
# Ladeleistung berechnen
charge_wh = min(config['max_charge_power'], remaining_capacity)
schedule.append({
'datetime': price_hour['datetime'].isoformat(),
'hour': hour,
'action': 'charge',
'power_w': -int(charge_wh), # Negativ = Laden
'price': price_hour['price'],
'pv_wh': charge_opportunity['pv_wh'],
'reason': f"Günstig ({price_hour['price']:.2f} ct) + wenig PV ({charge_opportunity['pv_wh']} Wh)"
})
remaining_capacity -= charge_wh
total_charge_energy += charge_wh / 1000 # kWh
total_charge_cost += (charge_wh / 1000) * (price_hour['price'] / 100) # Euro
else:
# Auto-Modus (Standard-Betrieb)
reason = "Automatik"
if not should_charge and abs_hour < current_hour + 24:
if price_hour['price'] >= price_threshold:
reason = f"Preis zu hoch ({price_hour['price']:.2f} > {price_threshold:.2f} ct)"
else:
pv_wh = pv_forecast['hourly'].get(abs_hour, 0)
if pv_wh >= config['pv_threshold']:
reason = f"Genug PV ({pv_wh} Wh)"
schedule.append({
'datetime': price_hour['datetime'].isoformat(),
'hour': hour,
'action': 'auto',
'power_w': 0,
'price': price_hour['price'],
'reason': reason
})
# Berechne Anzahl Ladungen für Log
num_charges = 0
for s in schedule:
if s['action'] == 'charge':
num_charges += 1
log.info(f"Ladeplan erstellt: {len(schedule)} Stunden, davon {num_charges} Ladungen")
log.info(f"Gesamte Ladeenergie: {total_charge_energy:.2f} kWh, Kosten: {total_charge_cost:.2f}")
return schedule
def create_auto_only_schedule(price_data):
"""Erstellt einen Plan nur mit Auto-Modus (keine Ladung)"""
schedule = []
current_hour = datetime.now().hour
for price_hour in price_data:
if price_hour['hour'] >= current_hour:
schedule.append({
'datetime': price_hour['datetime'].isoformat(),
'hour': price_hour['hour'],
'action': 'auto',
'power_w': 0,
'price': price_hour['price'],
'reason': "Keine Ladung nötig (Batterie voll)"
})
return schedule
def save_schedule(schedule):
"""
Speichert den Schedule als PyScript State mit Attributen
"""
if not schedule:
log.warning("Leerer Schedule - nichts zu speichern")
return
# Berechne Statistiken
num_charges = 0
total_energy = 0
total_price = 0
first_charge = None
for s in schedule:
if s['action'] == 'charge':
num_charges += 1
total_energy += abs(s['power_w'])
total_price += s['price']
if first_charge is None:
first_charge = s['datetime']
total_energy = total_energy / 1000 # kWh
avg_price = total_price / num_charges if num_charges > 0 else 0
# Speichere als PyScript State
state.set(
'pyscript.battery_charging_schedule',
value='active',
new_attributes={
'schedule': schedule,
'last_update': datetime.now().isoformat(),
'num_hours': len(schedule),
'num_charges': num_charges,
'total_energy_kwh': round(total_energy, 2),
'avg_charge_price': round(avg_price, 2),
'first_charge_time': first_charge,
'estimated_savings': 0 # Wird später berechnet
}
)
log.info(f"Ladeplan gespeichert: {len(schedule)} Stunden als Attribut")
# Status aktualisieren
if num_charges > 0:
input_text.battery_optimizer_status = f"{num_charges} Ladungen geplant, ab {first_charge}"
else:
input_text.battery_optimizer_status = "Keine Ladung nötig"
def log_statistics(schedule, price_data):
"""Gibt Statistiken über den erstellten Plan aus"""
# Filter charges
charges = []
for s in schedule:
if s['action'] == 'charge':
charges.append(s)
if not charges:
log.info("Keine Ladungen geplant")
return
total_energy = 0
total_price = 0
for s in charges:
total_energy += abs(s['power_w'])
total_price += s['price']
total_energy = total_energy / 1000 # kWh
avg_price = total_price / len(charges)
first_charge = charges[0]['datetime']
log.info(f"Geplante Ladungen: {len(charges)} Stunden")
log.info(f"Gesamte Lademenge: {total_energy:.1f} kWh")
log.info(f"Durchschnittspreis beim Laden: {avg_price:.2f} ct/kWh")
log.info(f"Erste Ladung: {first_charge}")
@service
def execute_charging_schedule():
"""
Führt den aktuellen Ladeplan aus (stündlich aufgerufen)
Nutzt das bestehende manuelle Steuerungs-System
"""
# Prüfe ob Optimierung aktiv ist
if state.get('input_boolean.battery_optimizer_enabled') != 'on':
return
# Prüfe auf manuelle Überschreibung
if state.get('input_boolean.battery_optimizer_manual_override') == 'on':
log.info("Manuelle Überschreibung aktiv - überspringe Ausführung")
return
# Lade Schedule
schedule_attr = state.getattr('pyscript.battery_charging_schedule')
if not schedule_attr or 'schedule' not in schedule_attr:
log.warning("Kein Ladeplan vorhanden")
return
schedule = schedule_attr['schedule']
# Aktuelle Stunde ermitteln
now = datetime.now()
current_hour_dt = now.replace(minute=0, second=0, microsecond=0)
log.info(f"Suche Ladeplan für Stunde: {current_hour_dt.isoformat()}")
# Finde passenden Eintrag im Schedule
current_entry = None
for entry in schedule:
entry_dt = datetime.fromisoformat(entry['datetime'])
entry_hour = entry_dt.replace(minute=0, second=0, microsecond=0)
# Prüfe ob diese Stunde passt (mit 30 Min Toleranz)
time_diff = abs((entry_hour - current_hour_dt).total_seconds() / 60)
if time_diff < 30: # Innerhalb 30 Minuten
current_entry = entry
log.info(f"Gefunden: {entry_dt.isoformat()} (Abweichung: {time_diff:.0f} min)")
break
if not current_entry:
log.warning(f"Keine Daten für aktuelle Stunde {current_hour_dt.hour}:00")
return
# Führe Aktion aus
action = current_entry['action']
power_w = current_entry['power_w']
price = current_entry['price']
reason = current_entry.get('reason', '')
log.info(f"Stunde {current_hour_dt.isoformat()}: Aktion={action}, Leistung={power_w}W, Preis={price:.2f} ct")
log.info(f"Grund: {reason}")
if action == 'charge':
# Aktiviere Laden über bestehendes System
log.info(f"Aktiviere Laden mit {power_w}W")
# Setze Ziel-Leistung
input_number.charge_power_battery = float(power_w)
# Aktiviere manuellen Modus (triggert deine Automationen)
input_boolean.goodwe_manual_control = "on"
log.info("Manuelles Laden aktiviert")
elif action == 'auto':
# Deaktiviere manuelles Laden, zurück zu Auto-Modus
if state.get('input_boolean.goodwe_manual_control') == 'on':
log.info("Deaktiviere manuelles Laden, aktiviere Auto-Modus")
input_boolean.goodwe_manual_control = "off"
else:
log.info("Auto-Modus bereits aktiv")
# ====================
# Zeit-Trigger
# ====================
@time_trigger("cron(5 14 * * *)")
def daily_optimization():
"""Tägliche Berechnung um 14:05 Uhr (nach haStrom Preis-Update)"""
log.info("=== Tägliche Optimierungs-Berechnung gestartet ===")
pyscript.calculate_charging_schedule()
@time_trigger("cron(5 * * * *)")
def hourly_execution():
"""Stündliche Ausführung des Plans um xx:05 Uhr"""
pyscript.execute_charging_schedule()

View File

@@ -0,0 +1,188 @@
# ============================================
# Battery Charging Optimizer - Konfiguration
# ============================================
# Speicherort: /config/packages/battery_optimizer_config.yaml
# oder in configuration.yaml unter entsprechenden Sektionen
# ====================
# Input Boolean
# ====================
input_boolean:
battery_optimizer_enabled:
name: "Batterie-Optimierung aktiviert"
icon: mdi:battery-charging-wireless
battery_optimizer_manual_override:
name: "Manuelle Überschreibung"
icon: mdi:hand-back-right
# ====================
# Input Number
# ====================
input_number:
# Batterie-Parameter
battery_capacity_kwh:
name: "Batterie-Kapazität"
min: 1
max: 50
step: 0.5
unit_of_measurement: "kWh"
icon: mdi:battery
mode: box
initial: 10
battery_optimizer_min_soc:
name: "Minimaler SOC"
min: 0
max: 50
step: 5
unit_of_measurement: "%"
icon: mdi:battery-low
mode: slider
initial: 20
battery_optimizer_max_soc:
name: "Maximaler SOC"
min: 50
max: 100
step: 5
unit_of_measurement: "%"
icon: mdi:battery-high
mode: slider
initial: 100
battery_optimizer_max_charge_power:
name: "Max. Ladeleistung"
min: 1000
max: 10000
step: 500
unit_of_measurement: "W"
icon: mdi:lightning-bolt
mode: box
initial: 5000
# Optimierungs-Parameter
battery_optimizer_price_threshold:
name: "Preis-Schwellwert"
min: 0
max: 50
step: 0.5
unit_of_measurement: "ct/kWh"
icon: mdi:currency-eur
mode: box
initial: 28
battery_optimizer_reserve_capacity:
name: "Reserve-Kapazität (Haushalt)"
min: 0
max: 5
step: 0.5
unit_of_measurement: "kWh"
icon: mdi:home-lightning-bolt
mode: box
initial: 2
battery_optimizer_pv_threshold:
name: "PV-Schwellwert (keine Ladung)"
min: 0
max: 5000
step: 100
unit_of_measurement: "Wh"
icon: mdi:solar-power
mode: box
initial: 500
# ====================
# Input Text
# ====================
input_text:
battery_optimizer_status:
name: "Optimierungs-Status"
max: 255
icon: mdi:information-outline
# ====================
# Sensor Templates
# ====================
template:
- sensor:
# Aktueller Ladeplan-Status
- name: "Batterie Ladeplan Status"
unique_id: battery_charging_plan_status
state: >
{% set schedule = state_attr('pyscript.battery_charging_schedule', 'schedule') %}
{% if schedule %}
{% set num_charges = schedule | selectattr('action', 'eq', 'charge') | list | count %}
{{ num_charges }} Ladungen geplant
{% else %}
Kein Plan
{% endif %}
icon: mdi:calendar-clock
attributes:
last_update: >
{{ state_attr('pyscript.battery_charging_schedule', 'last_update') }}
total_hours: >
{{ state_attr('pyscript.battery_charging_schedule', 'num_hours') }}
next_charge: >
{% set schedule = state_attr('pyscript.battery_charging_schedule', 'schedule') %}
{% if schedule %}
{% set charges = schedule | selectattr('action', 'eq', 'charge') | list %}
{% if charges | count > 0 %}
{{ charges[0].hour }}:00 Uhr ({{ charges[0].price }} ct/kWh)
{% else %}
Keine Ladung geplant
{% endif %}
{% else %}
Kein Plan vorhanden
{% endif %}
# Nächste geplante Aktion
- name: "Batterie Nächste Aktion"
unique_id: battery_next_action
state: >
{% set schedule = state_attr('pyscript.battery_charging_schedule', 'schedule') %}
{% if schedule %}
{% set now_hour = now().hour %}
{% set future = schedule | selectattr('hour', 'ge', now_hour) | list %}
{% if future | count > 0 %}
{{ future[0].hour }}:00 - {{ future[0].action }}
{% else %}
Keine weiteren Aktionen heute
{% endif %}
{% else %}
Kein Plan
{% endif %}
icon: mdi:clock-outline
attributes:
power: >
{% set schedule = state_attr('pyscript.battery_charging_schedule', 'schedule') %}
{% if schedule %}
{% set now_hour = now().hour %}
{% set future = schedule | selectattr('hour', 'ge', now_hour) | list %}
{% if future | count > 0 %}
{{ future[0].power_w }} W
{% endif %}
{% endif %}
price: >
{% set schedule = state_attr('pyscript.battery_charging_schedule', 'schedule') %}
{% if schedule %}
{% set now_hour = now().hour %}
{% set future = schedule | selectattr('hour', 'ge', now_hour) | list %}
{% if future | count > 0 %}
{{ future[0].price }} ct/kWh
{% endif %}
{% endif %}
# Geschätzte Ersparnis
- name: "Batterie Geschätzte Ersparnis"
unique_id: battery_estimated_savings
state: >
{% set schedule = state_attr('pyscript.battery_charging_schedule', 'schedule') %}
{% if schedule %}
{{ state_attr('pyscript.battery_charging_schedule', 'estimated_savings') | float(0) | round(2) }}
{% else %}
0
{% endif %}
unit_of_measurement: "€"
device_class: monetary
icon: mdi:piggy-bank

View File

@@ -0,0 +1,113 @@
# ============================================
# Battery Charging Optimizer - Dashboard
# ============================================
# Füge diese Cards zu deinem Lovelace Dashboard hinzu
type: vertical-stack
cards:
# Status-Übersicht
- type: entities
title: Batterie-Optimierung Status
show_header_toggle: false
entities:
- entity: input_boolean.battery_optimizer_enabled
name: Optimierung aktiviert
- entity: input_boolean.battery_optimizer_manual_override
name: Manuelle Überschreibung
- entity: input_text.battery_optimizer_status
name: Status
# Manuelle Steuerung (dein bestehendes System)
- type: entities
title: Manuelle Steuerung
show_header_toggle: false
entities:
- entity: input_boolean.goodwe_manual_control
name: Manueller Modus
- entity: input_number.charge_power_battery
name: Ladeleistung
- type: divider
- entity: sensor.esssoc
name: Batterie SOC
- entity: sensor.battery_power
name: Batterie Leistung
# Konfiguration
- type: entities
title: Optimierungs-Einstellungen
show_header_toggle: false
entities:
- entity: input_number.battery_capacity_kwh
name: Batterie-Kapazität
- entity: input_number.battery_optimizer_min_soc
name: Min. SOC
- entity: input_number.battery_optimizer_max_soc
name: Max. SOC
- entity: input_number.battery_optimizer_max_charge_power
name: Max. Ladeleistung
- type: divider
- entity: input_number.battery_optimizer_price_threshold
name: Preis-Schwellwert
- entity: input_number.battery_optimizer_reserve_capacity
name: Reserve-Kapazität
- entity: input_number.battery_optimizer_pv_threshold
name: PV-Schwellwert
# Aktionen
- type: entities
title: Aktionen
show_header_toggle: false
entities:
- type: button
name: Plan neu berechnen
icon: mdi:refresh
tap_action:
action: call-service
service: pyscript.calculate_charging_schedule
- type: button
name: Plan jetzt ausführen
icon: mdi:play
tap_action:
action: call-service
service: pyscript.execute_charging_schedule
# Ladeplan-Tabelle
- type: markdown
title: Geplante Ladungen (nächste 24h)
content: >
{% set schedule = state_attr('pyscript.battery_charging_schedule', 'schedule') %}
{% if schedule %}
{% set charges = schedule | selectattr('action', 'eq', 'charge') | list %}
{% if charges | count > 0 %}
| Zeit | Leistung | Preis | Grund |
|------|----------|-------|-------|
{% for charge in charges[:10] %}
| {{ charge.datetime[11:16] }} | {{ charge.power_w }}W | {{ charge.price }}ct | {{ charge.reason }} |
{% endfor %}
{% else %}
*Keine Ladungen geplant*
{% endif %}
{% else %}
*Kein Plan vorhanden - bitte neu berechnen*
{% endif %}
# Statistiken
- type: markdown
title: Plan-Statistiken
content: >
{% set attrs = state_attr('pyscript.battery_charging_schedule', 'schedule') %}
{% if attrs %}
**Letzte Aktualisierung:** {{ state_attr('pyscript.battery_charging_schedule', 'last_update') }}
**Anzahl Stunden:** {{ state_attr('pyscript.battery_charging_schedule', 'num_hours') }}
**Geplante Ladungen:** {{ state_attr('pyscript.battery_charging_schedule', 'num_charges') }}
**Gesamtenergie:** {{ state_attr('pyscript.battery_charging_schedule', 'total_energy_kwh') }} kWh
**Durchschnittspreis:** {{ state_attr('pyscript.battery_charging_schedule', 'avg_charge_price') }} ct/kWh
**Erste Ladung:** {{ state_attr('pyscript.battery_charging_schedule', 'first_charge_time') }}
{% else %}
*Keine Statistiken verfügbar*
{% endif %}