# 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