diff --git a/auslegung.py b/auslegung.py deleted file mode 100644 index 91d991f..0000000 --- a/auslegung.py +++ /dev/null @@ -1,356 +0,0 @@ -#!/usr/bin/env python3 -"""Auslegung einer Waermerueckgewinnung aus der Milchkuehlung. - -Eingangsgroesse: zu kuehlende Milchmenge pro Tag (Liter). -Ausgabe: erforderliche Groesse von Waermepumpe, PV-Anlage, Latentspeicher -(Natriumacetat) und Batterie. - -Alle Annahmen stehen in einer JSON-Datei (Standard: config.json), damit sich -Varianten leicht nachrechnen lassen. - -Aufruf: - python auslegung.py 1000 - python auslegung.py 1500 --config variante_winterautark.json - python auslegung.py 1000 --json ergebnis.json -""" -from __future__ import annotations - -import argparse -import json -import math -from pathlib import Path - -KJ_PER_KWH = 3600.0 -TAGE_PRO_JAHR = 365.0 - -# Tage je Monat (Schluessel muessen zu pv.monatsertrag_kWh_pro_kWp passen) -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 milch_kuehllast(milch_l_tag: float, cfg: dict) -> float: - """Taeglich der Milch zu entziehende Waerme in kWh/Tag.""" - m = cfg["milch"] - masse = milch_l_tag * m["dichte_kg_pro_L"] - dT = m["temp_ein_C"] - m["temp_aus_C"] - q_kj = masse * m["spez_waerme_kJ_pro_kgK"] * dT - return q_kj / KJ_PER_KWH - - -def waermepumpe(q_milch_tag: float, cfg: dict) -> dict: - """Strombedarf und Leistungsdaten der Waermepumpe. - - Q_speicher = Q_milch + W_el (die Verdichterarbeit landet als Waerme im Speicher). - """ - hp = cfg["waermepumpe"] - cop = hp["cop"] - if cop <= 1.0: - raise ValueError("COP muss > 1 sein.") - w_el_tag = q_milch_tag / (cop - 1.0) - q_speicher_tag = q_milch_tag + w_el_tag - laufzeit = hp["laufzeit_h_pro_tag"] - return { - "strom_kWh_tag": w_el_tag, - "speicher_kWh_tag": q_speicher_tag, - "kaelteleistung_kW": q_milch_tag / laufzeit, # Verdampferseite - "heizleistung_kW": q_speicher_tag / laufzeit, # Kondensatorseite - "elektrische_leistung_kW": w_el_tag / laufzeit, - } - - -def pcm_energiedichte_kWh_pro_kg(cfg: dict) -> float: - """Nutzbare Energiedichte des PCM je nach Modus.""" - p = cfg["pcm"] - e_kj = p["latentwaerme_kJ_pro_kg"] - modus = p["modus"] - if modus == "latent_only": - pass - elif modus == "latent_plus_hub": - ueberhitzung = max(0.0, p["ladetemp_C"] - p["schmelzpunkt_C"]) - unterkuehlung = max(0.0, p["schmelzpunkt_C"] - p["entladetemp_C"]) - e_kj += p["cp_fluessig_kJ_pro_kgK"] * ueberhitzung - e_kj += p["cp_fest_kJ_pro_kgK"] * unterkuehlung - else: - raise ValueError(f"Unbekannter pcm.modus: {modus!r}") - return e_kj / KJ_PER_KWH - - -def speicher(q_speicher_tag: float, cfg: dict, zyklus: float | None = None) -> dict: - """Groesse des Latentspeichers fuer einen Abholzyklus + LKW-Abgleich. - - zyklus: Abholzyklus in Tagen. None -> Wert aus der Config. - """ - a = cfg["abholung"] - p = cfg["pcm"] - if zyklus is None: - zyklus = a["zyklus_tage"] - e_kg = pcm_energiedichte_kWh_pro_kg(cfg) - - e_zyklus = q_speicher_tag * zyklus # kWh je Abholzyklus - masse_kg = e_zyklus / e_kg - volumen_m3 = masse_kg / (p["dichte_kg_pro_L"] * 1000.0) - - nutzlast = a["lkw_nutzlast_kg"] - lkw_vol = a["lkw_volumen_m3"] - # Welche Grenze bindet? - if masse_kg > nutzlast or volumen_m3 > lkw_vol: - bindend = "Gewicht" if masse_kg / nutzlast >= volumen_m3 / lkw_vol else "Volumen" - passt = False - else: - bindend = "Gewicht" if masse_kg / nutzlast >= volumen_m3 / lkw_vol else "Volumen" - passt = True - - # Wie oft muesste ein voller LKW (nach Gewicht) getauscht werden? - masse_pro_tag = q_speicher_tag / e_kg - voll_lkw_tage = nutzlast / masse_pro_tag if masse_pro_tag > 0 else float("inf") - - return { - "energiedichte_kWh_pro_kg": e_kg, - "energie_pro_zyklus_kWh": e_zyklus, - "masse_kg": masse_kg, - "volumen_m3": volumen_m3, - "auslastung_gewicht": masse_kg / nutzlast, - "auslastung_volumen": volumen_m3 / lkw_vol, - "bindende_grenze": bindend, - "passt_in_einen_lkw": passt, - "voller_lkw_tausch_tage": voll_lkw_tage, - } - - -def pv_anlage(w_el_tag: float, cfg: dict) -> dict: - """PV-Groesse fuer Jahresbilanz des WP-Strombedarfs.""" - pv = cfg["pv"] - jahresbedarf = w_el_tag * TAGE_PRO_JAHR - kwp = jahresbedarf / pv["spez_ertrag_kWh_pro_kWp_jahr"] - flaeche = kwp * pv["flaeche_m2_pro_kWp"] - return { - "jahresbedarf_kWh": jahresbedarf, - "kWp": kwp, - "flaeche_m2": flaeche, - } - - -def netzbezug(w_el_tag: float, kwp: float, cfg: dict) -> dict: - """Monatsbilanz -> jaehrlicher Netzbezug durch den Winter.""" - monats = cfg["pv"]["monatsertrag_kWh_pro_kWp"] - bezug = 0.0 - ueberschuss = 0.0 - jahresbedarf = 0.0 - monatsdetail = {} - for monat, tage in MONATSTAGE.items(): - bedarf = w_el_tag * tage - erzeugung = monats[monat] * kwp - jahresbedarf += bedarf - defizit = max(0.0, bedarf - erzeugung) - ueber = max(0.0, erzeugung - bedarf) - bezug += defizit - ueberschuss += ueber - monatsdetail[monat] = { - "bedarf_kWh": bedarf, - "pv_kWh": erzeugung, - "zukauf_kWh": defizit, - } - return { - "netzbezug_kWh": bezug, - "ueberschuss_kWh": ueberschuss, - "jahresbedarf_kWh": jahresbedarf, - "netzbezug_anteil": bezug / jahresbedarf if jahresbedarf else 0.0, - "monate": monatsdetail, - } - - -def batterie(w_el_tag: float, cfg: dict) -> dict: - """Batteriegroesse zur Ueberbbrueckung des Tag-/Nacht-Versatzes.""" - b = cfg["batterie"] - nacht_anteil = 1.0 - b["direkt_pv_stunden"] / 24.0 - nutzbar = w_el_tag * nacht_anteil * b["reserve_faktor"] - nominal = nutzbar / b["nutzbar_anteil"] - return { - "nutzbar_kWh": nutzbar, - "nominal_kWh": nominal, - } - - -def rechne(milch_l_tag: float, cfg: dict) -> dict: - q_milch = milch_kuehllast(milch_l_tag, cfg) - wp = waermepumpe(q_milch, cfg) - sp = speicher(wp["speicher_kWh_tag"], cfg) - pv = pv_anlage(wp["strom_kWh_tag"], cfg) - netz = netzbezug(wp["strom_kWh_tag"], pv["kWp"], cfg) - bat = batterie(wp["strom_kWh_tag"], cfg) - return { - "eingang": {"milch_l_tag": milch_l_tag}, - "milch_kuehllast_kWh_tag": q_milch, - "waermepumpe": wp, - "speicher": sp, - "pv": pv, - "netzbezug": netz, - "batterie": bat, - } - - -def runde_auf(wert: float, schritt: float) -> float: - """Auf das naechste Vielfache von 'schritt' aufrunden (praktische Groessen).""" - return math.ceil(wert / schritt) * schritt - - -def vorschlag(e: dict, cfg: dict) -> dict: - """Empfohlene, auf praktische Groessen gerundete Auslegung. - - PV und Batterie sind unabhaengig vom Abholzyklus. Fuer den Speicher wird - der groesste Wochen-Zyklus empfohlen, der noch in einen LKW passt - (beste Transportauslastung, wenigste Fahrten). - """ - wp = e["waermepumpe"] - pv = e["pv"] - bat = e["batterie"] - q_speicher_tag = wp["speicher_kWh_tag"] - - e_kg = pcm_energiedichte_kWh_pro_kg(cfg) - masse_pro_tag = q_speicher_tag / e_kg - nutzlast = cfg["abholung"]["lkw_nutzlast_kg"] - voll_lkw_tage = nutzlast / masse_pro_tag if masse_pro_tag > 0 else float("inf") - - # Kandidaten im Wochenraster, die in einen LKW passen - kandidaten = [] - z = 7 - while z <= voll_lkw_tage: - kandidaten.append((z, speicher(q_speicher_tag, cfg, zyklus=z))) - z += 7 - - if kandidaten: - empf_zyklus = kandidaten[-1][0] - else: - # LKW schon vor einer Woche voll -> haeufige Abholung noetig - empf_zyklus = max(1, math.floor(voll_lkw_tage)) - empf_speicher = speicher(q_speicher_tag, cfg, zyklus=empf_zyklus) - - return { - "pv_kWp": runde_auf(pv["kWp"], 0.5), - "pv_flaeche_m2": runde_auf(pv["flaeche_m2"], 5), - "batterie_kWh": runde_auf(bat["nominal_kWh"], 5), - "voll_lkw_tage": voll_lkw_tage, - "empf_zyklus_tage": empf_zyklus, - "empf_speicher": empf_speicher, - "kandidaten": kandidaten, - } - - -def vorschlag_report(e: dict, cfg: dict) -> str: - v = vorschlag(e, cfg) - sp = v["empf_speicher"] - L = [] - L.append("=" * 62) - L.append(f" EMPFOHLENE AUSLEGUNG ({e['eingang']['milch_l_tag']:.0f} L Milch/Tag)") - L.append("=" * 62) - L.append(f"PV-Anlage ............ ~{v['pv_kWp']:.1f} kWp (~{v['pv_flaeche_m2']:.0f} m2 Dachflaeche)") - L.append(f"Stromspeicher ........ ~{v['batterie_kWh']:.0f} kWh (Nennkapazitaet, LFP)") - L.append(f"Waermespeicher ....... ~{sp['masse_kg']/1000:.1f} t (~{sp['volumen_m3']:.1f} m3 {cfg['pcm']['name']})") - L.append(f"Abholzyklus .......... alle {v['empf_zyklus_tage']:.0f} Tage") - L.append("") - L.append(f" -> LKW dann zu {sp['auslastung_gewicht']*100:.0f} % ausgelastet " - f"({sp['energie_pro_zyklus_kWh']:.0f} kWh je Fuhre)") - L.append(f" -> ein voller LKW waere nach {v['voll_lkw_tage']:.0f} Tagen erreicht") - L.append("") - L.append("Abholoptionen (Latentspeicher je Zyklus):") - L.append(f" {'Zyklus':>8} | {'Masse':>8} | {'Volumen':>8} | {'LKW-Ausl.':>9}") - L.append(f" {'-'*8}-+-{'-'*8}-+-{'-'*8}-+-{'-'*9}") - for z, s in v["kandidaten"]: - marker = " <== empfohlen" if z == v["empf_zyklus_tage"] else "" - L.append(f" {z:>5.0f} Tg | {s['masse_kg']/1000:>6.1f} t | " - f"{s['volumen_m3']:>6.1f} m3 | {s['auslastung_gewicht']*100:>7.0f} %{marker}") - if not v["kandidaten"]: - L.append(" (LKW bereits vor einer Woche voll - haeufigere Abholung noetig)") - L.append("=" * 62) - return "\n".join(L) - - -def report(e: dict, cfg: dict) -> str: - q = e["milch_kuehllast_kWh_tag"] - wp = e["waermepumpe"] - sp = e["speicher"] - pv = e["pv"] - netz = e["netzbezug"] - bat = e["batterie"] - z = cfg["abholung"]["zyklus_tage"] - L = [] - L.append("=" * 62) - L.append(f" AUSLEGUNG WAERMERUECKGEWINNUNG MILCHKUEHLUNG") - L.append(f" Eingang: {e['eingang']['milch_l_tag']:.0f} L Milch/Tag" - f" ({cfg['milch']['temp_ein_C']:.0f} -> {cfg['milch']['temp_aus_C']:.0f} C)") - L.append("=" * 62) - L.append(f"Kuehllast der Milch ........... {q:8.1f} kWh/Tag") - L.append("") - L.append("WAERMEPUMPE (COP {:.2f}, Laufzeit {:.0f} h/Tag)".format( - cfg["waermepumpe"]["cop"], cfg["waermepumpe"]["laufzeit_h_pro_tag"])) - L.append(f" Kaelteleistung (Verdampfer) . {wp['kaelteleistung_kW']:8.1f} kW") - L.append(f" Heizleistung (Kondensator) .. {wp['heizleistung_kW']:8.1f} kW") - L.append(f" el. Leistung ................ {wp['elektrische_leistung_kW']:8.1f} kW") - L.append(f" Strombedarf ................. {wp['strom_kWh_tag']:8.1f} kWh/Tag") - L.append(f" Waerme in den Speicher ...... {wp['speicher_kWh_tag']:8.1f} kWh/Tag") - L.append("") - L.append("PV-ANLAGE (Jahresbilanz)") - L.append(f" Jahresstrombedarf WP ........ {pv['jahresbedarf_kWh']:8.0f} kWh/a") - L.append(f" Leistung .................... {pv['kWp']:8.1f} kWp") - L.append(f" Dachflaeche ................. {pv['flaeche_m2']:8.0f} m2") - L.append("") - L.append(f"NETZBEZUG (durch Wintermonate, Monatsbilanz)") - L.append(f" Netzbezug ................... {netz['netzbezug_kWh']:8.0f} kWh/a" - f" = {netz['netzbezug_anteil']*100:.0f} %") - L.append(f" Sommerueberschuss ........... {netz['ueberschuss_kWh']:8.0f} kWh/a") - L.append("") - L.append("BATTERIE (Tag-/Nacht-Ueberbbrueckung)") - L.append(f" nutzbare Kapazitaet ......... {bat['nutzbar_kWh']:8.1f} kWh") - L.append(f" Nennkapazitaet .............. {bat['nominal_kWh']:8.1f} kWh") - L.append("") - L.append(f"LATENTSPEICHER ({cfg['pcm']['name']}, Modus: {cfg['pcm']['modus']})") - L.append(f" Energiedichte ............... {sp['energiedichte_kWh_pro_kg']*1000:8.1f} Wh/kg") - L.append(f" Energie je Zyklus ({z:>2} Tage) . {sp['energie_pro_zyklus_kWh']:8.0f} kWh") - L.append(f" Masse ....................... {sp['masse_kg']:8.0f} kg") - L.append(f" Volumen ..................... {sp['volumen_m3']:8.1f} m3") - L.append(f" LKW-Auslastung Gewicht ...... {sp['auslastung_gewicht']*100:8.0f} %") - L.append(f" LKW-Auslastung Volumen ...... {sp['auslastung_volumen']*100:8.0f} %") - L.append(f" bindende Grenze ............. {sp['bindende_grenze']:>8}") - passt = "ja" if sp["passt_in_einen_lkw"] else "NEIN -> mehrere Behaelter noetig" - L.append(f" passt in einen LKW .......... {passt:>8}") - L.append(f" voller LKW voll nach ........ {sp['voller_lkw_tausch_tage']:8.1f} Tagen") - L.append("=" * 62) - return "\n".join(L) - - -def main() -> None: - ap = argparse.ArgumentParser(description=__doc__, - formatter_class=argparse.RawDescriptionHelpFormatter) - ap.add_argument("milch_l_tag", type=float, help="Milchmenge pro Tag in Litern") - ap.add_argument("--config", type=Path, default=Path(__file__).with_name("config.json"), - help="Pfad zur JSON-Konfiguration (Standard: config.json)") - ap.add_argument("--json", type=Path, default=None, - help="Ergebnis zusaetzlich als JSON in diese Datei schreiben") - ap.add_argument("--suggest-size", action="store_true", - help="Kompakte Empfehlung (gerundete Groessen + Abholzyklus) statt Detailreport") - args = ap.parse_args() - - cfg = lade_config(args.config) - ergebnis = rechne(args.milch_l_tag, cfg) - - if args.suggest_size: - print(vorschlag_report(ergebnis, cfg)) - ergebnis["vorschlag"] = vorschlag(ergebnis, cfg) - else: - print(report(ergebnis, cfg)) - - if args.json: - with args.json.open("w", encoding="utf-8") as f: - json.dump(ergebnis, f, ensure_ascii=False, indent=2) - print(f"\nErgebnis geschrieben: {args.json}") - - -if __name__ == "__main__": - main() diff --git a/bin/auslegung.bat b/bin/auslegung.bat index e4251e0..acd1b60 100644 --- a/bin/auslegung.bat +++ b/bin/auslegung.bat @@ -6,4 +6,6 @@ IF NOT DEFINED MILCH_HOME ( ) IF NOT DEFINED MILCH_LIB SET MILCH_LIB=%MILCH_HOME%\lib IF NOT DEFINED MILCH_CFG SET MILCH_CFG=%MILCH_HOME%\cfg -python "%MILCH_LIB%\auslegung.py" --config "%MILCH_CFG%\config.json" %* +IF NOT DEFINED MILCH_IN SET MILCH_IN=%MILCH_HOME%\in +IF NOT DEFINED MILCH_OUT SET MILCH_OUT=%MILCH_HOME%\out +python "%MILCH_LIB%\auslegung.py" --config "%MILCH_CFG%\config.json" --out "%MILCH_OUT%" %* diff --git a/bin/auslegung.sh b/bin/auslegung.sh index 4b0c27d..e98cf47 100755 --- a/bin/auslegung.sh +++ b/bin/auslegung.sh @@ -4,4 +4,6 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" MILCH_HOME="${MILCH_HOME:-$(dirname "$SCRIPT_DIR")}" MILCH_LIB="${MILCH_LIB:-$MILCH_HOME/lib}" MILCH_CFG="${MILCH_CFG:-$MILCH_HOME/cfg}" -exec python3 "$MILCH_LIB/auslegung.py" --config "$MILCH_CFG/config.json" "$@" +MILCH_IN="${MILCH_IN:-$MILCH_HOME/in}" +MILCH_OUT="${MILCH_OUT:-$MILCH_HOME/out}" +exec python3 "$MILCH_LIB/auslegung.py" --config "$MILCH_CFG/config.json" --out "$MILCH_OUT" "$@" diff --git a/bin/setenv.bat b/bin/setenv.bat index 50c9eba..08a5b3b 100644 --- a/bin/setenv.bat +++ b/bin/setenv.bat @@ -5,3 +5,5 @@ IF "%MILCH_HOME:~-1%"=="\" SET MILCH_HOME=%MILCH_HOME:~0,-1% SET MILCH_BIN=%MILCH_HOME%\bin SET MILCH_LIB=%MILCH_HOME%\lib SET MILCH_CFG=%MILCH_HOME%\cfg +SET MILCH_IN=%MILCH_HOME%\in +SET MILCH_OUT=%MILCH_HOME%\out diff --git a/bin/setenv.sh b/bin/setenv.sh index 2c77c0b..397cd02 100755 --- a/bin/setenv.sh +++ b/bin/setenv.sh @@ -4,3 +4,5 @@ export MILCH_HOME="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" export MILCH_BIN="$MILCH_HOME/bin" export MILCH_LIB="$MILCH_HOME/lib" export MILCH_CFG="$MILCH_HOME/cfg" +export MILCH_IN="$MILCH_HOME/in" +export MILCH_OUT="$MILCH_HOME/out" diff --git a/cfg/config.json b/cfg/config.json index e489599..eb9fe4e 100644 --- a/cfg/config.json +++ b/cfg/config.json @@ -59,5 +59,12 @@ "ww_kind_kWh_tag": 1.0, "heizbedarf_winter_kWh_tag": 60.0, "_kommentar": "Warmwasser je Person/Tag. heizbedarf_winter = max. taeglicher Raumheizbedarf eines Haushalts im tiefsten Winter, stark gebaeudeabhaengig (Neubau/saniert ~40, Altbau ~100). Auf 0 setzen, wenn nur Warmwasser betrachtet werden soll." + }, + + "brennstoffe": { + "gas": {"preis_ct_pro_kWh": 11.0, "wirkungsgrad": 0.95}, + "oel": {"preis_ct_pro_kWh": 11.5, "wirkungsgrad": 0.90}, + "pellets": {"preis_ct_pro_kWh": 7.0, "wirkungsgrad": 0.88}, + "_kommentar": "Vergleichswert: was der Kaeufer fuer dieselbe Nutzwaerme aus fossilen/Holz-Brennstoffen zahlen wuerde. preis_ct_pro_kWh = Brennstoffpreis je kWh Brennstoffenergie (Stand Juni 2026, Deutschland: Gas ~11, Oel ~11.5 (=115 ct/L bei 10 kWh/L), Pellets ~7 (=350 EUR/t bei 5 kWh/kg)). wirkungsgrad = Kesselwirkungsgrad. Nutzwaermekosten = preis/wirkungsgrad." } } diff --git a/config.json b/config.json deleted file mode 100644 index f218b87..0000000 --- a/config.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "_beschreibung": "Annahmen fuer die Auslegung der Waermerueckgewinnung aus der Milchkuehlung. Eine Variante laesst sich nachrechnen, indem man diese Datei kopiert und Werte aendert.", - - "milch": { - "temp_ein_C": 37.0, - "temp_aus_C": 4.0, - "dichte_kg_pro_L": 1.0, - "spez_waerme_kJ_pro_kgK": 4.186, - "_kommentar": "Milch wird als Wasser gerechnet. Eutertemperatur -> Lagertemperatur." - }, - - "waermepumpe": { - "cop": 2.6, - "laufzeit_h_pro_tag": 18.0, - "_kommentar": "COP fuer den Hub von ~0 C (Verdampfer) auf >58 C (Kondensator). Laufzeit bestimmt die Leistungsauslegung (kW)." - }, - - "pcm": { - "name": "Natriumacetat-Trihydrat", - "schmelzpunkt_C": 58.0, - "latentwaerme_kJ_pro_kg": 264.0, - "dichte_kg_pro_L": 1.3, - "cp_fluessig_kJ_pro_kgK": 2.9, - "cp_fest_kJ_pro_kgK": 1.9, - "modus": "latent_plus_hub", - "ladetemp_C": 80.0, - "entladetemp_C": 58.0, - "_kommentar": "modus = 'latent_only' (nur Phasenwechsel) oder 'latent_plus_hub' (zusaetzlich fuehlbare Waerme durch Ueberhitzung der Schmelze auf ladetemp_C und optionale Unterkuehlung des Feststoffs bis entladetemp_C)." - }, - - "abholung": { - "zyklus_tage": 14, - "lkw_nutzlast_kg": 24000, - "lkw_volumen_m3": 90, - "_kommentar": "zyklus_tage = wie oft der Speicher geleert/abgeholt wird. LKW-Grenzen = Standard-Sattelauflieger." - }, - - "pv": { - "spez_ertrag_kWh_pro_kWp_jahr": 1000, - "flaeche_m2_pro_kWp": 5.0, - "monatsertrag_kWh_pro_kWp": { - "Jan": 30, "Feb": 45, "Mae": 85, "Apr": 115, "Mai": 130, "Jun": 130, - "Jul": 135, "Aug": 120, "Sep": 95, "Okt": 60, "Nov": 32, "Dez": 25 - }, - "_kommentar": "Werte fuer optimal geneigte Suedflaeche, Standort Suedbayern. Summe der Monate ~ spez_ertrag." - }, - - "batterie": { - "direkt_pv_stunden": 9.0, - "nutzbar_anteil": 0.9, - "reserve_faktor": 1.2, - "_kommentar": "direkt_pv_stunden = Stunden/Tag, in denen die WP direkt aus PV laeuft. Rest wird ueber die Batterie ueberbrueckt. reserve_faktor = Sicherheitszuschlag, nutzbar_anteil = Entladetiefe (LFP ~0.9)." - } -} diff --git a/lib/auslegung.py b/lib/auslegung.py index e3e4bff..2847a26 100644 --- a/lib/auslegung.py +++ b/lib/auslegung.py @@ -5,24 +5,30 @@ Eingangsgroesse: zu kuehlende Milchmenge pro Tag (Liter). Ausgabe: erforderliche Groesse von Waermepumpe, PV-Anlage, Latentspeicher (Natriumacetat) und Batterie. -Alle Annahmen stehen in einer JSON-Datei (Standard: config.json), damit sich -Varianten leicht nachrechnen lassen. +Alle Annahmen stehen in einer JSON-Datei (Standard: cfg/config.json), damit sich +Varianten leicht nachrechnen lassen. Eine Datei im in/-Ordner (oder --file) kann +einzelne Werte gezielt ueberschreiben. Aufruf: python auslegung.py 1000 - python auslegung.py 1500 --config variante_winterautark.json - python auslegung.py 1000 --json ergebnis.json + python auslegung.py 1500 --file in/variante_winter.json + python auslegung.py 1000 -v + python auslegung.py 1000 --json ergebnis.json -v """ from __future__ import annotations import argparse +import datetime import json import math +import os from pathlib import Path KJ_PER_KWH = 3600.0 TAGE_PRO_JAHR = 365.0 +PROJECT_ROOT = Path(__file__).parent.parent + # Tage je Monat (Schluessel muessen zu pv.monatsertrag_kWh_pro_kWp passen) MONATSTAGE = { "Jan": 31, "Feb": 28, "Mae": 31, "Apr": 30, "Mai": 31, "Jun": 30, @@ -35,6 +41,17 @@ def lade_config(pfad: Path) -> dict: return json.load(f) +def deep_merge(base: dict, override: dict) -> dict: + """Rekursives Zusammenfuehren zweier Dicts; override gewinnt bei Konflikten.""" + result = base.copy() + for key, value in override.items(): + if key in result and isinstance(result[key], dict) and isinstance(value, dict): + result[key] = deep_merge(result[key], value) + else: + result[key] = value + return result + + def milch_kuehllast(milch_l_tag: float, cfg: dict) -> float: """Taeglich der Milch zu entziehende Waerme in kWh/Tag.""" m = cfg["milch"] @@ -363,31 +380,175 @@ def report(e: dict, cfg: dict) -> str: return "\n".join(L) +def report_md(e: dict, cfg: dict, quelldateien: list[str] | None = None) -> str: + q = e["milch_kuehllast_kWh_tag"] + wp = e["waermepumpe"] + sp = e["speicher"] + pv = e["pv"] + netz = e["netzbezug"] + bat = e["batterie"] + hh = e["haushalte"] + z = cfg["abholung"]["zyklus_tage"] + milch = e["eingang"]["milch_l_tag"] + ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M") + + L = [] + L.append("# Auslegung Waermerueckgewinnung Milchkuehlung") + L.append("") + L.append(f"**Erstellt:** {ts}") + L.append(f"**Eingang:** {milch:.0f} L Milch/Tag" + f" ({cfg['milch']['temp_ein_C']:.0f} -> {cfg['milch']['temp_aus_C']:.0f} C)") + if quelldateien: + L.append(f"**Konfiguration:** {', '.join(quelldateien)}") + L.append("") + L.append(f"**Kuehllast der Milch:** {q:.1f} kWh/Tag") + L.append("") + + L.append("## Waermepumpe") + L.append(f"COP {cfg['waermepumpe']['cop']:.2f}, Laufzeit {cfg['waermepumpe']['laufzeit_h_pro_tag']:.0f} h/Tag") + L.append("") + L.append("| Kenngroesse | Wert |") + L.append("|-------------|------|") + L.append(f"| Kaelteleistung (Verdampfer) | {wp['kaelteleistung_kW']:.1f} kW |") + L.append(f"| Heizleistung (Kondensator) | {wp['heizleistung_kW']:.1f} kW |") + L.append(f"| el. Leistung | {wp['elektrische_leistung_kW']:.1f} kW |") + L.append(f"| Strombedarf | {wp['strom_kWh_tag']:.1f} kWh/Tag |") + L.append(f"| Waerme in den Speicher | {wp['speicher_kWh_tag']:.1f} kWh/Tag |") + L.append("") + + L.append("## PV-Anlage (Jahresbilanz)") + L.append("") + L.append("| Kenngroesse | Wert |") + L.append("|-------------|------|") + L.append(f"| Jahresstrombedarf WP | {pv['jahresbedarf_kWh']:.0f} kWh/a |") + L.append(f"| Leistung | {pv['kWp']:.1f} kWp |") + L.append(f"| Dachflaeche | {pv['flaeche_m2']:.0f} m2 |") + L.append("") + + L.append("## Netzbezug (Wintermonate, Monatsbilanz)") + L.append("") + L.append("| Kenngroesse | Wert |") + L.append("|-------------|------|") + L.append(f"| Netzbezug | {netz['netzbezug_kWh']:.0f} kWh/a = {netz['netzbezug_anteil']*100:.0f} % |") + L.append(f"| Sommerueberschuss | {netz['ueberschuss_kWh']:.0f} kWh/a |") + L.append("") + L.append("### Monatsbilanz") + L.append("") + L.append("| Monat | Bedarf kWh | PV kWh | Zukauf kWh |") + L.append("|-------|-----------|--------|-----------|") + for monat, werte in netz["monate"].items(): + L.append(f"| {monat} | {werte['bedarf_kWh']:.0f} | {werte['pv_kWh']:.0f} | {werte['zukauf_kWh']:.0f} |") + L.append("") + + L.append("## Batterie (Tag-/Nacht-Ueberbbrueckung)") + L.append("") + L.append("| Kenngroesse | Wert |") + L.append("|-------------|------|") + L.append(f"| nutzbare Kapazitaet | {bat['nutzbar_kWh']:.1f} kWh |") + L.append(f"| Nennkapazitaet | {bat['nominal_kWh']:.1f} kWh |") + L.append("") + + L.append(f"## Versorgte Haushalte ({cfg['haushalt']['erwachsene']} Erw. + {cfg['haushalt']['kinder']} Kinder)") + L.append("") + L.append("| Kenngroesse | Wert |") + L.append("|-------------|------|") + L.append(f"| Warmwasser je Haushalt | {hh['ww_pro_haushalt_kWh_tag']:.1f} kWh/Tag |") + L.append(f"| Warmwasser ganzjaehrig | {hh['haushalte_warmwasser']:.1f} Haushalte |") + L.append(f"| max. Heizbedarf Winter | {hh['heizbedarf_winter_kWh_tag']:.1f} kWh/Tag/HH |") + L.append(f"| Winterbedarf (Heiz+WW) | {hh['winter_pro_haushalt_kWh_tag']:.1f} kWh/Tag/HH |") + L.append(f"| im Winter | {hh['haushalte_winter']:.1f} Haushalte |") + L.append("") + + passt = "ja" if sp["passt_in_einen_lkw"] else "NEIN - mehrere Behaelter noetig" + L.append(f"## Latentspeicher ({cfg['pcm']['name']}, Modus: {cfg['pcm']['modus']})") + L.append("") + L.append("| Kenngroesse | Wert |") + L.append("|-------------|------|") + L.append(f"| Energiedichte | {sp['energiedichte_kWh_pro_kg']*1000:.1f} Wh/kg |") + L.append(f"| Energie je Zyklus ({z} Tage) | {sp['energie_pro_zyklus_kWh']:.0f} kWh |") + L.append(f"| Masse | {sp['masse_kg']:.0f} kg |") + L.append(f"| Volumen | {sp['volumen_m3']:.1f} m3 |") + L.append(f"| LKW-Auslastung Gewicht | {sp['auslastung_gewicht']*100:.0f} % |") + L.append(f"| LKW-Auslastung Volumen | {sp['auslastung_volumen']*100:.0f} % |") + L.append(f"| bindende Grenze | {sp['bindende_grenze']} |") + L.append(f"| passt in einen LKW | {passt} |") + L.append(f"| voller LKW nach | {sp['voller_lkw_tausch_tage']:.1f} Tagen |") + L.append(f"| Energieinhalt voller LKW | {sp['voller_lkw_energie_kWh']:.0f} kWh |") + + return "\n".join(L) + + def main() -> None: ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("milch_l_tag", type=float, help="Milchmenge pro Tag in Litern") - ap.add_argument("--config", type=Path, default=Path(__file__).with_name("config.json"), - help="Pfad zur JSON-Konfiguration (Standard: config.json)") + ap.add_argument("--config", type=Path, + default=PROJECT_ROOT / "cfg" / "config.json", + help="Basis-Konfiguration (Standard: cfg/config.json)") + ap.add_argument("--file", type=Path, default=None, + help="Override-Datei, die cfg/config.json selektiv ueberschreibt " + "(Standard: alle *.json im in/-Ordner)") + ap.add_argument("--out", type=Path, default=None, + help="Ausgabeverzeichnis fuer die .md-Datei " + "(Standard: $MILCH_OUT oder out/)") ap.add_argument("--json", type=Path, default=None, help="Ergebnis zusaetzlich als JSON in diese Datei schreiben") ap.add_argument("--suggest-size", action="store_true", help="Kompakte Empfehlung (gerundete Groessen + Abholzyklus) statt Detailreport") + ap.add_argument("-v", "--verbose", action="store_true", + help="Ausgaben an der Konsole anzeigen") args = ap.parse_args() + # Ausgabeverzeichnis bestimmen + 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) + + # In-Ordner bestimmen + in_ordner = Path(os.environ.get("MILCH_IN", PROJECT_ROOT / "in")) + + # Konfiguration laden: Basis + Override cfg = lade_config(args.config) + quelldateien = [args.config.name] + + if args.file: + cfg = deep_merge(cfg, lade_config(args.file)) + quelldateien.append(args.file.name) + elif in_ordner.is_dir(): + for f in sorted(in_ordner.glob("*.json")): + cfg = deep_merge(cfg, lade_config(f)) + quelldateien.append(f.name) + ergebnis = rechne(args.milch_l_tag, cfg) if args.suggest_size: - print(vorschlag_report(ergebnis, cfg)) + text = vorschlag_report(ergebnis, cfg) ergebnis["vorschlag"] = vorschlag(ergebnis, cfg) else: - print(report(ergebnis, cfg)) + text = report(ergebnis, cfg) + + if args.verbose: + print(text) + + # Markdown-Ergebnis in out/ schreiben + ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + md_name = f"auslegung_{args.milch_l_tag:.0f}L_{ts}.md" + md_path = out_ordner / md_name + md_path.write_text(report_md(ergebnis, cfg, quelldateien), encoding="utf-8") + if args.verbose: + print(f"\nMarkdown 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) - print(f"\nErgebnis geschrieben: {args.json}") + if args.verbose: + print(f"JSON geschrieben: {args.json}") if __name__ == "__main__":