diff --git a/haos-addon/config.yaml b/haos-addon/config.yaml
index 565b5da..f82e9aa 100644
--- a/haos-addon/config.yaml
+++ b/haos-addon/config.yaml
@@ -1,5 +1,5 @@
name: ShineBridge
-version: "1.7.0"
+version: "1.7.1"
slug: shinebridge
description: Growatt Wechselrichter lokal in Home Assistant — Modbus TCP via ShineLAN-X, MQTT Discovery, Web UI
url: https://gitea.bitfire.work/retr0/shinebridge
diff --git a/haos-addon/src/history.py b/haos-addon/src/history.py
index 18272c8..080c648 100644
--- a/haos-addon/src/history.py
+++ b/haos-addon/src/history.py
@@ -52,10 +52,22 @@ def init_db():
log.info("History DB initialisiert: %s", DB_PATH)
-def period_key(period_type: str) -> str:
+def period_key(period_type: str, billing_day: int = 1, billing_month: int = 1) -> str:
import datetime
- now = datetime.date.today()
- return now.strftime("%Y-%m") if period_type == "monthly" else now.strftime("%Y")
+ today = datetime.date.today()
+ if period_type == "monthly":
+ return today.strftime("%Y-%m")
+ # Jahresperiode: Beginn = letzter Abrechnungsstichtag
+ try:
+ start = datetime.date(today.year, billing_month, billing_day)
+ except ValueError:
+ start = datetime.date(today.year, billing_month, 1)
+ if today < start:
+ try:
+ start = datetime.date(today.year - 1, billing_month, billing_day)
+ except ValueError:
+ start = datetime.date(today.year - 1, billing_month, 1)
+ return start.isoformat() # z.B. "2025-04-01"
def save_period_start_if_new(agg_id: str, period_type: str, key: str, current_value: float):
diff --git a/haos-addon/src/main.py b/haos-addon/src/main.py
index 3a60831..e54d597 100644
--- a/haos-addon/src/main.py
+++ b/haos-addon/src/main.py
@@ -88,6 +88,8 @@ def _defaults() -> Dict[str, Any]:
"mqtt_pass": "",
"price_import": 0.30,
"price_export": 0.08,
+ "billing_day": 1,
+ "billing_month": 1,
"inverters": [],
}
@@ -118,6 +120,8 @@ def save_config():
"mqtt_pass": State.mqtt_cfg.get("mqtt_pass", ""),
"price_import": State.mqtt_cfg.get("price_import", 0.30),
"price_export": State.mqtt_cfg.get("price_export", 0.08),
+ "billing_day": State.mqtt_cfg.get("billing_day", 1),
+ "billing_month": State.mqtt_cfg.get("billing_month", 1),
"inverters": State.inverters_cfg,
}
with open(CONFIG_PATH, "w") as f:
@@ -345,6 +349,9 @@ def api_save_config():
for k in ("price_import", "price_export"):
if k in data:
State.mqtt_cfg[k] = float(data[k])
+ for k in ("billing_day", "billing_month"):
+ if k in data:
+ State.mqtt_cfg[k] = int(data[k])
save_config()
threading.Thread(target=_restart_all, daemon=True).start()
return jsonify({"ok": True})
@@ -352,16 +359,37 @@ def api_save_config():
@app.get("/api/period-energy")
def api_period_energy():
+ import datetime
agg = _compute_aggregates()
- price_import = float(State.mqtt_cfg.get("price_import", 0.30))
- price_export = float(State.mqtt_cfg.get("price_export", 0.08))
+ price_import = float(State.mqtt_cfg.get("price_import", 0.30))
+ price_export = float(State.mqtt_cfg.get("price_export", 0.08))
+ billing_day = int(State.mqtt_cfg.get("billing_day", 1))
+ billing_month = int(State.mqtt_cfg.get("billing_month", 1))
- result = {"price_import": price_import, "price_export": price_export}
+ result = {
+ "price_import": price_import,
+ "price_export": price_export,
+ "billing_day": billing_day,
+ "billing_month": billing_month,
+ }
+
+ MONTHS_DE = ["","Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"]
for period_type in ("monthly", "yearly"):
- key = history.period_key(period_type)
+ key = history.period_key(period_type, billing_day, billing_month)
entry = {}
- for agg_id in ("grid_import_kwh", "grid_export_kwh", "total_energy_today"):
+
+ # Lesbare Beschriftung
+ if period_type == "monthly":
+ d = datetime.date.fromisoformat(key + "-01")
+ entry["label"] = f"{MONTHS_DE[d.month]} {d.year}"
+ else:
+ d = datetime.date.fromisoformat(key)
+ end = datetime.date(d.year + 1, billing_month, billing_day) if billing_month != 1 or billing_day != 1 \
+ else datetime.date(d.year + 1, 1, 1)
+ entry["label"] = f"{d.strftime('%d.%m.%Y')} – {(end - datetime.timedelta(days=1)).strftime('%d.%m.%Y')}"
+
+ for agg_id in ("grid_import_kwh", "grid_export_kwh"):
cur = agg.get(agg_id)
if cur is None:
continue
diff --git a/haos-addon/src/web/index.html b/haos-addon/src/web/index.html
index 89cea89..abbc2a1 100644
--- a/haos-addon/src/web/index.html
+++ b/haos-addon/src/web/index.html
@@ -213,7 +213,16 @@
-
+
+
@@ -424,22 +433,22 @@ function renderEnergy(inverters, aggregates, period) {
};
function icon(name, cx, cy, col, sz) {
- sz = sz || 22;
+ sz = sz || 26;
const s = (sz / 20).toFixed(4);
return `
${ICONS[name]}`;
}
// Circle node — HA style
- const R = 44;
+ const R = 56;
function node(cx, cy, iconName, topLabel, valW, col, active, sub) {
const c = active ? col : C.dim;
const sw = active ? 2 : 1;
const fi = active ? '0.08' : '0.03';
return `
- ${icon(iconName, cx, cy - 11, c, 22)}
- ${fW(valW)}
- ${topLabel}${sub?' · '+sub:''}
+ ${icon(iconName, cx, cy - 15, c, 26)}
+ ${fW(valW)}
+ ${topLabel}${sub?' · '+sub:''}
`;
}
@@ -457,15 +466,13 @@ function renderEnergy(inverters, aggregates, period) {
).join('');
}
- // Path segments (bezier from node-edge to node-edge)
- // Solar(145,75) bottom→ House(260,180) top; Grid(375,75) bottom→ House top
- // House bottom → Battery(145,292) top; House bottom → EV(375,292) top
- // Kreuz-Layout: Solar oben, Grid links, Haus Mitte, Batterie rechts, Wallbox unten (optional)
+ // Kreuz-Layout: Solar(260,72) oben, Grid(108,224) links, Haus(260,224) Mitte,
+ // Batterie(412,224) rechts, Wallbox(260,376) unten (optional). R=56, Abstand=40px.
const SEG = [
- { id:'ep-pv', d:`M 260,121 C 258,138 262,146 260,158`, col:C.pv, on:pvOn, rev:false },
- { id:'ep-grid', d:`M 140,200 C 162,197 192,203 214,200`, col:impOn?C.imp:C.exp, on:impOn||expOn, rev:expOn },
- { id:'ep-bat', d:`M 306,200 C 330,197 356,203 380,200`, col:C.bat, on:chOn||dchOn, rev:dchOn },
- ...(hasEV ? [{ id:'ep-ev', d:`M 260,244 C 258,261 262,269 260,279`, col:C.ev, on:evOn, rev:false }] : []),
+ { id:'ep-pv', d:`M 260,128 C 258,148 262,152 260,168`, col:C.pv, on:pvOn, rev:false },
+ { id:'ep-grid', d:`M 164,224 C 183,220 185,228 204,224`, col:impOn?C.imp:C.exp, on:impOn||expOn, rev:expOn },
+ { id:'ep-bat', d:`M 316,224 C 335,220 337,228 356,224`, col:C.bat, on:chOn||dchOn, rev:dchOn },
+ ...(hasEV ? [{ id:'ep-ev', d:`M 260,280 C 258,300 262,304 260,320`, col:C.ev, on:evOn, rev:false }] : []),
];
const defs = SEG.map(s => `
`).join('');
@@ -481,16 +488,16 @@ function renderEnergy(inverters, aggregates, period) {
const batLbl = chOn ? 'LADEN' : dchOn ? 'ENTLADEN' : 'BATTERIE';
const batSub = batSoc != null ? Math.round(batSoc) + '%' : '';
- const svgH = hasEV ? 410 : 278;
+ const svgH = hasEV ? 460 : 310;
const svg = `
`;
period = period || {};
@@ -525,8 +532,8 @@ function renderEnergy(inverters, aggregates, period) {
${imp}${exp}
`;
}
- const cards = sectionCards('Diesen Monat', mon, C.imp) +
- sectionCards('Dieses Jahr', yr, C.imp);
+ const cards = sectionCards(mon.label || 'Diesen Monat', mon, C.imp) +
+ sectionCards(yr.label || 'Dieses Jahr', yr, C.imp);
el.innerHTML = `
${svg}
@@ -768,12 +775,16 @@ async function loadSettings() {
document.getElementById("cfg-mqtt-user").value = globalConfig.mqtt_user || "";
document.getElementById("cfg-price-import").value = globalConfig.price_import ?? 0.30;
document.getElementById("cfg-price-export").value = globalConfig.price_export ?? 0.08;
+ document.getElementById("cfg-billing-day").value = globalConfig.billing_day ?? 1;
+ document.getElementById("cfg-billing-month").value = globalConfig.billing_month ?? 1;
}
async function savePrices() {
const body = {
- price_import: parseFloat(document.getElementById("cfg-price-import").value),
- price_export: parseFloat(document.getElementById("cfg-price-export").value),
+ price_import: parseFloat(document.getElementById("cfg-price-import").value),
+ price_export: parseFloat(document.getElementById("cfg-price-export").value),
+ billing_day: parseInt(document.getElementById("cfg-billing-day").value),
+ billing_month: parseInt(document.getElementById("cfg-billing-month").value),
};
try {
await fetchJSON(api("api/config"), {