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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user