v1.8.16: Finanzen-Tab — Festpreis vs. Spot-Vergleich

Täglicher Tarif-Tracking: kWh + EPEX-Ø-Preis werden ab jetzt täglich
gespeichert. Neuer Finanzen-Tab zeigt Balkendiagramm (Festpreis vs.
Spot hypothetisch), Summen-Karten und Empfehlung ob flexibler Tarif
sich lohnen würde.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
retr0
2026-05-05 13:29:55 +02:00
parent dfb42e6902
commit 2456f356b4
4 changed files with 253 additions and 2 deletions
+83
View File
@@ -77,6 +77,7 @@ class State:
surplus_devices_cfg: List[Dict[str, Any]] = []
z2m_base: str = "zigbee2mqtt"
z2m_devices: List[Dict[str, Any]] = []
last_tariff_snapshot_date: str = ""
_publisher: Optional[MqttPublisher] = None
_surplus_ctrl: Optional[SurplusDeviceController] = None
@@ -193,6 +194,37 @@ def _get_pv_surplus() -> float:
# ── Poll Loop ─────────────────────────────────────────────────
def _run_daily_tariff_snapshot():
import datetime
today = datetime.date.today()
yesterday = today - datetime.timedelta(days=1)
today_iso = today.isoformat()
yesterday_iso = yesterday.isoformat()
agg = _compute_aggregates(allow_stale=True)
cur_kwh = agg.get("grid_import_kwh")
if cur_kwh is None:
return
history.save_daily_kwh_start(today_iso, cur_kwh)
prev_kwh = history.get_daily_kwh_start(yesterday_iso)
if prev_kwh is None:
return
fixed_ct = float(State.mqtt_cfg.get("price_import", 0.30)) * 100
markup_ct = float(State.mqtt_cfg.get("spot_markup", 0.0))
country = str(State.mqtt_cfg.get("spot_country", "de"))
y_start = datetime.datetime.combine(yesterday, datetime.time.min).timestamp()
y_end = datetime.datetime.combine(today, datetime.time.min).timestamp()
spot_ct = _get_avg_spot_price(y_start, y_end, country)
history.save_daily_tariff_snapshot(yesterday_iso, prev_kwh, cur_kwh, spot_ct, fixed_ct, markup_ct)
log.info("Tariftag %s gespeichert: %.3f kWh, spot=%.2f ct, fixed=%.2f ct",
yesterday_iso, max(0, cur_kwh - prev_kwh), spot_ct or 0, fixed_ct)
def _poll_loop(inv_cfg: Dict[str, Any], stop: threading.Event):
inv_id = inv_cfg["id"]
model_id = inv_cfg.get("inverter_model", "MIC_1500_TL_X")
@@ -294,6 +326,14 @@ def _poll_loop(inv_cfg: Dict[str, Any], stop: threading.Event):
if agg:
_publisher.publish_aggregates(agg)
# Täglicher Tarif-Snapshot (einmal pro Tag, beim ersten Poll nach Mitternacht)
if values is not None:
import datetime
today_iso = datetime.date.today().isoformat()
if State.last_tariff_snapshot_date != today_iso:
State.last_tariff_snapshot_date = today_iso
threading.Thread(target=_run_daily_tariff_snapshot, daemon=True).start()
stop.wait(max(0.0, interval - (time.time() - t0)))
reader.close()
@@ -533,6 +573,49 @@ def api_period_energy():
return jsonify(result)
@app.get("/api/finance")
def api_finance():
import datetime
billing_day = int(State.mqtt_cfg.get("billing_day", 1))
billing_month = int(State.mqtt_cfg.get("billing_month", 1))
yr_key = history.period_key("yearly", billing_day, billing_month)
yr_start = datetime.date.fromisoformat(yr_key)
today = datetime.date.today()
rows = history.get_tariff_days(yr_key, today.isoformat())
days = []
fixed_total = 0.0
spot_total = 0.0
spot_available = 0
for date_iso, kwh, spot_ct, fixed_ct, markup_ct in rows:
fixed_day = round(kwh * fixed_ct / 100, 4)
spot_day = round(kwh * ((spot_ct + markup_ct) / 100), 4) if spot_ct is not None else None
fixed_total += fixed_day
if spot_day is not None:
spot_total += spot_day
spot_available += 1
days.append({
"date": date_iso,
"kwh": round(kwh, 3),
"fixed_eur": fixed_day,
"spot_eur": spot_day,
"spot_ct": spot_ct,
"fixed_ct": fixed_ct,
})
result = {
"period_start": yr_key,
"days": days,
"fixed_total_eur": round(fixed_total, 2),
"spot_total_eur": round(spot_total, 2) if spot_available else None,
"spot_days": spot_available,
"total_days": len(days),
"savings_eur": round(fixed_total - spot_total, 2) if spot_available else None,
}
return jsonify(result)
@app.get("/api/z2m-devices")
def api_z2m_devices():
with State.lock: