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:
@@ -1,5 +1,5 @@
|
|||||||
name: ShineBridge
|
name: ShineBridge
|
||||||
version: "1.8.13"
|
version: "1.8.14"
|
||||||
slug: shinebridge
|
slug: shinebridge
|
||||||
description: Growatt Wechselrichter lokal in Home Assistant — Modbus TCP via ShineLAN-X, MQTT Discovery, Web UI
|
description: Growatt Wechselrichter lokal in Home Assistant — Modbus TCP via ShineLAN-X, MQTT Discovery, Web UI
|
||||||
url: https://gitea.bitfire.work/retr0/shinebridge
|
url: https://gitea.bitfire.work/retr0/shinebridge
|
||||||
|
|||||||
@@ -101,6 +101,9 @@ def _defaults() -> Dict[str, Any]:
|
|||||||
"spot_country": "de",
|
"spot_country": "de",
|
||||||
"spot_markup": 0.0,
|
"spot_markup": 0.0,
|
||||||
"spot_chart": True,
|
"spot_chart": True,
|
||||||
|
"billing_tracker_enabled": False,
|
||||||
|
"monthly_rate_eur": 0.0,
|
||||||
|
"grundpreis_eur_per_month": 0.0,
|
||||||
"inverters": [],
|
"inverters": [],
|
||||||
"surplus_devices": [],
|
"surplus_devices": [],
|
||||||
"z2m_base": "zigbee2mqtt",
|
"z2m_base": "zigbee2mqtt",
|
||||||
@@ -139,6 +142,9 @@ def save_config():
|
|||||||
"spot_country": State.mqtt_cfg.get("spot_country", "de"),
|
"spot_country": State.mqtt_cfg.get("spot_country", "de"),
|
||||||
"spot_markup": State.mqtt_cfg.get("spot_markup", 0.0),
|
"spot_markup": State.mqtt_cfg.get("spot_markup", 0.0),
|
||||||
"spot_chart": State.mqtt_cfg.get("spot_chart", True),
|
"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,
|
"inverters": State.inverters_cfg,
|
||||||
"surplus_devices": State.surplus_devices_cfg,
|
"surplus_devices": State.surplus_devices_cfg,
|
||||||
"z2m_base": State.z2m_base,
|
"z2m_base": State.z2m_base,
|
||||||
@@ -410,6 +416,11 @@ def api_save_config():
|
|||||||
State.mqtt_cfg[k] = str(data[k])
|
State.mqtt_cfg[k] = str(data[k])
|
||||||
if "spot_chart" in data:
|
if "spot_chart" in data:
|
||||||
State.mqtt_cfg["spot_chart"] = bool(data["spot_chart"])
|
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()
|
save_config()
|
||||||
threading.Thread(target=_restart_all, daemon=True).start()
|
threading.Thread(target=_restart_all, daemon=True).start()
|
||||||
return jsonify({"ok": True})
|
return jsonify({"ok": True})
|
||||||
@@ -493,6 +504,29 @@ def api_period_energy():
|
|||||||
|
|
||||||
result[period_type] = entry
|
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)
|
return jsonify(result)
|
||||||
|
|
||||||
@app.get("/api/z2m-devices")
|
@app.get("/api/z2m-devices")
|
||||||
|
|||||||
@@ -254,6 +254,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="font-size:11px;color:var(--text-dim);margin-top:4px">z.B. 1 . 4 = 01. April</div>
|
<div style="font-size:11px;color:var(--text-dim);margin-top:4px">z.B. 1 . 4 = 01. April</div>
|
||||||
</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>
|
<button class="btn btn-primary" onclick="savePrices()">Speichern</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -648,9 +665,43 @@ function renderEnergy(inverters, aggregates, period, spotData) {
|
|||||||
|
|
||||||
const spotHtml = (period.spot_chart !== false) ? renderSpotChart(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">
|
el.innerHTML = `<div class="energy-wrap">
|
||||||
<div class="energy-svg-wrap">${svg}${spotHtml}</div>
|
<div class="energy-svg-wrap">${svg}${spotHtml}</div>
|
||||||
${cards ? `<div style="margin-top:16px">${cards}</div>` : ''}
|
${cards ? `<div style="margin-top:16px">${cards}</div>` : ''}
|
||||||
|
${trackerHtml}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -894,6 +945,11 @@ function updateTariffUI() {
|
|||||||
document.getElementById("tariff-spot-fields").style.display = isSpot ? "" : "none";
|
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() {
|
async function loadSettings() {
|
||||||
const cfg = await fetchJSON(api("api/config"));
|
const cfg = await fetchJSON(api("api/config"));
|
||||||
globalConfig = cfg;
|
globalConfig = cfg;
|
||||||
@@ -910,6 +966,11 @@ async function loadSettings() {
|
|||||||
document.getElementById("cfg-spot-chart").checked = cfg.spot_chart ?? true;
|
document.getElementById("cfg-spot-chart").checked = cfg.spot_chart ?? true;
|
||||||
document.getElementById((cfg.tariff_type ?? "fixed") === "spot" ? "tariff-spot" : "tariff-fixed").checked = true;
|
document.getElementById((cfg.tariff_type ?? "fixed") === "spot" ? "tariff-spot" : "tariff-fixed").checked = true;
|
||||||
updateTariffUI();
|
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() {
|
async function savePrices() {
|
||||||
@@ -923,6 +984,9 @@ async function savePrices() {
|
|||||||
spot_chart: document.getElementById("cfg-spot-chart").checked,
|
spot_chart: document.getElementById("cfg-spot-chart").checked,
|
||||||
billing_day: parseInt(document.getElementById("cfg-billing-day").value),
|
billing_day: parseInt(document.getElementById("cfg-billing-day").value),
|
||||||
billing_month: parseInt(document.getElementById("cfg-billing-month").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 {
|
try {
|
||||||
await fetchJSON(api("api/config"), {
|
await fetchJSON(api("api/config"), {
|
||||||
|
|||||||
Reference in New Issue
Block a user