360 lines
11 KiB
Markdown
360 lines
11 KiB
Markdown
# 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
|