11 KiB
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
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:
ctrlBalancing0mit 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:
- Batterieladung starten (ID:
1730457901370) - Batterieladung stoppen (ID:
1730457994517) - 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
# 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
# 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
# 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
- Minimal: Schneller Status-Check
- Standard: Tägliche Nutzung
- Detail: Analyse und Debugging
PyScript-Besonderheiten
Bekannte Limitierungen
# 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
# 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
# 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
- Controller-Priorität ist kritisch: Immer höchsten verfügbaren Channel nutzen
- Zeitzone-Handling explizit: UTC/Local nie vermischen
- API-Spezifikationen prüfen: Feldnamen können abweichen (haStrom!)
- PyScript-Limitierungen kennen: Keine komplexen Comprehensions
- Attributes > State: Für komplexe Datenstrukturen
- Bestehende Automationen nutzen: Nicht neuerfinden
- Transparent loggen: Nachvollziehbarkeit ist Schlüssel
Quick Reference - Häufige Befehle
Modbus Register auslesen
# Via HA Developer Tools > States
sensor.openems_battery_soc
sensor.openems_grid_power
PyScript neu laden
# HA Developer Tools > Services
service: pyscript.reload
openEMS Logs prüfen
# Auf BeagleBone
tail -f /var/log/openems/openems.log
Manueller Ladetest
# 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