diff --git a/haos-addon/config.yaml b/haos-addon/config.yaml index c46d7af..602160e 100644 --- a/haos-addon/config.yaml +++ b/haos-addon/config.yaml @@ -1,5 +1,5 @@ name: ShineBridge -version: "1.8.0" +version: "1.8.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/main.py b/haos-addon/src/main.py index 210ef80..6b5b9d2 100644 --- a/haos-addon/src/main.py +++ b/haos-addon/src/main.py @@ -90,6 +90,10 @@ def _defaults() -> Dict[str, Any]: "price_export": 0.08, "billing_day": 1, "billing_month": 1, + "tariff_type": "fixed", + "spot_country": "de", + "spot_markup": 0.0, + "spot_chart": True, "inverters": [], } @@ -122,6 +126,10 @@ def save_config(): "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), + "tariff_type": State.mqtt_cfg.get("tariff_type", "fixed"), + "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), "inverters": State.inverters_cfg, } with open(CONFIG_PATH, "w") as f: @@ -346,12 +354,17 @@ def api_save_config(): State.mqtt_cfg[k] = data[k] if data.get("mqtt_pass"): State.mqtt_cfg["mqtt_pass"] = data["mqtt_pass"] - for k in ("price_import", "price_export"): + for k in ("price_import", "price_export", "spot_markup"): 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]) + for k in ("tariff_type", "spot_country"): + if k in data: + State.mqtt_cfg[k] = str(data[k]) + if "spot_chart" in data: + State.mqtt_cfg["spot_chart"] = bool(data["spot_chart"]) save_config() threading.Thread(target=_restart_all, daemon=True).start() return jsonify({"ok": True}) @@ -365,29 +378,34 @@ def api_period_energy(): 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)) + tariff_type = str(State.mqtt_cfg.get("tariff_type", "fixed")) + spot_country = str(State.mqtt_cfg.get("spot_country", "de")) + spot_markup = float(State.mqtt_cfg.get("spot_markup", 0.0)) # ct/kWh result = { "price_import": price_import, "price_export": price_export, - "billing_day": billing_day, - "billing_month": billing_month, + "tariff_type": tariff_type, + "spot_chart": bool(State.mqtt_cfg.get("spot_chart", True)), } MONTHS_DE = ["","Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"] + now_ts = time.time() for period_type in ("monthly", "yearly"): key = history.period_key(period_type, billing_day, billing_month) entry = {} - # Lesbare Beschriftung + # Perioden-Startzeitpunkt für Spot-Preisabfrage if period_type == "monthly": - d = datetime.date.fromisoformat(key + "-01") - entry["label"] = f"{MONTHS_DE[d.month]} {d.year}" + pd = datetime.date.fromisoformat(key + "-01") + entry["label"] = f"{MONTHS_DE[pd.month]} {pd.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')}" + pd = datetime.date.fromisoformat(key) + end_d = datetime.date(pd.year + 1, billing_month, billing_day) if billing_month != 1 or billing_day != 1 \ + else datetime.date(pd.year + 1, 1, 1) + entry["label"] = f"{pd.strftime('%d.%m.%Y')} – {(end_d - datetime.timedelta(days=1)).strftime('%d.%m.%Y')}" + pd_start_ts = datetime.datetime.combine(pd, datetime.time.min).timestamp() for agg_id in ("grid_import_kwh", "grid_export_kwh"): cur = agg.get(agg_id) @@ -397,10 +415,24 @@ def api_period_energy(): val = history.get_period_consumption(agg_id, period_type, key, cur) if val is not None: entry[agg_id] = round(val, 2) + + # Kostenberechnung: Festpreis oder Börsenpreis if "grid_import_kwh" in entry: - entry["import_cost"] = round(entry["grid_import_kwh"] * price_import, 2) + if tariff_type == "spot": + avg_ct = _get_avg_spot_price(pd_start_ts, now_ts, spot_country) + if avg_ct is not None: + eff_price = (avg_ct + spot_markup) / 100 # ct/kWh → €/kWh + entry["import_cost"] = round(entry["grid_import_kwh"] * eff_price, 2) + entry["spot_avg_ct"] = avg_ct + entry["spot_markup_ct"] = spot_markup + entry["effective_price"]= round(eff_price, 4) + else: + entry["import_cost"] = round(entry["grid_import_kwh"] * price_import, 2) + else: + entry["import_cost"] = round(entry["grid_import_kwh"] * price_import, 2) if "grid_export_kwh" in entry: entry["export_revenue"] = round(entry["grid_export_kwh"] * price_export, 2) + result[period_type] = entry return jsonify(result) @@ -545,6 +577,31 @@ def api_import_config(): threading.Thread(target=_restart_all, daemon=True).start() return jsonify({"ok": True, "inverters": len(inverters)}) +_hist_spot_cache: Dict[str, Any] = {} # key → {"ts", "avg_ct"} + +def _get_avg_spot_price(start_ts: float, end_ts: float, country: str = "de") -> Optional[float]: + """Durchschnittlicher EPEX-SPOT-Preis (ct/kWh) für einen Zeitraum. None bei Fehler.""" + import urllib.request as _ur + cache_key = f"{country}-{int(start_ts//3600)}-{int(end_ts//3600)}" + now = time.time() + cached = _hist_spot_cache.get(cache_key) + if cached and now - cached["ts"] < 3600: + return cached["avg_ct"] + try: + url = (f"https://api.awattar.{country}/v1/marketdata" + f"?start={int(start_ts*1000)}&end={int(end_ts*1000)}") + with _ur.urlopen(url, timeout=10) as r: + raw = json.loads(r.read()) + prices = [d["marketprice"] / 10 for d in raw.get("data", [])] + if not prices: + return None + avg = round(sum(prices) / len(prices), 2) + _hist_spot_cache[cache_key] = {"ts": now, "avg_ct": avg} + return avg + except Exception as e: + log.warning("Historischer Spot-Preis Fehler: %s", e) + return None + _spot_cache: Dict[str, Any] = {"ts": 0.0, "data": []} _SPOT_TTL = 900 # 15 Minuten diff --git a/haos-addon/src/web/index.html b/haos-addon/src/web/index.html index 5d22248..2e237b7 100644 --- a/haos-addon/src/web/index.html +++ b/haos-addon/src/web/index.html @@ -208,11 +208,40 @@