revival zur Nutzung von Wärme für grosse PV Anlagen
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
@echo off
|
||||
REM Aufruf: revival.bat <kWp> [weitere Optionen]
|
||||
REM Beispiel: revival.bat 100
|
||||
REM revival.bat 250 --sonnenstunden 5.5 -v
|
||||
IF NOT DEFINED MILCH_HOME (
|
||||
SET MILCH_HOME=%~dp0..
|
||||
IF "%MILCH_HOME:~-1%"=="\" SET MILCH_HOME=%MILCH_HOME:~0,-1%
|
||||
)
|
||||
IF NOT DEFINED MILCH_LIB SET MILCH_LIB=%MILCH_HOME%\lib
|
||||
IF NOT DEFINED MILCH_CFG SET MILCH_CFG=%MILCH_HOME%\cfg
|
||||
IF NOT DEFINED MILCH_OUT SET MILCH_OUT=%MILCH_HOME%\out
|
||||
python "%MILCH_LIB%\revival.py" --config "%MILCH_CFG%\config.json" --out "%MILCH_OUT%" %*
|
||||
+386
@@ -0,0 +1,386 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Revival grosser PV-Anlagen: Strom als Waerme weiterverkaufen.
|
||||
|
||||
Berechnet fuer eine gegebene PV-Anlagengroesse (kWp), welche Waermemenge
|
||||
ueber eine Waermepumpe produziert werden kann und welche Leistung
|
||||
Waermepumpe und Waermetauscher haben muessen, um den taeglichen PV-Strom
|
||||
vollstaendig in Waerme umzuwandeln.
|
||||
|
||||
Aufruf:
|
||||
python revival.py 100
|
||||
python revival.py 250 --config cfg/config.json -v
|
||||
python revival.py 500 --sonnenstunden 5.5
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
def t(wert: float, nachkomma: int = 0) -> str:
|
||||
"""Zahl mit Hochkomma als Tausendertrenner formatieren."""
|
||||
if nachkomma > 0:
|
||||
s = f"{wert:,.{nachkomma}f}"
|
||||
else:
|
||||
s = f"{wert:,.0f}"
|
||||
return s.replace(",", "'")
|
||||
|
||||
|
||||
KJ_PER_KWH = 3600.0
|
||||
TAGE_PRO_JAHR = 365.0
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
|
||||
MONATSTAGE = {
|
||||
"Jan": 31, "Feb": 28, "Mae": 31, "Apr": 30, "Mai": 31, "Jun": 30,
|
||||
"Jul": 31, "Aug": 31, "Sep": 30, "Okt": 31, "Nov": 30, "Dez": 31,
|
||||
}
|
||||
|
||||
|
||||
def lade_config(pfad: Path) -> dict:
|
||||
with pfad.open(encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def monatsberechnung(kwp: float, cfg: dict) -> dict:
|
||||
"""Monatliche PV-Erzeugung, WP-Strom und Waermeproduktion (Luft + Wasser)."""
|
||||
pv = cfg["pv"]
|
||||
cop_luft = cfg["waermepumpe"]["cop"]
|
||||
cop_wasser = cfg["waermepumpe"].get("cop_wasser", 4.0)
|
||||
monats = pv["monatsertrag_kWh_pro_kWp"]
|
||||
detail = {}
|
||||
jahres_pv = 0.0
|
||||
jahres_waerme_luft = 0.0
|
||||
jahres_waerme_wasser = 0.0
|
||||
|
||||
for monat, tage in MONATSTAGE.items():
|
||||
pv_monat_kWh = monats[monat] * kwp
|
||||
pv_tag_kWh = pv_monat_kWh / tage
|
||||
wl_monat = pv_monat_kWh * cop_luft
|
||||
ww_monat = pv_monat_kWh * cop_wasser
|
||||
jahres_pv += pv_monat_kWh
|
||||
jahres_waerme_luft += wl_monat
|
||||
jahres_waerme_wasser += ww_monat
|
||||
detail[monat] = {
|
||||
"tage": tage,
|
||||
"pv_kWh": pv_monat_kWh,
|
||||
"pv_kWh_tag": pv_tag_kWh,
|
||||
"waerme_luft_kWh": wl_monat,
|
||||
"waerme_luft_kWh_tag": wl_monat / tage,
|
||||
"waerme_wasser_kWh": ww_monat,
|
||||
"waerme_wasser_kWh_tag": ww_monat / tage,
|
||||
}
|
||||
return {
|
||||
"monate": detail,
|
||||
"jahres_pv_kWh": jahres_pv,
|
||||
"jahres_waerme_luft_kWh": jahres_waerme_luft,
|
||||
"jahres_waerme_wasser_kWh": jahres_waerme_wasser,
|
||||
"cop_luft": cop_luft,
|
||||
"cop_wasser": cop_wasser,
|
||||
}
|
||||
|
||||
|
||||
def spitzenmonat(monate: dict) -> tuple[str, dict]:
|
||||
"""Monat mit der hoechsten taeglichen PV-Erzeugung."""
|
||||
return max(monate.items(), key=lambda x: x[1]["pv_kWh_tag"])
|
||||
|
||||
|
||||
def waermepumpe_auslegung(kwp: float, cfg: dict, sonnenstunden: float) -> dict:
|
||||
"""Erforderliche WP-Leistung um den PV-Strom tagsueberaufzunehmen.
|
||||
|
||||
sonnenstunden: aequivalente Volllaststunden Sonne pro Tag im Spitzenmonat.
|
||||
Die WP muss den gesamten Tagesstrom in diesen Stunden verarbeiten koennen.
|
||||
"""
|
||||
pv = cfg["pv"]
|
||||
cop = cfg["waermepumpe"]["cop"]
|
||||
|
||||
# Spitzenmonat: maximale taegliche Erzeugung
|
||||
monats = pv["monatsertrag_kWh_pro_kWp"]
|
||||
max_monat, max_ertrag = max(
|
||||
((m, monats[m] / MONATSTAGE[m]) for m in MONATSTAGE),
|
||||
key=lambda x: x[1],
|
||||
)
|
||||
pv_peak_tag_kWh = max_ertrag * kwp
|
||||
|
||||
# WP muss diese Energie in den Sonnenstunden aufnehmen
|
||||
wp_el_kW = pv_peak_tag_kWh / sonnenstunden
|
||||
wp_therm_kW = wp_el_kW * cop
|
||||
wp_kaelte_kW = wp_therm_kW - wp_el_kW # Verdampferseite
|
||||
|
||||
return {
|
||||
"spitzenmonat": max_monat,
|
||||
"pv_peak_tag_kWh": pv_peak_tag_kWh,
|
||||
"sonnenstunden": sonnenstunden,
|
||||
"wp_elektrisch_kW": wp_el_kW,
|
||||
"wp_thermisch_kW": wp_therm_kW,
|
||||
"wp_kaelte_kW": wp_kaelte_kW,
|
||||
}
|
||||
|
||||
|
||||
def waermetauscher_auslegung(wp: dict, sicherheitsfaktor: float = 1.2) -> dict:
|
||||
"""Erforderliche WT-Leistung auf Lade- und Entladeseite."""
|
||||
return {
|
||||
"ladeleistung_kW": wp["wp_thermisch_kW"] * sicherheitsfaktor,
|
||||
"entladeleistung_kW": wp["wp_thermisch_kW"] * sicherheitsfaktor,
|
||||
"sicherheitsfaktor": sicherheitsfaktor,
|
||||
"_hinweis": "Entladeseite kann kleiner sein wenn Entladung ueber laengere Zeit erfolgt",
|
||||
}
|
||||
|
||||
|
||||
def haushalte_versorgung(jahres_waerme_kWh: float, cfg: dict) -> dict:
|
||||
"""Wie viele Haushalte kann die Waerme versorgen?"""
|
||||
h = cfg["haushalt"]
|
||||
ww_tag = h["erwachsene"] * h["ww_erwachsener_kWh_tag"] + h["kinder"] * h["ww_kind_kWh_tag"]
|
||||
winter_tag = ww_tag + h["heizbedarf_winter_kWh_tag"]
|
||||
waerme_tag_schnitt = jahres_waerme_kWh / TAGE_PRO_JAHR
|
||||
return {
|
||||
"waerme_tag_schnitt_kWh": waerme_tag_schnitt,
|
||||
"ww_pro_haushalt_kWh_tag": ww_tag,
|
||||
"winter_pro_haushalt_kWh_tag": winter_tag,
|
||||
"haushalte_warmwasser": waerme_tag_schnitt / ww_tag if ww_tag > 0 else 0.0,
|
||||
"haushalte_winter": waerme_tag_schnitt / winter_tag if winter_tag > 0 else 0.0,
|
||||
}
|
||||
|
||||
|
||||
def wirtschaftlichkeit(kwp: float, jahres_pv_kWh: float,
|
||||
jahres_waerme_kWh: float, cfg: dict) -> dict:
|
||||
"""Erloes der Waerme im Vergleich zu fossilen Brennstoffen und Stromverkauf."""
|
||||
vergleich = []
|
||||
for b in cfg.get("brennstoffe", []):
|
||||
nw_ct = b["preis_ct_kWh"] / b["wirkungsgrad"]
|
||||
erloes_jahr = jahres_waerme_kWh * nw_ct / 100.0
|
||||
vergleich.append({
|
||||
"name": b["name"],
|
||||
"nutzwaerme_ct_kWh": nw_ct,
|
||||
"erloes_jahr_eur": erloes_jahr,
|
||||
})
|
||||
# PV-Gestehungskosten als Vergleich
|
||||
pv_gestehung = cfg.get("anbieter", {}).get("pv_gestehung_ct_kWh", 9.0)
|
||||
cop = cfg["waermepumpe"]["cop"]
|
||||
waerme_gestehung_ct = pv_gestehung / cop
|
||||
|
||||
# Stromverkauf als Alternative (Einspeisung / Direktvermarktung)
|
||||
pv_cfg = cfg.get("pv", {})
|
||||
einspeisung_ct = pv_cfg.get("einspeiseverguetung_ct_kWh", 8.1)
|
||||
direktvermarktung_ct = pv_cfg.get("direktvermarktung_ct_kWh", 7.0)
|
||||
erloes_einspeisung = jahres_pv_kWh * einspeisung_ct / 100.0
|
||||
erloes_direktvermarktung = jahres_pv_kWh * direktvermarktung_ct / 100.0
|
||||
|
||||
return {
|
||||
"pv_gestehung_ct_kWh": pv_gestehung,
|
||||
"waerme_gestehung_ct_kWh": waerme_gestehung_ct,
|
||||
"vergleich": vergleich,
|
||||
"stromverkauf": {
|
||||
"jahres_pv_kWh": jahres_pv_kWh,
|
||||
"einspeiseverguetung_ct_kWh": einspeisung_ct,
|
||||
"direktvermarktung_ct_kWh": direktvermarktung_ct,
|
||||
"erloes_einspeisung_eur": erloes_einspeisung,
|
||||
"erloes_direktvermarktung_eur": erloes_direktvermarktung,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def rechne(kwp: float, cfg: dict, sonnenstunden: float = 5.0) -> dict:
|
||||
mon = monatsberechnung(kwp, cfg)
|
||||
wp = waermepumpe_auslegung(kwp, cfg, sonnenstunden)
|
||||
wt = waermetauscher_auslegung(wp)
|
||||
hh = haushalte_versorgung(mon["jahres_waerme_luft_kWh"], cfg)
|
||||
wi = wirtschaftlichkeit(kwp, mon["jahres_pv_kWh"], mon["jahres_waerme_luft_kWh"], cfg)
|
||||
return {
|
||||
"eingang": {"kwp": kwp, "sonnenstunden": sonnenstunden},
|
||||
"cop_luft": mon["cop_luft"],
|
||||
"cop_wasser": mon["cop_wasser"],
|
||||
"monatsberechnung": mon,
|
||||
"waermepumpe": wp,
|
||||
"waermetauscher": wt,
|
||||
"haushalte": hh,
|
||||
"wirtschaftlichkeit": wi,
|
||||
}
|
||||
|
||||
|
||||
def report(e: dict, cfg: dict) -> str:
|
||||
wp = e["waermepumpe"]
|
||||
wt = e["waermetauscher"]
|
||||
mon = e["monatsberechnung"]
|
||||
hh = e["haushalte"]
|
||||
wi = e["wirtschaftlichkeit"]
|
||||
kwp = e["eingang"]["kwp"]
|
||||
cop_l = e["cop_luft"]
|
||||
cop_w = e["cop_wasser"]
|
||||
L = []
|
||||
|
||||
L.append(f"# PV-REVIVAL: {kwp:.0f} kWp Anlage -> Waermeproduktion")
|
||||
L.append("")
|
||||
L.append(f"Luft-WP COP {cop_l:.1f} / Wasser-WP COP {cop_w:.1f}, "
|
||||
f"{wp['sonnenstunden']:.1f} h aequiv. Sonnenstunden/Tag")
|
||||
|
||||
L.append("")
|
||||
L.append("## Jaehrliche Erzeugung")
|
||||
L.append("")
|
||||
L.append("| Kenngroesse | Wert | Einheit |")
|
||||
L.append("|:---|---:|:---|")
|
||||
L.append(f"| PV-Strom | {t(mon['jahres_pv_kWh'])} | kWh/a |")
|
||||
L.append(f"| Waerme Luft-WP (COP {cop_l:.1f}) | {t(mon['jahres_waerme_luft_kWh'])} | kWh/a |")
|
||||
L.append(f"| Waerme Wasser-WP (COP {cop_w:.1f}) | {t(mon['jahres_waerme_wasser_kWh'])} | kWh/a |")
|
||||
L.append(f"| Waerme Tagesmittel (Luft) | {t(hh['waerme_tag_schnitt_kWh'], 1)} | kWh/Tag |")
|
||||
|
||||
L.append("")
|
||||
L.append("## Erforderliche Waermepumpe")
|
||||
L.append("")
|
||||
L.append("| Kenngroesse | Wert | Einheit |")
|
||||
L.append("|:---|---:|:---|")
|
||||
L.append(f"| Spitzenmonat | {wp['spitzenmonat']} | |")
|
||||
L.append(f"| PV-Erzeugung Spitzentag | {t(wp['pv_peak_tag_kWh'], 1)} | kWh/Tag |")
|
||||
L.append(f"| Elektr. Leistung WP | {t(wp['wp_elektrisch_kW'], 1)} | kW |")
|
||||
L.append(f"| Thermische Leistung WP | {t(wp['wp_thermisch_kW'], 1)} | kW |")
|
||||
L.append(f"| Kaelteleistung (Verdampfer) | {t(wp['wp_kaelte_kW'], 1)} | kW |")
|
||||
|
||||
L.append("")
|
||||
L.append(f"## Erforderlicher Waermetauscher (Sicherheitsfaktor {wt['sicherheitsfaktor']:.1f}x)")
|
||||
L.append("")
|
||||
L.append("| Kenngroesse | Wert | Einheit |")
|
||||
L.append("|:---|---:|:---|")
|
||||
L.append(f"| Ladeleistung | {t(wt['ladeleistung_kW'], 1)} | kW |")
|
||||
L.append(f"| Entladeleistung | {t(wt['entladeleistung_kW'], 1)} | kW |")
|
||||
|
||||
L.append("")
|
||||
L.append("## Monatsuebersicht")
|
||||
L.append("")
|
||||
L.append(f"| Monat | PV kWh | kWh/Tag | Luft kWh (COP {cop_l:.1f}) | Wasser kWh (COP {cop_w:.1f}) |")
|
||||
L.append("|:---|---:|---:|---:|---:|")
|
||||
for monat in MONATSTAGE:
|
||||
m = mon["monate"][monat]
|
||||
L.append(f"| {monat} | {t(m['pv_kWh'])} | {t(m['pv_kWh_tag'], 1)} |"
|
||||
f" {t(m['waerme_luft_kWh'])} | {t(m['waerme_wasser_kWh'])} |")
|
||||
L.append(f"| **Summe** | **{t(mon['jahres_pv_kWh'])}** | |"
|
||||
f" **{t(mon['jahres_waerme_luft_kWh'])}** |"
|
||||
f" **{t(mon['jahres_waerme_wasser_kWh'])}** |")
|
||||
|
||||
# Mermaid-Diagramm
|
||||
L.append("")
|
||||
L.append("## Diagramm: Strom- und Waermeproduktion pro Monat")
|
||||
L.append("")
|
||||
L.append("```mermaid")
|
||||
L.append("%%{init: {'theme': 'base', 'themeVariables': {"
|
||||
"'xyChart': {'plotColorPalette': '#f0c808, #5bc0eb, #1d3557'}"
|
||||
"}}}%%")
|
||||
L.append("xychart-beta")
|
||||
L.append(f" title \"PV-Revival {kwp:.0f} kWp - Monatsproduktion (MWh)\"")
|
||||
L.append(" x-axis [Jan, Feb, Mae, Apr, Mai, Jun, Jul, Aug, Sep, Okt, Nov, Dez]")
|
||||
# Werte in MWh fuer bessere Lesbarkeit
|
||||
pv_vals = [f"{mon['monate'][m]['pv_kWh']/1000:.1f}" for m in MONATSTAGE]
|
||||
luft_vals = [f"{mon['monate'][m]['waerme_luft_kWh']/1000:.1f}" for m in MONATSTAGE]
|
||||
wasser_vals = [f"{mon['monate'][m]['waerme_wasser_kWh']/1000:.1f}" for m in MONATSTAGE]
|
||||
L.append(f" y-axis \"MWh\"")
|
||||
L.append(f" bar [{', '.join(pv_vals)}]")
|
||||
L.append(f" line [{', '.join(luft_vals)}]")
|
||||
L.append(f" line [{', '.join(wasser_vals)}]")
|
||||
L.append("```")
|
||||
L.append("")
|
||||
L.append(f"*Balken: PV-Strom | Linie 1: Waerme Luft-WP (COP {cop_l:.1f}) | "
|
||||
f"Linie 2: Waerme Wasser-WP (COP {cop_w:.1f}) -- Werte in MWh*")
|
||||
|
||||
L.append("")
|
||||
L.append(f"## Versorgbare Haushalte ({cfg['haushalt']['erwachsene']} Erw. + "
|
||||
f"{cfg['haushalt']['kinder']} Kinder, Tagesmittel)")
|
||||
L.append("")
|
||||
L.append("| Kenngroesse | Wert |")
|
||||
L.append("|:---|---:|")
|
||||
L.append(f"| Warmwasser ganzjaehrig | {t(hh['haushalte_warmwasser'])} Haushalte |")
|
||||
L.append(f"| Winter (Heizung + WW) | {t(hh['haushalte_winter'], 1)} Haushalte |")
|
||||
|
||||
if wi["vergleich"]:
|
||||
L.append("")
|
||||
L.append("## Wirtschaftlichkeit")
|
||||
L.append("")
|
||||
L.append("| Kenngroesse | Wert | Einheit |")
|
||||
L.append("|:---|---:|:---|")
|
||||
L.append(f"| PV-Stromgestehung | {t(wi['pv_gestehung_ct_kWh'], 1)} | ct/kWh |")
|
||||
L.append(f"| Waermegestehung (/ COP) | {t(wi['waerme_gestehung_ct_kWh'], 1)} | ct/kWh |")
|
||||
L.append("")
|
||||
L.append("Zum Vergleich: Kosten der anderen Brennstoffe (Nutzwaerme-Preis):")
|
||||
L.append("")
|
||||
L.append("| Brennstoff | ct/kWh | Erloes EUR/a |")
|
||||
L.append("|:---|---:|---:|")
|
||||
for v in wi["vergleich"]:
|
||||
L.append(f"| {v['name']} | {t(v['nutzwaerme_ct_kWh'], 1)} | {t(v['erloes_jahr_eur'])} |")
|
||||
L.append("")
|
||||
L.append("*Erloes = Waerme bewertet zu Ersatzbrennstoffkosten*")
|
||||
|
||||
sv = wi["stromverkauf"]
|
||||
L.append("")
|
||||
L.append("### Alternative: Strom direkt verkaufen")
|
||||
L.append("")
|
||||
L.append("| Variante | ct/kWh | Erloes EUR/a |")
|
||||
L.append("|:---|---:|---:|")
|
||||
L.append(f"| Einspeiseverguetung (EEG) | {t(sv['einspeiseverguetung_ct_kWh'], 1)} |"
|
||||
f" {t(sv['erloes_einspeisung_eur'])} |")
|
||||
L.append(f"| Direktvermarktung | {t(sv['direktvermarktung_ct_kWh'], 1)} |"
|
||||
f" {t(sv['erloes_direktvermarktung_eur'])} |")
|
||||
L.append("")
|
||||
# Vergleich: bester Waerme-Erloes vs. Stromverkauf
|
||||
best_waerme = max(wi["vergleich"], key=lambda v: v["erloes_jahr_eur"])
|
||||
vorteil = best_waerme["erloes_jahr_eur"] - sv["erloes_einspeisung_eur"]
|
||||
if vorteil > 0:
|
||||
L.append(f"*Waermeverkauf (vs. {best_waerme['name']}) bringt "
|
||||
f"{t(vorteil)} EUR/a mehr als Einspeisung.*")
|
||||
else:
|
||||
L.append(f"*Einspeisung bringt {t(-vorteil)} EUR/a mehr als "
|
||||
f"Waermeverkauf (vs. {best_waerme['name']}).*")
|
||||
|
||||
return "\n".join(L)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("kwp", type=float, help="Groesse der PV-Anlage in kWp")
|
||||
ap.add_argument("--config", type=Path,
|
||||
default=PROJECT_ROOT / "cfg" / "config.json",
|
||||
help="Konfiguration (Standard: cfg/config.json)")
|
||||
ap.add_argument("--sonnenstunden", type=float, default=5.0,
|
||||
help="Aequivalente Volllaststunden Sonne pro Tag im Spitzenmonat "
|
||||
"(Standard: 5.0 h, bestimmt die noetige WP-Spitzenleistung)")
|
||||
ap.add_argument("--out", type=Path, default=None,
|
||||
help="Ausgabeverzeichnis (Standard: $MILCH_OUT oder out/)")
|
||||
ap.add_argument("--json", type=Path, default=None,
|
||||
help="Ergebnis zusaetzlich als JSON schreiben")
|
||||
ap.add_argument("-v", "--verbose", action="store_true",
|
||||
help="Ausgaben auf der Konsole")
|
||||
args = ap.parse_args()
|
||||
|
||||
out_ordner = (
|
||||
args.out
|
||||
or Path(os.environ.get("MILCH_OUT", ""))
|
||||
or PROJECT_ROOT / "out"
|
||||
)
|
||||
if not out_ordner or str(out_ordner) == ".":
|
||||
out_ordner = PROJECT_ROOT / "out"
|
||||
out_ordner = Path(out_ordner)
|
||||
out_ordner.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
cfg = lade_config(args.config)
|
||||
ergebnis = rechne(args.kwp, cfg, sonnenstunden=args.sonnenstunden)
|
||||
text = report(ergebnis, cfg)
|
||||
|
||||
if args.verbose:
|
||||
print(text)
|
||||
|
||||
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
md_name = f"revival_{args.kwp:.0f}kWp_{ts}.md"
|
||||
md_path = out_ordner / md_name
|
||||
md_path.write_text(text, encoding="utf-8")
|
||||
if args.verbose:
|
||||
print(f"\nReport geschrieben: {md_path}")
|
||||
|
||||
if args.json:
|
||||
with args.json.open("w", encoding="utf-8") as f:
|
||||
json.dump(ergebnis, f, ensure_ascii=False, indent=2)
|
||||
if args.verbose:
|
||||
print(f"JSON geschrieben: {args.json}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user