## 🎯 Hauptänderungen ### Version 3.4.0 - SOC-Drift & Charging Capacity - ✨ Sicherheitspuffer (20-50% konfigurierbar) für untertägige SOC-Schwankungen - ✨ Monatliche automatische Batterie-Kalibrierung - 🐛 SOC-Plausibilitäts-Check (filtert 65535% Spikes beim Modus-Wechsel) - 🐛 Zeitabhängige API-Abfrage (vor/nach 14:00 Uhr) ### Neue Features - 🔋 **Safety Buffer**: Kompensiert SOC-Drift und Eigenverbrauch - 🔋 **Auto-Calibration**: Monatlicher Vollzyklus für SOC-Genauigkeit - 🔋 **Spike Protection**: 4-fach Schutz gegen ungültige SOC-Werte - 🔋 **Smart API**: Verhindert HTTP 500 Errors bei fehlenden Tomorrow-Preisen ### Dokumentation - 📚 SOC_CALIBRATION_GUIDE.md - Umfassender Kalibrierungs-Guide - 📚 FIX_CHARGING_CAPACITY.md - Sicherheitspuffer-Dokumentation - 📚 FIX_SOC_SPIKE_PROBLEM.md - Spike-Protection-Lösung - 📚 FIX_API_TIMING.md - Zeitabhängige API-Abfrage - 📚 DIAGNOSE_LADE_PROBLEM.md - Debug-Guide ### Neue Dateien - battery_calibration_automation.yaml - 4 Automations für Kalibrierung - battery_calibration_input_helper.yaml - Input Helper Config - battery_optimizer_input_helper_safety_buffer.yaml - Puffer Config - debug_schedule.py - Umfassendes Debug-Script ### Scripts - battery_charging_optimizer.py v3.4.0 - hastrom_flex_extended.py v1.1.0 - debug_schedule.py v1.0.0 ### Fixes - 🐛 SOC springt auf 65535% beim ESS-Modus-Wechsel → Debounce + Plausibilitäts-Check - 🐛 API-HTTP-500 vor 14:00 → Zeitabhängige Abfrage - 🐛 Batterie nicht bis 100% geladen → Sicherheitspuffer - 🐛 SOC driftet ohne Vollzyklen → Automatische Kalibrierung ## 🚀 Installation 1. Input Helper erstellen (siehe battery_optimizer_input_helper_safety_buffer.yaml) 2. Automations installieren (siehe battery_calibration_automation.yaml) 3. Scripts aktualisieren (battery_charging_optimizer.py v3.4.0) 4. PyScript neu laden ## 📊 Verbesserungen - Präzisere Ladeplanung durch Sicherheitspuffer - Robustheit gegen SOC-Drift - Keine API-Fehler mehr vor 14:00 - Hardware-Stopp bei 100% wird respektiert - Bessere Batterie-Gesundheit durch regelmäßige Kalibrierung 🤖 Generated with Claude Code (claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
168 lines
5.0 KiB
Python
168 lines
5.0 KiB
Python
"""
|
|
Battery Power Control via Modbus
|
|
Hilfs-Script für das Schreiben der Batterieleistung über Modbus
|
|
|
|
Speicherort: /config/pyscript/battery_power_control.py
|
|
|
|
Verwendet die bewährte float_to_regs_be Methode von Felix's ess_set_power.py
|
|
"""
|
|
|
|
import struct
|
|
|
|
@service
|
|
def set_battery_power_modbus(power_w: float = 0.0, hub: str = "openEMS", slave: int = 1):
|
|
"""
|
|
Schreibt die Batterieleistung direkt über Modbus Register 706
|
|
|
|
Register 706 = ess0/SetActivePowerEquals (FLOAT32 Big-Endian)
|
|
|
|
Args:
|
|
power_w: Leistung in Watt (negativ = laden, positiv = entladen)
|
|
hub: Modbus Hub Name (default: "openEMS")
|
|
slave: Modbus Slave ID (default: 1)
|
|
"""
|
|
|
|
ADDR_EQUALS = 706
|
|
|
|
def float_to_regs_be(val: float):
|
|
"""Konvertiert Float zu Big-Endian Register-Paar"""
|
|
b = struct.pack(">f", float(val)) # Big Endian
|
|
return [(b[0] << 8) | b[1], (b[2] << 8) | b[3]] # [hi, lo]
|
|
|
|
# Konvertiere zu Float
|
|
try:
|
|
p = float(power_w)
|
|
except Exception:
|
|
log.warning(f"Konnte {power_w} nicht zu Float konvertieren, nutze 0.0")
|
|
p = 0.0
|
|
|
|
# Konvertiere zu Register-Paar
|
|
regs = float_to_regs_be(p)
|
|
|
|
log.info(f"OpenEMS ESS Ziel: {p:.1f} W -> Register {ADDR_EQUALS} -> {regs}")
|
|
|
|
try:
|
|
service.call(
|
|
"modbus",
|
|
"write_register",
|
|
hub=hub,
|
|
slave=slave,
|
|
address=ADDR_EQUALS,
|
|
value=regs # Liste mit 2 Registern für FLOAT32
|
|
)
|
|
|
|
log.info(f"Erfolgreich {p:.1f}W geschrieben")
|
|
|
|
except Exception as e:
|
|
log.error(f"Fehler beim Modbus-Schreiben: {e}")
|
|
raise
|
|
|
|
|
|
@service
|
|
def start_charging_cycle(power_w: float = -10000.0, hub: str = "openEMS", slave: int = 1):
|
|
"""
|
|
Startet einen kompletten Lade-Zyklus
|
|
|
|
Ablauf:
|
|
1. ESS → REMOTE Mode (manuelle Steuerung aktivieren)
|
|
2. Leistung über Modbus setzen (Register 706)
|
|
3. Keep-Alive aktivieren (alle 30s neu schreiben)
|
|
|
|
Args:
|
|
power_w: Ladeleistung in Watt (negativ = laden, positiv = entladen)
|
|
hub: Modbus Hub Name (default: "openEMS")
|
|
slave: Modbus Slave ID (default: 1)
|
|
"""
|
|
|
|
log.info(f"Starte Lade-Zyklus mit {power_w}W")
|
|
|
|
# 1. ESS in REMOTE Mode setzen
|
|
# WICHTIG: VOR dem Schreiben der Leistung!
|
|
service.call('rest_command', 'set_ess_remote_mode')
|
|
task.sleep(1.0) # Warte auf Modusänderung
|
|
|
|
# 2. Ladeleistung setzen (mit korrekter FLOAT32-Konvertierung)
|
|
set_battery_power_modbus(power_w=power_w, hub=hub, slave=slave)
|
|
|
|
# 3. Status für Keep-Alive setzen
|
|
state.set('pyscript.battery_charging_active', True)
|
|
state.set('pyscript.battery_charging_power', power_w)
|
|
state.set('pyscript.battery_charging_hub', hub)
|
|
state.set('pyscript.battery_charging_slave', slave)
|
|
|
|
log.info("Lade-Zyklus gestartet (ESS in REMOTE Mode)")
|
|
|
|
|
|
@service
|
|
def stop_charging_cycle():
|
|
"""
|
|
Stoppt den Lade-Zyklus und aktiviert automatischen Betrieb
|
|
|
|
Ablauf:
|
|
1. ESS → INTERNAL Mode (zurück zur automatischen Steuerung)
|
|
2. Status zurücksetzen (Keep-Alive deaktivieren)
|
|
"""
|
|
|
|
log.info("Stoppe Lade-Zyklus")
|
|
|
|
# 1. ESS zurück in INTERNAL Mode
|
|
# WICHTIG: Nach dem Laden, um wieder auf Automatik zu schalten!
|
|
service.call('rest_command', 'set_ess_internal_mode')
|
|
task.sleep(1.0) # Warte auf Modusänderung
|
|
|
|
# 2. Status zurücksetzen
|
|
state.set('pyscript.battery_charging_active', False)
|
|
state.set('pyscript.battery_charging_power', 0)
|
|
|
|
log.info("Automatischer Betrieb aktiviert (ESS in INTERNAL Mode)")
|
|
|
|
|
|
@time_trigger('cron(*/30 * * * *)')
|
|
def keep_alive_charging():
|
|
"""
|
|
Keep-Alive: Schreibt alle 30 Sekunden die Leistung neu
|
|
Verhindert Timeout im REMOTE Mode
|
|
"""
|
|
|
|
# Prüfe ob Laden aktiv ist
|
|
if not state.get('pyscript.battery_charging_active'):
|
|
return
|
|
|
|
power_w = state.get('pyscript.battery_charging_power')
|
|
hub = state.get('pyscript.battery_charging_hub') or "openEMS"
|
|
slave = state.get('pyscript.battery_charging_slave') or 1
|
|
|
|
if power_w is None:
|
|
return
|
|
|
|
try:
|
|
power_w = float(power_w)
|
|
log.debug(f"Keep-Alive: Schreibe {power_w}W")
|
|
set_battery_power_modbus(power_w=power_w, hub=hub, slave=int(slave))
|
|
except Exception as e:
|
|
log.error(f"Keep-Alive Fehler: {e}")
|
|
|
|
|
|
@service
|
|
def emergency_stop():
|
|
"""
|
|
Notfall-Stop: Deaktiviert sofort alle manuellen Steuerungen
|
|
"""
|
|
|
|
log.warning("NOTFALL-STOP ausgelöst!")
|
|
|
|
try:
|
|
# Alles zurück auf Auto
|
|
stop_charging_cycle()
|
|
|
|
# Optimierung deaktivieren
|
|
input_boolean.battery_optimizer_enabled = False
|
|
|
|
# Notification
|
|
service.call('notify.persistent_notification',
|
|
title="Batterie-Optimierung",
|
|
message="NOTFALL-STOP ausgelöst! Alle Automatisierungen deaktiviert.")
|
|
|
|
except Exception as e:
|
|
log.error(f"Fehler beim Notfall-Stop: {e}")
|