Files
battery-charging-optimizer/openems/hastrom_flex_extended.py
felix.zoesch 0fa03a566a feat: Major update - Battery Optimizer v3.4.0 with comprehensive fixes
## 🎯 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>
2025-12-12 08:04:07 +01:00

215 lines
8.4 KiB
Python

# /homeassistant/pyscript/hastrom_flex_extended.py
# Version: 1.1.0 - FIXED: Zeitabhängige API-Abfrage
# VOR 14:00: Nur heute (verhindert HTTP 500 Error)
# AB 14:00: Heute + morgen
import requests, json
import datetime
from zoneinfo import ZoneInfo
# Konstante für Timezone
TIMEZONE = ZoneInfo("Europe/Berlin")
def get_local_now():
"""Gibt die aktuelle Zeit in lokaler Timezone zurück (Europe/Berlin)"""
return datetime.datetime.now(TIMEZONE)
@service
def getprices_extended():
"""
Erweiterte Version von haStrom FLEX PRO Preisabfrage mit Tomorrow-Support.
Erstellt neue Sensoren: sensor.hastrom_flex_ext und sensor.hastrom_flex_pro_ext
FIXED: Proper timezone handling - alle Datetimes in Europe/Berlin
"""
now = get_local_now()
today = now.strftime("%Y%m%d")
tomorrow_date = now + datetime.timedelta(days=1)
tomorrow = tomorrow_date.strftime("%Y%m%d")
hr = int(now.strftime("%H"))
# ==========================================
# Zeitabhängige API-Abfrage
# ==========================================
# VOR 14:00: Nur heute abfragen (Tomorrow-Preise noch nicht verfügbar)
# AB 14:00: Heute + morgen abfragen
if hr < 14:
end_date = today
log.info(f"Lade Preise nur für {today} (vor 14:00 - Tomorrow nicht verfügbar)")
else:
end_date = tomorrow
log.info(f"Lade Preise für {today} bis {tomorrow} (ab 14:00 - Tomorrow verfügbar)")
log.info(f"Lokale Zeit: {now.strftime('%Y-%m-%d %H:%M:%S %Z')}")
# ==========================================
# API-Call für heute (+ morgen ab 14:00) - FLEX PRO
# ==========================================
url = f"http://eex.stwhas.de/api/spotprices/flexpro?start_date={today}&end_date={end_date}"
try:
response = task.executor(requests.get, url, timeout=10)
# Check HTTP status
if response.status_code != 200:
log.error(f"❌ API-Fehler: HTTP {response.status_code}")
log.error(f"URL: {url}")
log.error(f"Response: {response.text[:200]}")
return
# Try to parse JSON
try:
data = response.json()
except ValueError as json_err:
log.error(f"❌ JSON Parse-Fehler: {json_err}")
log.error(f"Response Text: {response.text[:200]}")
return
# Check if data structure is valid
if 'data' not in data:
log.error(f"❌ API-Response hat kein 'data' Feld")
log.error(f"Response keys: {list(data.keys())}")
return
log.info(f"✓ API-Abfrage erfolgreich: {len(data.get('data', []))} Datenpunkte")
except Exception as e:
log.error(f"❌ Fehler beim Abrufen der Strompreise: {e}")
log.error(f"URL: {url}")
return
# ==========================================
# Verarbeite Daten mit TIMEZONE AWARENESS
# ==========================================
today_date = now.date()
tomorrow_date_obj = tomorrow_date.date()
# Sammle Daten
price_list_today = []
datetime_list_today = []
price_list_tomorrow = []
datetime_list_tomorrow = []
current_price = None
for item in data["data"]:
# FIXED: Parse timestamps und lokalisiere nach Europe/Berlin
start_dt_naive = datetime.datetime.strptime(item["start_timestamp"], "%Y-%m-%d %H:%M:%S")
end_dt_naive = datetime.datetime.strptime(item["end_timestamp"], "%Y-%m-%d %H:%M:%S")
# Timestamps from API are in local time (Europe/Berlin), so we add timezone info
start_dt = start_dt_naive.replace(tzinfo=TIMEZONE)
end_dt = end_dt_naive.replace(tzinfo=TIMEZONE)
# FLEX PRO Preis: t_price_has_pro_incl_vat
price = item["t_price_has_pro_incl_vat"]
timestamp = item["start_timestamp"]
# FIXED: Aktueller Preis - vergleiche timezone-aware datetimes
if start_dt <= now < end_dt:
current_price = price
# Sortiere nach Datum
if start_dt.date() == today_date:
price_list_today.append(price)
datetime_list_today.append(timestamp)
elif start_dt.date() == tomorrow_date_obj:
price_list_tomorrow.append(price)
datetime_list_tomorrow.append(timestamp)
# ==========================================
# UPDATE: sensor.hastrom_flex_ext
# ==========================================
if current_price is not None:
state.set("sensor.hastrom_flex_ext", value=float(current_price))
# Tariff info (angepasst für FLEX PRO)
if "tariff_info_flex_pro" in data:
for key, value in data["tariff_info_flex_pro"].items():
state.setattr(f"sensor.hastrom_flex_ext.{key}", value)
# Prices
state.setattr("sensor.hastrom_flex_ext.prices_today", price_list_today)
state.setattr("sensor.hastrom_flex_ext.datetime_today", datetime_list_today)
state.setattr("sensor.hastrom_flex_ext.prices_tomorrow", price_list_tomorrow)
state.setattr("sensor.hastrom_flex_ext.datetime_tomorrow", datetime_list_tomorrow)
# Status
state.setattr("sensor.hastrom_flex_ext.tomorrow_available", len(price_list_tomorrow) > 0)
state.setattr("sensor.hastrom_flex_ext.tomorrow_count", len(price_list_tomorrow))
state.setattr("sensor.hastrom_flex_ext.last_update", now.strftime("%Y-%m-%d %H:%M:%S"))
# ==========================================
# UPDATE: sensor.hastrom_flex_pro_ext
# ==========================================
if current_price is not None:
state.set("sensor.hastrom_flex_pro_ext", value=float(current_price))
# Tariff info
if "tariff_info_flex_pro" in data:
for key, value in data["tariff_info_flex_pro"].items():
state.setattr(f"sensor.hastrom_flex_pro_ext.{key}", value)
# Prices
state.setattr("sensor.hastrom_flex_pro_ext.prices_today", price_list_today)
state.setattr("sensor.hastrom_flex_pro_ext.datetime_today", datetime_list_today)
state.setattr("sensor.hastrom_flex_pro_ext.prices_tomorrow", price_list_tomorrow)
state.setattr("sensor.hastrom_flex_pro_ext.datetime_tomorrow", datetime_list_tomorrow)
# Status
state.setattr("sensor.hastrom_flex_pro_ext.tomorrow_available", len(price_list_tomorrow) > 0)
state.setattr("sensor.hastrom_flex_pro_ext.tomorrow_count", len(price_list_tomorrow))
state.setattr("sensor.hastrom_flex_pro_ext.last_update", now.strftime("%Y-%m-%d %H:%M:%S"))
# ==========================================
# Logging & Debug
# ==========================================
tomorrow_expected = hr >= 14
tomorrow_available = len(price_list_tomorrow) > 0
log.info(f"📊 haStrom FLEX PRO Extended - Preise aktualisiert:")
log.info(f" ├─ Heute: {len(price_list_today)} Stunden")
if tomorrow_expected:
if tomorrow_available:
log.info(f" └─ Morgen: {len(price_list_tomorrow)} Stunden ✓ verfügbar (nach 14:00)")
else:
log.warning(f" └─ Morgen: {len(price_list_tomorrow)} Stunden ⚠ NICHT verfügbar (sollte verfügbar sein nach 14:00!)")
else:
log.info(f" └─ Morgen: {len(price_list_tomorrow)} Stunden (noch nicht erwartet vor 14:00)")
if price_list_today:
min_today = min(price_list_today)
max_today = max(price_list_today)
avg_today = sum(price_list_today) / len(price_list_today)
log.info(f" 📈 Heute: Min={min_today:.2f}, Max={max_today:.2f}, Avg={avg_today:.2f} ct/kWh")
if price_list_tomorrow:
min_tomorrow = min(price_list_tomorrow)
max_tomorrow = max(price_list_tomorrow)
avg_tomorrow = sum(price_list_tomorrow) / len(price_list_tomorrow)
log.info(f" 📈 Morgen: Min={min_tomorrow:.2f}, Max={max_tomorrow:.2f}, Avg={avg_tomorrow:.2f} ct/kWh")
# ==========================================
# Automatische Aktualisierung
# ==========================================
@time_trigger("cron(0 * * * *)") # Jede volle Stunde
def update_prices_hourly():
"""Aktualisiere Preise jede Stunde"""
pyscript.getprices_extended()
@time_trigger("cron(5 14 * * *)") # Täglich um 14:05
def update_prices_afternoon():
"""Extra Update um 14:05 wenn Preise für morgen verfügbar werden"""
log.info("=== TRIGGER: 14:05 Update für Tomorrow-Preise ===")
pyscript.getprices_extended()
@time_trigger("cron(5 0 * * *)") # Um Mitternacht
def update_prices_midnight():
"""Update um Mitternacht für neuen Tag"""
log.info("=== TRIGGER: Midnight Update ===")
pyscript.getprices_extended()