v1.8.14: Abschlags-Tracker (monatliche Rate + Grundpreis)

Neues optionales Feature: Abschlags-Übersicht im Energie-Dashboard.
Zeigt Bereits bezahlt, Grundpreis anteilig, Energiekosten sowie
voraussichtliche Nachzahlung oder Guthaben für das laufende Abrechnungsjahr.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
retr0
2026-05-05 11:45:56 +02:00
parent 5ab8ee75fb
commit fec49ec4fb
3 changed files with 99 additions and 1 deletions
+34
View File
@@ -101,6 +101,9 @@ def _defaults() -> Dict[str, Any]:
"spot_country": "de",
"spot_markup": 0.0,
"spot_chart": True,
"billing_tracker_enabled": False,
"monthly_rate_eur": 0.0,
"grundpreis_eur_per_month": 0.0,
"inverters": [],
"surplus_devices": [],
"z2m_base": "zigbee2mqtt",
@@ -139,6 +142,9 @@ def save_config():
"spot_country": State.mqtt_cfg.get("spot_country", "de"),
"spot_markup": State.mqtt_cfg.get("spot_markup", 0.0),
"spot_chart": State.mqtt_cfg.get("spot_chart", True),
"billing_tracker_enabled": State.mqtt_cfg.get("billing_tracker_enabled", False),
"monthly_rate_eur": State.mqtt_cfg.get("monthly_rate_eur", 0.0),
"grundpreis_eur_per_month": State.mqtt_cfg.get("grundpreis_eur_per_month", 0.0),
"inverters": State.inverters_cfg,
"surplus_devices": State.surplus_devices_cfg,
"z2m_base": State.z2m_base,
@@ -410,6 +416,11 @@ def api_save_config():
State.mqtt_cfg[k] = str(data[k])
if "spot_chart" in data:
State.mqtt_cfg["spot_chart"] = bool(data["spot_chart"])
if "billing_tracker_enabled" in data:
State.mqtt_cfg["billing_tracker_enabled"] = bool(data["billing_tracker_enabled"])
for k in ("monthly_rate_eur", "grundpreis_eur_per_month"):
if k in data:
State.mqtt_cfg[k] = float(data[k])
save_config()
threading.Thread(target=_restart_all, daemon=True).start()
return jsonify({"ok": True})
@@ -493,6 +504,29 @@ def api_period_energy():
result[period_type] = entry
if State.mqtt_cfg.get("billing_tracker_enabled", False):
monthly_rate = float(State.mqtt_cfg.get("monthly_rate_eur", 0.0))
grundpreis = float(State.mqtt_cfg.get("grundpreis_eur_per_month", 0.0))
yr_key = history.period_key("yearly", billing_day, billing_month)
yr_start = datetime.date.fromisoformat(yr_key)
days_elapsed = (datetime.date.today() - yr_start).days
months_elapsed = round(days_elapsed / 30.4375, 4)
total_paid = round(months_elapsed * monthly_rate, 2)
grundpreis_total= round(months_elapsed * grundpreis, 2)
energy_cost = result.get("yearly", {}).get("import_cost", 0.0)
total_cost = round(energy_cost + grundpreis_total, 2)
nachzahlung = round(total_cost - total_paid, 2)
result["billing_tracker"] = {
"monthly_rate_eur": monthly_rate,
"grundpreis_eur_per_month": grundpreis,
"months_elapsed": round(months_elapsed, 1),
"total_paid_eur": total_paid,
"grundpreis_total_eur": grundpreis_total,
"energy_cost_eur": energy_cost,
"total_cost_eur": total_cost,
"nachzahlung_eur": nachzahlung,
}
return jsonify(result)
@app.get("/api/z2m-devices")
+64
View File
@@ -254,6 +254,23 @@
</div>
<div style="font-size:11px;color:var(--text-dim);margin-top:4px">z.B. 1 . 4 = 01. April</div>
</div>
<div class="field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:4px">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px">
<input type="checkbox" id="cfg-billing-tracker" style="width:auto;margin:0" onchange="updateBillingTrackerUI()">
<label for="cfg-billing-tracker" style="margin:0;cursor:pointer;font-size:13px;font-weight:600">Abschlags-Tracker aktivieren</label>
</div>
<div id="billing-tracker-fields" style="display:none">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
<div><label style="font-size:11px;color:var(--text-dim)">Monatliche Rate (€)</label>
<input type="number" id="cfg-monthly-rate" step="0.01" min="0" placeholder="120.00">
</div>
<div><label style="font-size:11px;color:var(--text-dim)">Grundpreis pro Monat (€)</label>
<input type="number" id="cfg-grundpreis" step="0.01" min="0" placeholder="12.50">
</div>
</div>
<div style="font-size:11px;color:var(--text-dim);margin-top:6px">Zeigt Nachzahlung / Guthaben für das laufende Abrechnungsjahr.</div>
</div>
</div>
<button class="btn btn-primary" onclick="savePrices()">Speichern</button>
</div>
@@ -648,9 +665,43 @@ function renderEnergy(inverters, aggregates, period, spotData) {
const spotHtml = (period.spot_chart !== false) ? renderSpotChart(spotData) : '';
const bt = period.billing_tracker;
const trackerHtml = bt ? (() => {
const nz = bt.nachzahlung_eur;
const isNachzahlung = nz > 0;
const nzColor = isNachzahlung ? '#e05c5c' : '#4caf82';
const nzLabel = isNachzahlung ? 'Voraussichtliche Nachzahlung' : 'Voraussichtliches Guthaben';
const nzSign = isNachzahlung ? '+' : '';
return `<div style="margin-top:16px;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:14px 16px">
<div style="font-size:10px;font-weight:700;letter-spacing:.08em;color:var(--text-dim);text-transform:uppercase;margin-bottom:12px">Abschlags-Übersicht · ${bt.months_elapsed} Monate</div>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:12px">
<div class="kwh-card">
<div class="kv">${fEur(bt.total_paid_eur)}</div>
<div class="kl">Bereits bezahlt<br><span style="opacity:.6">${bt.months_elapsed} × ${fEur(bt.monthly_rate_eur)}/Monat</span></div>
</div>
<div class="kwh-card">
<div class="kv">${fEur(bt.grundpreis_total_eur)}</div>
<div class="kl">Grundpreis anteilig<br><span style="opacity:.6">${fEur(bt.grundpreis_eur_per_month)}/Monat</span></div>
</div>
<div class="kwh-card">
<div class="kv">${fEur(bt.energy_cost_eur)}</div>
<div class="kl">Energiekosten<br><span style="opacity:.6">Netzbezug</span></div>
</div>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;background:var(--bg);border-radius:8px;padding:12px 16px">
<div>
<div style="font-size:12px;color:var(--text-dim)">${nzLabel}</div>
<div style="font-size:11px;color:var(--text-dim);margin-top:2px">Gesamtkosten ${fEur(bt.total_cost_eur)} Bezahlt ${fEur(bt.total_paid_eur)}</div>
</div>
<div style="font-size:24px;font-weight:700;color:${nzColor};font-variant-numeric:tabular-nums">${nzSign}${fEur(Math.abs(nz))}</div>
</div>
</div>`;
})() : '';
el.innerHTML = `<div class="energy-wrap">
<div class="energy-svg-wrap">${svg}${spotHtml}</div>
${cards ? `<div style="margin-top:16px">${cards}</div>` : ''}
${trackerHtml}
</div>`;
}
@@ -894,6 +945,11 @@ function updateTariffUI() {
document.getElementById("tariff-spot-fields").style.display = isSpot ? "" : "none";
}
function updateBillingTrackerUI() {
const enabled = document.getElementById("cfg-billing-tracker").checked;
document.getElementById("billing-tracker-fields").style.display = enabled ? "" : "none";
}
async function loadSettings() {
const cfg = await fetchJSON(api("api/config"));
globalConfig = cfg;
@@ -910,6 +966,11 @@ async function loadSettings() {
document.getElementById("cfg-spot-chart").checked = cfg.spot_chart ?? true;
document.getElementById((cfg.tariff_type ?? "fixed") === "spot" ? "tariff-spot" : "tariff-fixed").checked = true;
updateTariffUI();
const trackerEnabled = cfg.billing_tracker_enabled ?? false;
document.getElementById("cfg-billing-tracker").checked = trackerEnabled;
document.getElementById("billing-tracker-fields").style.display = trackerEnabled ? "" : "none";
document.getElementById("cfg-monthly-rate").value = cfg.monthly_rate_eur ?? 0;
document.getElementById("cfg-grundpreis").value = cfg.grundpreis_eur_per_month ?? 0;
}
async function savePrices() {
@@ -923,6 +984,9 @@ async function savePrices() {
spot_chart: document.getElementById("cfg-spot-chart").checked,
billing_day: parseInt(document.getElementById("cfg-billing-day").value),
billing_month: parseInt(document.getElementById("cfg-billing-month").value),
billing_tracker_enabled: document.getElementById("cfg-billing-tracker").checked,
monthly_rate_eur: parseFloat(document.getElementById("cfg-monthly-rate").value || 0),
grundpreis_eur_per_month: parseFloat(document.getElementById("cfg-grundpreis").value || 0),
};
try {
await fetchJSON(api("api/config"), {